feat(addendum): 补遗追加(FR-M2-10)
- 招标文件解析后可以追加补遗 / 澄清:单独解析、扣 1 次;新条目标「补遗 N」进正式清单,清单回到草稿(AD1)
- 同一种日期、时间不同的旧条目标「已被补遗 N 更新」,逐条确认更新或保留两条(AD2、AD3)
- 项目字段的新值在概览「采用 / 不采用」:采用时以补遗为准、清掉人工值;采用截止时间时日期条目一起确认;
没确认前查标用旧值,检查预览提示(AD4)
- 补遗不改项目名称、编号、采购人、代理机构、法律体系(更正公告标题会被抽成项目名称);时间比较按北京时间补时区
- 接口:fields/{key}/pending、checklist-items/{id}/supersession;清单项 origin_document、superseded_by;
关键日期 replaces;解析结果 dates_superseded、fields_pending;检查预览 pending_updates
- 前端:补遗模式的上传页、解析页结果、概览确认、读标页来源筛选和日期确认、检查预览提示
- 决策 AD1–AD6;方案和落地记录 docs/architecture/addendum-plan.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,15 +3,17 @@
|
||||
* 项目概览(原型 project.html,PI1):这一步只做三张卡——项目要点(基本信息字段,带出处,可改、可恢复 AI 值)、
|
||||
* 采购包(所投的包和限价)、关键日期(时间线和倒计时,FR-M2-09)。检查记录、文件、AI 摘要卡留到第 6 步。
|
||||
* 查标按这里的值核对截止时间、限价、法律体系;改了以后重新检查才生效(PI5)。项目已递交、已归档时只读。
|
||||
* 补遗(FR-M2-10):招标文件解析后可以追加;补遗带来的新值在这里「采用 / 不采用」(AD3),没确认前查标用旧值(AD4)。
|
||||
*/
|
||||
import type { Schemas } from "@bid-doctor/api-client";
|
||||
import { Pencil, X } from "@lucide/vue";
|
||||
import { unwrap, type Schemas } from "@bid-doctor/api-client";
|
||||
import { Pencil, Plus, X } from "@lucide/vue";
|
||||
import { useQueryClient } from "@tanstack/vue-query";
|
||||
import { computed, ref } from "vue";
|
||||
import { RouterLink } from "vue-router";
|
||||
import FieldDialog from "@/components/FieldDialog.vue";
|
||||
import PdfViewer, { type ViewerTarget } from "@/components/PdfViewer.vue";
|
||||
import ProjectHead from "@/components/ProjectHead.vue";
|
||||
import { api, errorTitle } from "@/lib/api";
|
||||
import { useCrumbs } from "@/lib/crumbs";
|
||||
import { qk } from "@/lib/keys";
|
||||
import { deadline, FIELD_LABEL, FIELD_ORDER, fieldText, fmtDateTimeFull, fmtYuan } from "@/lib/labels";
|
||||
@@ -40,7 +42,9 @@ const rows = computed(() =>
|
||||
FIELD_ORDER.map((key) => {
|
||||
const f = byKey.value.get(key) ?? null;
|
||||
const col = COLUMN[key];
|
||||
const value = f && f.value != null ? f.value : col && p.value ? (p.value[col] as unknown) : null;
|
||||
// 名称:建项目时填的名称优先于 AI 抽到的(解析不覆盖用户填的名称),和项目头一致
|
||||
const value =
|
||||
key === "name" && p.value && f?.source !== "human" ? p.value.name : f && f.value != null ? f.value : col && p.value ? (p.value[col] as unknown) : null;
|
||||
return { key, f, value };
|
||||
}),
|
||||
);
|
||||
@@ -57,6 +61,24 @@ function onSaved(f: ProjectField, restored: boolean) {
|
||||
toast(restored ? `已恢复${FIELD_LABEL[f.key]}的 AI 值` : `已修改${FIELD_LABEL[f.key]}`);
|
||||
}
|
||||
|
||||
/* ---------- 补遗带来的新值:采用 / 不采用(AD3) ---------- */
|
||||
const resolving = ref<string | null>(null);
|
||||
async function resolvePending(key: Schemas["ProjectFieldKey"], accept: boolean) {
|
||||
resolving.value = key;
|
||||
try {
|
||||
await unwrap(api.POST("/projects/{project_id}/fields/{field_key}/pending", { params: { path: { project_id: props.projectId, field_key: key } }, body: { accept } }));
|
||||
void qc.invalidateQueries({ queryKey: qk.project(props.projectId) });
|
||||
void qc.invalidateQueries({ queryKey: qk.projects });
|
||||
void qc.invalidateQueries({ queryKey: qk.checklistItems(props.projectId) });
|
||||
toast(accept ? `已采用补遗的${FIELD_LABEL[key]}` : `保留原来的${FIELD_LABEL[key]}`);
|
||||
} catch (e) {
|
||||
toast(errorTitle(e), { ok: false });
|
||||
} finally {
|
||||
resolving.value = null;
|
||||
}
|
||||
}
|
||||
const canAddendum = computed(() => !!p.value && p.value.status !== "tracking" && !readOnly.value);
|
||||
|
||||
const viewer = ref<{ doc: { id: string; label: string }; target: ViewerTarget & { id: string } } | null>(null);
|
||||
function openCitation(c: Citation) {
|
||||
viewer.value = { doc: { id: c.document_id, label: c.document_label }, target: { id: c.id, page: c.page_no, quads: c.quads, sha: c.normalized_sha256 } };
|
||||
@@ -70,6 +92,7 @@ function openCitation(c: Citation) {
|
||||
<div class="card-h">
|
||||
<h2 id="h-facts">项目要点</h2>
|
||||
<span class="cap muted r">查标按这里的值核对截止时间、限价和法律体系</span>
|
||||
<RouterLink v-if="canAddendum" class="btn sm" :to="{ name: 'project-new', query: { project: projectId, addendum: '1' } }"><Plus class="ic" :stroke-width="1.5" />追加补遗 / 澄清</RouterLink>
|
||||
</div>
|
||||
<dl class="facts-list">
|
||||
<div v-for="{ key, f, value } in rows" :key="key" class="frow">
|
||||
@@ -79,9 +102,16 @@ function openCitation(c: Citation) {
|
||||
<span v-else class="dash">没写明</span>
|
||||
<span v-if="f?.source === 'human'" class="flag box">已修改</span>
|
||||
<button v-if="f?.citation" type="button" class="cite-src inline" @click="openCitation(f.citation)">
|
||||
第 <span class="fig">{{ f.citation.page_no }}</span> 页
|
||||
<template v-if="f.citation.document_label && f.citation.document_label !== '招标文件'">{{ f.citation.document_label }} </template>第 <span class="fig">{{ f.citation.page_no }}</span> 页
|
||||
</button>
|
||||
<span v-if="f?.pending" class="cap warn-text">补遗带来了新值:{{ fieldText(key, f.pending.value) }}(下一步补遗确认)</span>
|
||||
<span v-if="f?.pending" class="pend-row">
|
||||
<span class="cap warn-text">{{ f.pending.document_label }}:{{ fieldText(key, f.pending.value) }}<template v-if="value != null && value !== ''">(原为 {{ fieldText(key, value) }})</template>,没确认前查标用原值</span>
|
||||
<button v-if="f.pending.citation" type="button" class="cite-src inline" @click="openCitation(f.pending.citation)">{{ f.pending.document_label }} 第 <span class="fig">{{ f.pending.citation.page_no }}</span> 页</button>
|
||||
<template v-if="!readOnly">
|
||||
<button type="button" class="btn sm" :disabled="resolving === key" @click="resolvePending(key, true)">采用</button>
|
||||
<button type="button" class="btn sm text" :disabled="resolving === key" @click="resolvePending(key, false)">不采用</button>
|
||||
</template>
|
||||
</span>
|
||||
</dd>
|
||||
<button v-if="!readOnly" type="button" class="icon-btn sm" :aria-label="`修改${FIELD_LABEL[key]}`" @click="edit(key, f ?? { key, value: value as never, source: 'none' })">
|
||||
<Pencil class="ic" :stroke-width="1.5" />
|
||||
@@ -124,6 +154,8 @@ function openCitation(c: Citation) {
|
||||
<span class="fig when">{{ fmtDateTimeFull(d.at) }}</span>
|
||||
<b>{{ d.label }}</b>
|
||||
<span v-if="d.edited" class="flag box">已修改</span>
|
||||
<span v-if="d.superseded_pending" class="cap warn-text">已被补遗更新,待确认</span>
|
||||
<span v-if="d.replaces" class="cap muted rep">已按{{ d.replaces.document_label }} 更新 · 原为 {{ d.replaces.at ? fmtDateTimeFull(d.replaces.at) : "—" }}</span>
|
||||
<span v-if="d.due && d.due.cls !== 'due-past'" class="cap" :class="d.due.cls || 'muted'">{{ d.due.main }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
@@ -199,6 +231,16 @@ function openCitation(c: Citation) {
|
||||
.warn-text {
|
||||
color: var(--warn-text);
|
||||
}
|
||||
.pend-row {
|
||||
display: flex;
|
||||
flex-basis: 100%;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sp-2);
|
||||
align-items: baseline;
|
||||
}
|
||||
.timeline .rep {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
.none,
|
||||
.note {
|
||||
margin: 0;
|
||||
|
||||
Reference in New Issue
Block a user