feat(check): 需人工核对(P2)和分项 / 单价上限(C7)
- 需人工核对:份数、密封、递交、CA 签章加密、解密、保证金这类文件里看不出来的要求, 从正式清单按主题合并、带招标原文出处;manual-checks 接口按项目存勾选、写勾选人和时间, 复查沿用;查标页加「需人工核对」一块,覆盖范围写明列了几条 - 「报价」类清单项:读标从采购需求的清单表规则读出分项 / 单价上限(checklist/price.py); 查标交给规则 PRICE.OVER_CEILING_ITEM 比对分项报价表,不交给模型;规则集 2026.11-r2 - 版面工具(分页、分行、归栏)挪到 document/pipeline/layout.py,读标和查标共用 - 评测:price_defects.py 从规矩投标文件派生只带报价问题的 4 份(P01、P02、P03) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,5 +14,6 @@ export const qk = {
|
||||
checkRuns: (projectId: string) => ["check-runs", projectId] as const,
|
||||
checkRun: (id: string) => ["check-run", id] as const,
|
||||
findings: (runId: string) => ["findings", runId] as const,
|
||||
manualChecks: (projectId: string) => ["manual-checks", projectId] as const,
|
||||
itemResults: (runId: string) => ["item-results", runId] as const,
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ export type CheckPreview = Schemas["CheckPreview"];
|
||||
export type CheckRun = Schemas["CheckRun"];
|
||||
export type Finding = Schemas["Finding"];
|
||||
export type CheckItemResult = Schemas["CheckItemResult"];
|
||||
export type ManualCheck = Schemas["ManualCheck"];
|
||||
|
||||
export const fetchMe = () => unwrap(api.GET("/me"));
|
||||
|
||||
@@ -125,6 +126,13 @@ export function useCheckRun(id: MaybeRefOrGetter<string>) {
|
||||
});
|
||||
}
|
||||
|
||||
export function useManualChecks(projectId: MaybeRefOrGetter<string>) {
|
||||
return useQuery({
|
||||
queryKey: computed(() => qk.manualChecks(toValue(projectId))),
|
||||
queryFn: () => unwrap(api.GET("/projects/{project_id}/manual-checks", { params: { path: { project_id: toValue(projectId) } } })),
|
||||
});
|
||||
}
|
||||
|
||||
export function useFindings(runId: MaybeRefOrGetter<string>, enabled: MaybeRefOrGetter<boolean>) {
|
||||
return useQuery({
|
||||
queryKey: computed(() => qk.findings(toValue(runId))),
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
* 说明框在行内展开;处理后显示署名,可以撤销(U 键在第 6 步);
|
||||
* - 每个问题带招标原文和投标原文两侧出处,「未找到」写明找过哪些分册;点出处在右侧抽屉里看原文;
|
||||
* - 只有最近一次检查的问题可以处理,历史批次只读。
|
||||
* TODO(第 6 步):键盘操作、批量处理「提示」、判断有误反馈、需人工核对、导出报告。
|
||||
* - 需人工核对(FR-M3-04、P2):份数、密封、递交、CA 加密等文件里看不出来的要求,按项目勾选,写勾选人和时间;
|
||||
* TODO(第 6 步):键盘操作、批量处理「提示」、判断有误反馈、导出报告。
|
||||
*/
|
||||
import { unwrap } from "@bid-doctor/api-client";
|
||||
import { ChevronDown, ChevronRight, X } from "@lucide/vue";
|
||||
@@ -21,7 +22,7 @@ import { api, errorTitle, isApiError } from "@/lib/api";
|
||||
import { useCrumbs } from "@/lib/crumbs";
|
||||
import { qk } from "@/lib/keys";
|
||||
import { CHECK_STEPS, checkStepIndex, FINDING_ACTION, fmtDateTime, SEVERITY, SEVERITY_CLASS } from "@/lib/labels";
|
||||
import { TERMINAL_JOB, useCheckRun, useFindings, useProject, type Citation, type Finding } from "@/lib/queries";
|
||||
import { TERMINAL_JOB, useCheckRun, useFindings, useManualChecks, useProject, type Citation, type Finding, type ManualCheck } from "@/lib/queries";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
||||
const props = defineProps<{ projectId: string; runId: string }>();
|
||||
@@ -116,6 +117,27 @@ function refresh() {
|
||||
void qc.invalidateQueries({ queryKey: qk.projects });
|
||||
}
|
||||
|
||||
/* ---------- 需人工核对 ---------- */
|
||||
const manualQ = useManualChecks(() => props.projectId);
|
||||
const manual = computed(() => manualQ.data.value?.items ?? []);
|
||||
const manualDone = computed(() => manual.value.filter((m) => m.checked).length);
|
||||
const manualBusy = ref<string | null>(null);
|
||||
const projectReadOnly = computed(() => p.value?.status === "submitted" || p.value?.status === "archived");
|
||||
|
||||
async function toggleManual(m: ManualCheck, checked: boolean) {
|
||||
manualBusy.value = m.check_key;
|
||||
try {
|
||||
await unwrap(
|
||||
api.PUT("/projects/{project_id}/manual-checks", { params: { path: { project_id: props.projectId } }, body: { check_key: m.check_key, checked } }),
|
||||
);
|
||||
void qc.invalidateQueries({ queryKey: qk.manualChecks(props.projectId) });
|
||||
} catch (e) {
|
||||
toast(errorTitle(e), { ok: false });
|
||||
} finally {
|
||||
manualBusy.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 原文抽屉 ---------- */
|
||||
const viewer = ref<{ doc: { id: string; label: string }; target: ViewerTarget & { id: string } } | null>(null);
|
||||
function openCitation(c: Citation) {
|
||||
@@ -262,6 +284,30 @@ const errorText = computed(() => {
|
||||
</template>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<section v-if="manual.length" class="card flush manual" aria-labelledby="h-manual">
|
||||
<div class="card-h">
|
||||
<h2 id="h-manual">需人工核对</h2>
|
||||
<span class="cap">文件里看不出来,递交前请自己核对。勾选按项目保存,复查后还在。</span>
|
||||
<span class="cap r">已核对 <span class="fig">{{ manualDone }}</span> / <span class="fig">{{ manual.length }}</span></span>
|
||||
</div>
|
||||
<article v-for="m in manual" :key="m.check_key" class="finding">
|
||||
<div>
|
||||
<label class="choice">
|
||||
<input type="checkbox" class="ck" :checked="m.checked" :disabled="projectReadOnly || manualBusy === m.check_key" @change="toggleManual(m, ($event.target as HTMLInputElement).checked)" />
|
||||
<span class="f-title">{{ m.label }}</span>
|
||||
</label>
|
||||
<p v-if="m.requirement_text" class="f-text">招标文件:{{ m.requirement_text }}<template v-if="(m.related_items?.length ?? 0) > 1">(同类要求共 <span class="fig">{{ m.related_items?.length }}</span> 条)</template></p>
|
||||
<div v-if="m.checked" class="sign">
|
||||
<span aria-hidden="true">✓</span><b>{{ m.checked_by?.name }}</b>
|
||||
<span v-if="m.checked_at" class="fig">{{ fmtDateTime(m.checked_at) }}</span><span>已核对</span>
|
||||
</div>
|
||||
</div>
|
||||
<aside>
|
||||
<CitationBlock v-if="m.citation" :citation="m.citation" :active="viewer?.target.id === m.citation.id" @open="openCitation(m.citation)" />
|
||||
</aside>
|
||||
</article>
|
||||
</section>
|
||||
</template>
|
||||
<div v-else-if="runQ.error.value" class="alert warn" role="alert"><span class="ico w" aria-hidden="true">!</span><p>{{ errorTitle(runQ.error.value) }}</p></div>
|
||||
</div>
|
||||
@@ -368,6 +414,21 @@ const errorText = computed(() => {
|
||||
background: var(--c-n100);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.manual {
|
||||
margin-top: var(--sp-4);
|
||||
}
|
||||
.manual .card-h {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
.manual .card-h h2 {
|
||||
margin: 0;
|
||||
font-size: var(--fs-body);
|
||||
}
|
||||
.manual .card-h .r {
|
||||
margin-left: auto;
|
||||
}
|
||||
.sign.ign {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user