feat: 读标补齐五类清单与项目基本信息,修复国动样本暴露的 4 个问题
- 文字层 v2:页眉页脚带单独成句;写到页尾的句子和下一页首句接续(出处两页各一条) - 结构识别(最小版):十进制编号、冒号引出的列表找上级条款;上级条款写明无效的,条目照样认定 - 抽取:资格、评分、日期与递交、技术参数、项目基本信息各一份提示词;实质性条款出 v2 (认上级条款、补「做无效响应处理」等写法);各类取值核对;按首句合并; 拆开被合成一条的列举条款;描述标注方式的句子和表头碎片不当条款;* 号标注 - 项目:解析出的名称、编号、采购人、预算、截止时间、分包写回项目,用户改过的不覆盖 - 前端:清单项显示分值、参数要求、日期、证明材料 - 测试 40 个,新增两页虚构样例 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
import { computed } from "vue";
|
||||
import CitationBlock from "./CitationBlock.vue";
|
||||
import { category, IMPORTANCE_MARK, IMPORTANCE_NAME, pendingReason } from "@/lib/labels";
|
||||
import { category, IMPORTANCE_MARK, IMPORTANCE_NAME, pendingReason, specFacts } from "@/lib/labels";
|
||||
import type { ChecklistItem } from "@/lib/queries";
|
||||
|
||||
const props = defineProps<{ item: ChecklistItem; pending?: boolean }>();
|
||||
@@ -16,6 +16,8 @@ const more = computed(() => props.item.citations.length - shown.value.length);
|
||||
const mark = computed(() => IMPORTANCE_MARK[props.item.importance] ?? "");
|
||||
/** AI 摘要和标题不同时才单独显示,避免重复 */
|
||||
const summary = computed(() => (props.item.summary && props.item.summary !== props.item.title ? props.item.summary : ""));
|
||||
/** 结构化取值:分值、参数要求、日期等,都来自原文(取值校验通过的才进正式清单) */
|
||||
const facts = computed(() => specFacts(props.item.spec));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -33,6 +35,10 @@ const summary = computed(() => (props.item.summary && props.item.summary !== pro
|
||||
<span v-if="pending && item.pending_reason" class="flag">{{ pendingReason(item.pending_reason) }}</span>
|
||||
<span v-if="item.no_citation_ack" class="flag">人工确认、无出处</span>
|
||||
</div>
|
||||
<p v-if="facts.length" class="facts">
|
||||
<span v-for="f in facts" :key="f">{{ f }}</span>
|
||||
</p>
|
||||
<p v-if="item.evidence_required" class="evid"><span class="muted">证明材料</span>{{ item.evidence_required }}</p>
|
||||
<p v-if="summary" class="sum"><span class="tag ai">AI 摘要</span>{{ summary }}</p>
|
||||
<div v-if="shown.length" class="cites">
|
||||
<CitationBlock v-for="c in shown" :key="c.id" :citation="c" />
|
||||
@@ -63,6 +69,25 @@ const summary = computed(() => (props.item.summary && props.item.summary !== pro
|
||||
align-items: baseline;
|
||||
margin: var(--sp-2) 0 0;
|
||||
}
|
||||
.facts {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sp-1) var(--sp-3);
|
||||
margin: var(--sp-2) 0 0;
|
||||
font-size: var(--fs-caption);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.facts span + span::before {
|
||||
content: "·";
|
||||
margin-right: var(--sp-3);
|
||||
color: var(--text-disabled);
|
||||
}
|
||||
.evid {
|
||||
display: flex;
|
||||
gap: var(--sp-2);
|
||||
margin: var(--sp-1) 0 0;
|
||||
font-size: var(--fs-caption);
|
||||
}
|
||||
.cites .cap {
|
||||
margin: var(--sp-1) 0 0;
|
||||
}
|
||||
|
||||
@@ -132,3 +132,41 @@ export function fmtSize(bytes: number): string {
|
||||
if (bytes < 1024 * 1024) return `${Math.max(1, Math.round(bytes / 1024))} KB`;
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
/* ---------- 清单项的结构化取值(ItemSpec,openapi.yaml):只把原文里有的值整理出来显示,不推测 ---------- */
|
||||
const PART: Record<string, string> = { price: "价格分", technical: "技术分", commercial: "商务分" };
|
||||
const QUAL_KIND: Record<string, string> = { general: "通用资格", specific: "特定资格", credit: "信用要求" };
|
||||
const OP: Record<string, string> = { ">=": "≥", "<=": "≤", "=": "=", contains: "包含", supports: "支持" };
|
||||
const DATE_KIND: Record<string, string> = {
|
||||
question_deadline: "提疑截止",
|
||||
bid_deadline: "投标截止",
|
||||
opening: "开标",
|
||||
deposit_deadline: "保证金截止",
|
||||
validity: "有效期",
|
||||
};
|
||||
const SUBMISSION: Record<string, string> = { electronic: "电子递交", paper: "纸质递交", ca_encrypted: "CA 加密", mixed: "电子 + 纸质" };
|
||||
|
||||
type Spec = Record<string, Record<string, unknown> | undefined>;
|
||||
const str = (v: unknown): string => (v === null || v === undefined ? "" : String(v));
|
||||
|
||||
export function specFacts(spec: unknown): string[] {
|
||||
const s = (spec ?? {}) as Spec;
|
||||
const out: string[] = [];
|
||||
const q = s.qualification;
|
||||
if (q && QUAL_KIND[str(q.kind)]) out.push(QUAL_KIND[str(q.kind)]!);
|
||||
const sc = s.scoring;
|
||||
if (sc) {
|
||||
if (sc.points) out.push(`分值 ${str(sc.points)}`);
|
||||
if (PART[str(sc.part)]) out.push(PART[str(sc.part)]!);
|
||||
}
|
||||
const t = s.tech_param;
|
||||
if (t && t.value) out.push([str(t.param), OP[str(t.op)] ?? "", `${str(t.value)}${str(t.unit)}`].filter(Boolean).join(" "));
|
||||
const d = s.schedule;
|
||||
if (d) {
|
||||
if (d.at) out.push(`${DATE_KIND[str(d.date_kind)] ?? "时间"} ${fmtDateTime(str(d.at))}`);
|
||||
else if (DATE_KIND[str(d.date_kind)]) out.push(DATE_KIND[str(d.date_kind)]!);
|
||||
if (d.deposit_amount) out.push(`保证金 ${str(d.deposit_amount)}`);
|
||||
if (SUBMISSION[str(d.submission_mode)]) out.push(SUBMISSION[str(d.submission_mode)]!);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user