feat(check): 文件要在、扫描件兜底(C10);响应方式存进清单并可改(C12)
- check/presence.py:「提供《授权委托书》」「须提交投标函」这类要求按标题里有没有这个文件名判断,不调模型。
- 扫描件兜底:要附证明材料的要求没找到、投标文件里有扫描件页时,改为提示级「可能在扫描件里,请人工核对」。
- 响应方式解析时识别、带依据句子存进 checklist.response_modes(迁移 0008);清单页显示、可改(写审计,
重新解析不覆盖人工改过的);查标用存下来的。新接口 PUT /projects/{id}/checklist/response-modes/{category}。
- check_item_result.layer 加 presence(迁移 0007);契约同步默认接受的两种结论。
开发集 17 份:规矩投标文件的致命问题 120 → 51,致命级召回不变(22 / 32)(docs/architecture/check-eval.md §5)。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
import { unwrap } from "@bid-doctor/api-client";
|
||||
import { FileText, Info, Plus, Search } from "@lucide/vue";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { RouterLink } from "vue-router";
|
||||
import ChecklistItemRow from "@/components/ChecklistItemRow.vue";
|
||||
@@ -23,6 +23,7 @@ import { api, errorTitle } from "@/lib/api";
|
||||
import { useChecklistOps, useDeletedItems, type ItemFields } from "@/lib/checklistOps";
|
||||
import { groupCitations, type CitationGroup } from "@/lib/citations";
|
||||
import { useCrumbs } from "@/lib/crumbs";
|
||||
import { qk } from "@/lib/keys";
|
||||
import { category, CATEGORY_ORDER, fmtDateTime, MARKER, projectStatus } from "@/lib/labels";
|
||||
import { useChecklist, useChecklistItems, useProject, type ChecklistItem, type Citation, type MarkingRule } from "@/lib/queries";
|
||||
import { toast } from "@/lib/toast";
|
||||
@@ -108,6 +109,29 @@ function openCitation(c: Citation, tail: Citation[] = []) {
|
||||
target.value = { id: c.id, page: c.page_no, quads: c.quads, sha: c.normalized_sha256, more };
|
||||
}
|
||||
const openGroup = (g: CitationGroup) => openCitation(g.head, g.tail);
|
||||
|
||||
/* ---------- 响应方式(C12):招标文件要求逐条响应,还是只列偏离、没列出的视为接受;查标按它判没找到响应算不算问题 ---------- */
|
||||
const qc = useQueryClient();
|
||||
const MODE_CATEGORY: Record<string, string> = { technical: "技术要求", commercial: "商务要求" };
|
||||
const MODE_STATUS: Record<string, string> = { unknown: "招标文件没写明,按逐条响应处理", conflict: "招标文件两处写法不一致,按逐条响应处理" };
|
||||
const modeBusy = ref("");
|
||||
async function setMode(category: "technical" | "commercial", mode: "explicit" | "implicit") {
|
||||
modeBusy.value = category;
|
||||
try {
|
||||
await unwrap(
|
||||
api.PUT("/projects/{project_id}/checklist/response-modes/{category}", {
|
||||
params: { path: { project_id: props.projectId, category } },
|
||||
body: { mode },
|
||||
}),
|
||||
);
|
||||
await qc.invalidateQueries({ queryKey: qk.checklist(props.projectId) });
|
||||
toast(`已改为「${mode === "implicit" ? "只列偏离,没列出的视为接受" : "逐条响应"}」,下次检查按这个判断`);
|
||||
} catch (e) {
|
||||
toast(errorTitle(e), { ok: false });
|
||||
} finally {
|
||||
modeBusy.value = "";
|
||||
}
|
||||
}
|
||||
function select(i: ChecklistItem, opt: { scroll?: boolean } = {}) {
|
||||
if (picking.value && picking.value.item.id !== i.id) cancelPick();
|
||||
selId.value = i.id;
|
||||
@@ -266,6 +290,26 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onKey));
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="cl?.response_modes?.length" class="modes" aria-label="招标文件的响应方式">
|
||||
<span class="cap muted">响应方式</span>
|
||||
<div v-for="m in cl.response_modes" :key="m.category" class="mode">
|
||||
<label :for="`mode-${m.category}`">{{ MODE_CATEGORY[m.category] }}</label>
|
||||
<select
|
||||
:id="`mode-${m.category}`"
|
||||
class="input sm"
|
||||
:value="m.mode"
|
||||
:disabled="readOnly || modeBusy === m.category"
|
||||
@change="setMode(m.category, ($event.target as HTMLSelectElement).value as 'explicit' | 'implicit')"
|
||||
>
|
||||
<option value="explicit">逐条响应</option>
|
||||
<option value="implicit">只列偏离,没列出的视为接受</option>
|
||||
</select>
|
||||
<button v-if="m.citations.length" type="button" class="plain cap" @click="openCitation(m.citations[0]!)">依据 · 第 {{ m.citations[0]!.page_no }} 页</button>
|
||||
<span v-if="m.origin === 'human'" class="sign cap"><span aria-hidden="true">✓</span><b>{{ m.set_by?.name }}</b><span>改过</span></span>
|
||||
<span v-else-if="MODE_STATUS[m.status]" class="hint-warn cap">{{ MODE_STATUS[m.status] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-for="w in cl?.warnings ?? []" :key="w.code" class="alert warn warn-row" role="note">
|
||||
<span class="ico w" aria-hidden="true">!</span>
|
||||
<p>{{ w.message }}</p>
|
||||
@@ -423,6 +467,31 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onKey));
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 响应方式(C12):清单栏下面一行,每类一个选择框 */
|
||||
.modes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--sp-2) var(--sp-6);
|
||||
margin-bottom: var(--sp-3);
|
||||
}
|
||||
.modes .mode {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
.modes label {
|
||||
font-size: var(--fs-caption);
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.modes .sign {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.modes > .cap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* 清单栏:标题 + 清单状态;右侧页面级操作(主按钮一个,Q12) */
|
||||
.cl-bar {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user