feat(addendum): 补遗追加(FR-M2-10)
Some checks failed
ci / server (push) Has been cancelled
ci / web (push) Has been cancelled
ci / contract (push) Has been cancelled

- 招标文件解析后可以追加补遗 / 澄清:单独解析、扣 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:
YANG JIANKUAN
2026-11-05 00:27:21 +08:00
parent ed6a2a907d
commit 6e0d4c1dd6
27 changed files with 752 additions and 45 deletions

View File

@@ -33,12 +33,14 @@ const facts = computed(() => specFacts(props.item.spec));
<div class="t">{{ item.title }}</div>
<div class="meta">
<span v-if="item.origin === 'ai'" class="tag ai">AI</span>
<span v-else class="tag">{{ item.origin_label || (item.origin === "human" ? "人工添加" : "规则") }}</span>
<span v-else-if="item.origin === 'human' || !item.origin_document || item.origin_document.label === '招标文件'" class="tag">{{ item.origin === "human" ? "人工添加" : "规则" }}</span>
<span v-if="item.origin !== 'human' && item.origin_document && item.origin_document.label !== '招标文件'" class="tag">{{ item.origin_document.label }}</span>
<span v-for="c in item.categories" :key="c" class="tag">{{ category(c) }}</span>
<span class="tag">{{ item.lot_no ? `采购包 ${item.lot_no}` : "通用" }}</span>
<span v-if="pending && item.pending_reason" class="flag">{{ [pendingReason(item.pending_reason), failureText(item.ai_values)].filter(Boolean).join(" · ") }}</span>
<span v-if="item.no_citation_ack" class="flag">人工确认、无出处</span>
<span v-if="item.edited_fields.length" class="flag">已修改</span>
<span v-if="item.superseded_by && !item.superseded_by.confirmed" class="flag">已被{{ item.superseded_by.document_label }} 更新,待确认</span>
</div>
<p v-if="facts.length" class="facts">
<span v-for="f in facts" :key="f">{{ f }}</span>

View File

