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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user