@@ -213,6 +213,12 @@ async function start() {
本次查:通用条目 + {{ project.data.value.bidding_lots.map((n) => `采购包 ${n}`).join("、") }}
<RouterLink class="lnk" :to="{ name: 'lots', params: { projectId } }">更改所投的包</RouterLink>
</p>
<!-- AD4:补遗带来的新值没确认前查标用旧值;只提示,不阻止检查 -->
<div v-if="pv?.pending_updates" class="alert warn" role="note">
<span class="ico w" aria-hidden="true">!</span>
<p>补遗带来的新值还有 <span class="fig">{{ pv.pending_updates }}</span> 处没确认,这次检查按原来的值。可以先在概览和读标页确认。</p>
<RouterLink class="btn sm" :to="{ name: 'overview', params: { projectId } }">去确认</RouterLink>
</div>
<div v-if="startErr" class="alert warn" role="alert"><span class="ico w" aria-hidden="true">!</span><p>{{ startErr }}</p></div>
</div>

View File

@@ -11,7 +11,7 @@ import { unwrap } from "@bid-doctor/api-client";
import { FileText, Info, Plus, Search } from "@lucide/vue";
import { useQuery, useQueryClient } from "@tanstack/vue-query";
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { RouterLink } from "vue-router";
import { RouterLink, useRoute } from "vue-router";
import ChecklistItemRow from "@/components/ChecklistItemRow.vue";
import CitationBlock from "@/components/CitationBlock.vue";
import ConfirmDialog from "@/components/ConfirmDialog.vue";
@@ -58,13 +58,35 @@ type Cat = "all" | "pending" | "deleted" | ChecklistItem["categories"][number];
const cat = ref<Cat>("all");
const kw = ref("");
const starOnly = ref(false);
/* 补遗(FR-M2-10):按来源文件筛选(解析页「核对补遗」带 ?source=<文档>),只看待确认的日期更新 */
const route = useRoute();
const source = ref(typeof route.query.source === "string" ? route.query.source : "");
const supOnly = ref(false);
const sources = computed(() => {
const m = new Map<string, { id: string; label: string; n: number }>();
for (const i of formal.value) {
const d = i.origin_document;
if (!d) continue;
const e = m.get(d.id) ?? { id: d.id, label: d.label, n: 0 };
e.n++;
m.set(d.id, e);
}
return [...m.values()].sort((a, b) => (a.label === "招标文件" ? -1 : b.label === "招标文件" ? 1 : a.label.localeCompare(b.label, "zh-CN")));
});
const addenda = computed(() => sources.value.filter((x) => x.label !== "招标文件"));
const supPending = (i: ChecklistItem) => !!i.superseded_by && !i.superseded_by.confirmed;
const supCount = computed(() => formal.value.filter(supPending).length);
const deletedQ = useDeletedItems(() => props.projectId, () => cat.value === "deleted");
const deleted = computed(() => deletedQ.data.value ?? []);
const catCount = (c: string) => formal.value.filter((i) => i.categories.includes(c as ChecklistItem["categories"][number])).length;
const cats = computed(() => CATEGORY_ORDER.filter((c) => catCount(c) > 0));
const match = (i: ChecklistItem) => (!starOnly.value || i.importance === "substantive") && (!kw.value || i.title.includes(kw.value.trim()));
const match = (i: ChecklistItem) =>
(!starOnly.value || i.importance === "substantive") &&
(!kw.value || i.title.includes(kw.value.trim())) &&
(!source.value || i.origin_document?.id === source.value) &&
(!supOnly.value || supPending(i));
/** 「全部」视图下每条只出现一次,放在它的第一个分类里(一条要求可以属于多个分类) */
const groups = computed(() => {
@@ -79,7 +101,7 @@ const groups = computed(() => {
.filter((g) => g.rows.length);
});
const listMode = computed(() => (cat.value === "pending" || cat.value === "deleted" ? cat.value : "formal"));
const showTop = computed(() => cat.value === "all" && !kw.value && !starOnly.value);
const showTop = computed(() => cat.value === "all" && !kw.value && !starOnly.value && !source.value && !supOnly.value);
const showPending = computed(() => (cat.value === "all" && showTop.value) || cat.value === "pending");
/** 当前视图里自上而下的条目(J / K 用) */
const visible = computed<ChecklistItem[]>(() => {
@@ -88,6 +110,19 @@ const visible = computed<ChecklistItem[]>(() => {
return cat.value === "pending" ? top : [...top, ...groups.value.flatMap((g) => g.rows)];
});
/** 确认「已被补遗 N 更新」:采用时旧条目删除(可以恢复),保留两条时解除关联(AD3) */
async function resolveSupersession(i: ChecklistItem, accept: boolean) {
try {
await unwrap(api.POST("/checklist-items/{item_id}/supersession", { params: { path: { item_id: i.id } }, body: { accept } }));
void qc.invalidateQueries({ queryKey: qk.checklistItems(props.projectId) });
void qc.invalidateQueries({ queryKey: qk.checklist(props.projectId) });
void qc.invalidateQueries({ queryKey: qk.project(props.projectId) });
toast(accept ? "已按补遗更新,旧条目已删除(可以在「已删除」里恢复)" : "两条都保留,请自己核对");
} catch (e) {
toast(errorTitle(e), { ok: false });
}
}
/* ---------- 标注规则(如「打 ★ 的为实质性要求」)和数量核对 ---------- */
function ruleText(r: MarkingRule): string {
const mk = r.marker === "keyword" ? `「${r.marker_text ?? ""}」字样` : `「${r.marker_text || MARKER[r.marker]}」`;
@@ -289,11 +324,21 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onKey));
</template>
</div>
<div v-if="cl && !readOnly" class="actions">
<RouterLink v-if="tender" class="btn" :to="{ name: 'project-new', query: { project: projectId, addendum: '1' } }"><Plus class="ic" :stroke-width="1.5" />追加补遗</RouterLink>
<button type="button" class="btn" :disabled="!all.length" @click="openDialog('create')"><Plus class="ic" :stroke-width="1.5" />新增条目</button>
<button v-if="cl.status !== 'confirmed'" type="button" class="btn primary" :disabled="!formal.length" @click="confirmOpen = true">确认清单</button>
</div>
</div>
<div v-if="addenda.length" class="alert warn warn-row" role="note">
<span class="ico w" aria-hidden="true">!</span>
<p>
{{ addenda.map((x) => `${x.label} 带来 ${x.n} 条`).join(",") }}<template v-if="supCount">,{{ supCount }} 处日期等你确认</template>。标博士不判断哪些旧条目被补遗改了或作废了,请核对:改了的旧条目可以修改或删除。
</p>
<button v-for="x in addenda" :key="x.id" type="button" class="btn sm" @click="(source = x.id), (cat = 'all')">只看{{ x.label }}</button>
<button v-if="supCount" type="button" class="btn sm" @click="(supOnly = true), (cat = 'all')">看日期更新</button>
</div>
<div v-if="realLots.length > 1" class="lot-scope" aria-label="清单范围">
<span class="cap muted">采购包</span>
<template v-if="lotScope === 'bidding'">
@@ -368,6 +413,11 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onKey));
<input v-model="kw" class="input" placeholder="搜索清单" aria-label="搜索清单" />
</label>
<label class="choice cap"><input v-model="starOnly" type="checkbox" class="ck" />只看 ★</label>
<select v-if="sources.length > 1" v-model="source" class="input sm src" aria-label="来源">
<option value="">全部来源</option>
<option v-for="x in sources" :key="x.id" :value="x.id">{{ x.label }}({{ x.n }})</option>
</select>
<label v-if="supCount" class="choice cap"><input v-model="supOnly" type="checkbox" class="ck" />只看日期更新({{ supCount }})</label>
</div>
<template v-if="showTop">
@@ -416,6 +466,10 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onKey));
@open-citation="(g) => { selId = i.id; openGroup(g); }"
>
<template #actions>
<template v-if="!readOnly && supPending(i)">
<button type="button" class="btn sm" @click="resolveSupersession(i, true)">确认更新(删除旧条目)</button>
<button type="button" class="btn sm text" @click="resolveSupersession(i, false)">保留两条</button>
</template>
<template v-if="!readOnly">
<button type="button" class="btn sm" @click="openDialog('edit', i)">修改</button>
<button type="button" class="btn sm" @click="startPick(i, 'add')">补出处</button>
@@ -485,6 +539,9 @@ onBeforeUnmount(() => window.removeEventListener("keydown", onKey));
</template>
<style scoped>
.lst-tb .src {
width: auto;
}
.lot-scope {
display: flex;
flex-wrap: wrap;

View File

@@ -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;

View File

@@ -110,13 +110,15 @@ async function retry() {
}
const result = computed(() => j.value?.result ?? null);
/** 补遗 / 澄清(FR-M2-10):结果按补遗写,下一步去读标页核对这份补遗的条目 */
const isAddendum = computed(() => !!doc.data.value && doc.data.value.role !== "tender");
</script>
<template>
<div class="content narrow">
<div class="page-title">
<div>
<h1>解析招标文件</h1>
<h1>{{ isAddendum ? `解析${doc.data.value?.label}` : "解析招标文件" }}</h1>
<p v-if="doc.data.value">
{{ doc.data.value.original_filename }}<template v-if="doc.data.value.page_count"> · <span class="fig">{{ doc.data.value.page_count }}</span> 页</template>
</p>
@@ -147,7 +149,14 @@ const result = computed(() => j.value?.result ?? null);
· 其中 <span class="fig">{{ result.items_pending }}</span> 条没找到可靠出处或把握不大,放在「待确认」里</template>
</dd>
<template v-if="result?.items_updated"><dt>更新</dt><dd><span class="fig">{{ result.items_updated }}</span> 条</dd></template>
<template v-if="result?.lots_found"><dt>采购包</dt><dd>识别到 <span class="fig">{{ result.lots_found }}</span> 个</dd></template>
<template v-if="isAddendum">
<dt>日期</dt>
<dd v-if="result?.dates_superseded"><span class="fig">{{ result.dates_superseded }}</span> 处日期有新值,旧条目标「已被{{ doc.data.value?.label }} 更新」,等你确认</dd>
<dd v-else>没有改动已有的日期</dd>
<template v-if="result?.fields_pending"><dt>项目信息</dt><dd><span class="fig">{{ result.fields_pending }}</span> 个字段有新值,在概览里采用或不采用</dd></template>
<dt>清单</dt><dd>回到草稿。标博士不判断哪些旧条目被补遗改了或作废了,核对后重新确认</dd>
</template>
<template v-else-if="result?.lots_found"><dt>采购包</dt><dd>识别到 <span class="fig">{{ result.lots_found }}</span> 个</dd></template>
</dl>
</template>
@@ -191,7 +200,8 @@ const result = computed(() => j.value?.result ?? null);
</template>
<template v-else-if="status === 'succeeded'">
<!-- 识别到 2 个及以上的包时先选包(FR-M2-04);只有 1 个包按不分包处理(PI4) -->
<RouterLink v-if="(result?.lots_found ?? 0) > 1" class="btn primary" :to="{ name: 'lots', params: { projectId } }">选择要投的包</RouterLink>
<RouterLink v-if="isAddendum" class="btn primary" :to="{ name: 'checklist', params: { projectId }, query: { source: doc.data.value?.id } }">核对补遗</RouterLink>
<RouterLink v-else-if="(result?.lots_found ?? 0) > 1" class="btn primary" :to="{ name: 'lots', params: { projectId } }">选择要投的包</RouterLink>
<RouterLink v-else class="btn primary" :to="{ name: 'checklist', params: { projectId } }">查看检查清单</RouterLink>
<span class="r"><span class="cost">已结算 <span class="fig">1</span> 次解析 · {{ j.finished_at ? fmtDateTime(j.finished_at) : "" }}</span></span>
</template>

View File

@@ -4,6 +4,8 @@
* 选好文件就开始分片上传(格式、加密、页数在上传完成时由服务端拦截,不扣点);
* 「开始解析」:建项目 → 把文件挂到项目上 → 发起解析(预扣 1 次,带 expected_charge,API4)。
* 带 ?project=<id> 时给已有的项目补传招标文件(例如上一次上传失败),不再建新项目。
* 再带 &addendum=1 时是追加补遗 / 澄清(FR-M2-10,原型 project-new.html?addendum=1):不填项目名称,文件类型选补遗或澄清答复,
* 单独解析、扣 1 次;新条目标注「补遗 N」并入清单,标博士不判断哪些旧条目已作废,并入后由用户核对。
*/
import { ApiError, unwrap, uploadFile, type Schemas } from "@bid-doctor/api-client";
import { Upload, X } from "@lucide/vue";
@@ -30,7 +32,14 @@ const parse = useQuotaAccount("parse");
const projectId = ref(typeof route.query.project === "string" ? route.query.project : "");
const existing = useProject(() => (route.query.project ? projectId.value : undefined));
useCrumbs(() => [{ label: "工作台", to: { name: "workbench" } }, { label: existing.data.value ? `${existing.data.value.name} · 上传招标文件` : "新建项目" }]);
const addendum = computed(() => !!route.query.project && route.query.addendum === "1");
const role = ref<"addendum" | "clarification">("addendum");
const docWord = computed(() => (addendum.value ? "补遗 / 澄清文件" : "招标文件"));
useCrumbs(() =>
addendum.value
? [{ label: "工作台", to: { name: "workbench" } }, { label: existing.data.value?.name ?? "项目", to: { name: "overview", params: { projectId: projectId.value } } }, { label: "追加补遗" }]
: [{ label: "工作台", to: { name: "workbench" } }, { label: existing.data.value ? `${existing.data.value.name} · 上传招标文件` : "新建项目" }],
);
const limits = computed(() => plans.data.value?.items.find((p) => p.code === me.data.value?.plan.code)?.limits);
const name = ref("");
@@ -90,7 +99,7 @@ const startErr = ref("");
async function start() {
if (up.value.kind !== "done") {
fileErr.value = up.value.kind === "uploading" ? "文件还在上传,传完再开始解析" : "请先上传招标文件";
fileErr.value = up.value.kind === "uploading" ? "文件还在上传,传完再开始解析" : `请先上传${docWord.value}`;
return;
}
const uploadId = up.value.result.id;
@@ -107,7 +116,7 @@ async function start() {
const d = await unwrap(
api.POST("/projects/{project_id}/documents", {
params: { path: { project_id: projectId.value } },
body: { upload_id: uploadId, role: "tender" },
body: { upload_id: uploadId, role: addendum.value ? role.value : "tender" },
}),
);
documentId.value = d.id;
@@ -139,8 +148,9 @@ async function start() {
<div class="content narrow">
<div class="page-title">
<div>
<h1>{{ existing.data.value ? "上传招标文件" : "新建项目" }}</h1>
<p>上传招标文件,标博士会逐条提取要求,每一条都带原文出处。</p>
<h1>{{ addendum ? "追加补遗 / 澄清" : existing.data.value ? "上传招标文件" : "新建项目" }}</h1>
<p v-if="addendum">补遗单独解析,新条目标注「补遗 N」并入清单,日期和项目信息的新值等你确认。标博士不判断哪些旧条目已作废,并入后由你核对。</p>
<p v-else>上传招标文件,标博士会逐条提取要求,每一条都带原文出处。</p>
</div>
</div>
@@ -156,9 +166,16 @@ async function start() {
<label>项目</label>
<span>{{ existing.data.value.name }}</span>
</div>
<div v-if="addendum" class="field">
<label>文件类型</label>
<div class="seg" role="group" aria-label="文件类型">
<button type="button" :aria-pressed="role === 'addendum'" :disabled="busy" @click="role = 'addendum'">补遗 / 更正公告</button>
<button type="button" :aria-pressed="role === 'clarification'" :disabled="busy" @click="role = 'clarification'">澄清答复</button>
</div>
</div>
<div class="field" :class="{ err: fileErr }">
<label id="l-file">招标文件</label>
<label id="l-file">{{ docWord }}</label>
<div
v-if="!fileOf"
class="drop"
@@ -216,7 +233,7 @@ async function start() {
<div class="card-f">
<!-- 红线 9:扣点规则写在按钮旁边 -->
<span class="cost">将扣 <span class="fig">1</span> 次解析;解析失败、超时或取消不扣点<template v-if="parse"> · 剩 <span class="fig">{{ parse.balance }}</span> 次</template></span>
<RouterLink class="btn" :to="{ name: 'workbench' }">取消</RouterLink>
<RouterLink class="btn" :to="addendum ? { name: 'overview', params: { projectId } } : { name: 'workbench' }">取消</RouterLink>
<button type="submit" class="btn primary swap" :class="{ 'is-loading': busy }" :aria-busy="busy" :disabled="busy || up.kind !== 'done'">
<span class="idle">开始解析</span><span class="busy">正在提交…</span>
</button>