feat(ui): 查标第 6 步前端补齐
- 读标页:每条清单项写最近一次检查的结论,点投标侧出处看投标原文;
「已响应」也能反馈判断有误(漏报);证书要求写资质库里的情况
(新接口 GET /projects/{id}/qualification-matches,资质库空时不写)
- 概览:检查记录卡(历次检查、各级问题数、报告下载)、文件卡
- 工作台:「致命」列、统计可点筛选、待处理的致命问题卡(?f= 定位到问题)
- 日期输入加日历弹层(DateInput,Reka UI Calendar)
- 查标页:? 键显示快捷键说明;反馈对话框抽成 FeedbackDialog 共用
- 扫描件提示去掉多余空格
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 项目概览(原型 project.html,PI1):这一步只做三张卡——项目要点(基本信息字段,带出处,可改、可恢复 AI 值)、
|
||||
* 采购包(所投的包和限价)、关键日期(时间线和倒计时,FR-M2-09)。检查记录、文件、AI 摘要卡留到第 6 步。
|
||||
* 项目概览(原型 project.html,PI1):关键日期(时间线和倒计时,FR-M2-09)和采购包、项目要点(基本信息字段,
|
||||
* 带出处,可改、可恢复 AI 值)、检查记录(历次检查的结果和报告)、文件(招标方的文件和投标文件的最新版本)。
|
||||
* 查标按这里的值核对截止时间、限价、法律体系;改了以后重新检查才生效(PI5)。项目已递交、已归档时只读。
|
||||
* 补遗(FR-M2-10):招标文件解析后可以追加;补遗带来的新值在这里「采用 / 不采用」(AD3),没确认前查标用旧值(AD4)。
|
||||
*/
|
||||
import { unwrap, type Schemas } from "@bid-doctor/api-client";
|
||||
import { Pencil, Plus } from "@lucide/vue";
|
||||
import { Download, Pencil, Plus } from "@lucide/vue";
|
||||
import { useQueryClient } from "@tanstack/vue-query";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
import { RouterLink } from "vue-router";
|
||||
@@ -17,8 +17,18 @@ 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";
|
||||
import { useLots, useProject, useProjectDocuments, type Citation, type ProjectField } from "@/lib/queries";
|
||||
import { deadline, FIELD_LABEL, FIELD_ORDER, fieldText, fmtDateTime, fmtDateTimeFull, fmtYuan, SEVERITY, SEVERITY_CLASS } from "@/lib/labels";
|
||||
import {
|
||||
reportFileUrl,
|
||||
useCheckRuns,
|
||||
useLots,
|
||||
useProject,
|
||||
useProjectDocuments,
|
||||
useReports,
|
||||
type Citation,
|
||||
type DocumentInfo,
|
||||
type ProjectField,
|
||||
} from "@/lib/queries";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
||||
const props = defineProps<{ projectId: string }>();
|
||||
@@ -80,6 +90,36 @@ async function resolvePending(key: Schemas["ProjectFieldKey"], accept: boolean)
|
||||
}
|
||||
const canAddendum = computed(() => !!p.value && p.value.status !== "tracking" && !readOnly.value);
|
||||
|
||||
/* ---------- 检查记录:历次检查和对应的报告 ---------- */
|
||||
const runsQ = useCheckRuns(() => props.projectId);
|
||||
const runs = computed(() => runsQ.data.value?.items ?? []);
|
||||
const reportsQ = useReports(() => props.projectId);
|
||||
const reportsByRun = computed(() => {
|
||||
const m = new Map<string, NonNullable<typeof reportsQ.data.value>["items"]>();
|
||||
for (const r of reportsQ.data.value?.items ?? []) m.set(r.check_run_id, [...(m.get(r.check_run_id) ?? []), r]);
|
||||
return m;
|
||||
});
|
||||
const RUN_STATE: Record<string, string> = { queued: "排队中", running: "检查中", failed: "检查失败", cancelled: "已取消" };
|
||||
async function openReport(id: string) {
|
||||
try {
|
||||
window.open(await reportFileUrl(id), "_blank", "noopener");
|
||||
} catch (e) {
|
||||
toast(errorTitle(e), { ok: false });
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- 文件:招标方的文件全列;投标文件每个分册只列最新版本 ---------- */
|
||||
const tenderDocs = computed(() => (docsQ.data.value?.items ?? []).filter((d) => d.role !== "bid_volume"));
|
||||
const bidDocs = computed(() => (docsQ.data.value?.items ?? []).filter((d) => d.role === "bid_volume" && d.is_latest_version !== false));
|
||||
function parseState(d: DocumentInfo): string {
|
||||
const s = d.latest_parse_job?.status;
|
||||
if (s === "succeeded") return "解析成功";
|
||||
if (s === "queued" || s === "running") return "解析中";
|
||||
if (s === "failed") return "解析失败";
|
||||
if (s === "cancelled") return "已取消解析";
|
||||
return d.status === "failed" ? "读取失败" : "未解析";
|
||||
}
|
||||
|
||||
/* 右侧原文(左右布局,同读标页):默认摆招标文件,点出处翻到那一页并高亮;补遗的出处换成那份补遗 */
|
||||
const docsQ = useProjectDocuments(() => props.projectId);
|
||||
const shown = ref<{ doc: { id: string; label: string } | null; target: (ViewerTarget & { id: string }) | null }>({ doc: null, target: null });
|
||||
@@ -142,7 +182,6 @@ 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">
|
||||
@@ -170,6 +209,75 @@ function openCitation(c: Citation) {
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="card flush" aria-labelledby="h-runs">
|
||||
<div class="card-h">
|
||||
<h2 id="h-runs">检查记录</h2>
|
||||
<RouterLink v-if="!readOnly && p && p.status !== 'tracking'" class="btn sm r" :to="{ name: 'bid-check', params: { projectId } }">
|
||||
{{ runs.length ? "上传新版本并检查" : "上传投标文件并检查" }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
<p v-if="!runs.length" class="cap muted none pad">还没有检查过。上传投标文件后,每次检查的结果和导出的报告都列在这里。</p>
|
||||
<table v-else class="data runs">
|
||||
<thead>
|
||||
<tr><th>批次</th><th>时间</th><th>投标文件</th><th>结果</th><th>报告</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="r in runs" :key="r.id">
|
||||
<td>第 <span class="fig">{{ r.run_no }}</span> 次</td>
|
||||
<td class="fig nowrap">{{ fmtDateTime(r.created_at) }}</td>
|
||||
<td>{{ (r.documents ?? []).map((d) => d.label).join("、") || "—" }}</td>
|
||||
<td>
|
||||
<template v-if="r.status === 'succeeded'">
|
||||
<!-- 只列有问题的级别:「致命 0」不该是红色 -->
|
||||
<span class="sevs">
|
||||
<template v-for="s in (['fatal', 'important', 'hint'] as const)" :key="s">
|
||||
<span v-if="r.counts?.[s]" class="sev" :class="SEVERITY_CLASS[s]">{{ SEVERITY[s] }}<span class="fig">{{ r.counts[s] }}</span></span>
|
||||
</template>
|
||||
<span v-if="!r.counts?.fatal && !r.counts?.important && !r.counts?.hint" class="cap muted">没有发现问题</span>
|
||||
</span>
|
||||
<RouterLink class="lnk cap" :to="{ name: 'results', params: { projectId, runId: r.id } }">查看结果</RouterLink>
|
||||
</template>
|
||||
<RouterLink v-else class="lnk cap" :to="{ name: 'results', params: { projectId, runId: r.id } }">{{ RUN_STATE[r.status] ?? r.status }}</RouterLink>
|
||||
</td>
|
||||
<td>
|
||||
<button v-for="rep in reportsByRun.get(r.id) ?? []" :key="rep.id" type="button" class="lnk cap rep" @click="openReport(rep.id)">
|
||||
<Download class="ic" :stroke-width="1.5" />{{ fmtDateTime(rep.created_at) }}
|
||||
</button>
|
||||
<span v-if="!reportsByRun.get(r.id)?.length" class="dash" aria-label="没有导出">—</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="card flush" aria-labelledby="h-files">
|
||||
<div class="card-h">
|
||||
<h2 id="h-files">文件</h2>
|
||||
<RouterLink v-if="canAddendum" class="btn sm r" :to="{ name: 'project-new', query: { project: projectId, addendum: '1' } }"><Plus class="ic" :stroke-width="1.5" />追加补遗 / 澄清</RouterLink>
|
||||
</div>
|
||||
<div class="files">
|
||||
<h3 class="cap muted">招标方</h3>
|
||||
<p v-if="!tenderDocs.length" class="cap muted none">还没有上传招标文件</p>
|
||||
<div v-for="d in tenderDocs" :key="d.id" class="file-row">
|
||||
<span class="ext">{{ d.detected_format.toUpperCase() }}</span>
|
||||
<div>
|
||||
<b>{{ d.label }}</b>
|
||||
<span class="cap muted">{{ d.original_filename }}<template v-if="d.page_count"> · <span class="fig">{{ d.page_count }}</span> 页</template> · {{ parseState(d) }}</span>
|
||||
</div>
|
||||
<span class="acts"><RouterLink class="btn sm text" :to="{ name: 'checklist', params: { projectId } }">读标</RouterLink></span>
|
||||
</div>
|
||||
<h3 class="cap muted">投标文件(加密前的 Word / PDF 稿)</h3>
|
||||
<p v-if="!bidDocs.length" class="cap muted none">还没有上传投标文件</p>
|
||||
<div v-for="d in bidDocs" :key="d.id" class="file-row">
|
||||
<span class="ext">{{ d.detected_format.toUpperCase() }}</span>
|
||||
<div>
|
||||
<b>{{ d.label }}</b>
|
||||
<span class="cap muted"><template v-if="d.page_count"><span class="fig">{{ d.page_count }}</span> 页 · </template>{{ fmtDateTime(d.created_at) }} 上传</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
<aside class="split-doc" aria-label="原文">
|
||||
<DocStage v-model:active="side" :slots="slots" />
|
||||
@@ -217,6 +325,47 @@ function openCitation(c: Citation) {
|
||||
.card-h .r {
|
||||
margin-left: auto;
|
||||
}
|
||||
.pad {
|
||||
padding: 0 var(--card-x) var(--sp-4);
|
||||
}
|
||||
.runs {
|
||||
margin: 0;
|
||||
}
|
||||
.runs td {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
.runs .nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sevs {
|
||||
display: inline-flex;
|
||||
gap: var(--sp-1);
|
||||
margin-right: var(--sp-2);
|
||||
}
|
||||
.rep {
|
||||
display: inline-flex;
|
||||
gap: var(--sp-1);
|
||||
align-items: center;
|
||||
}
|
||||
.rep + .rep {
|
||||
margin-left: var(--sp-2);
|
||||
}
|
||||
.files {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-2);
|
||||
padding: 0 var(--card-x) var(--sp-4);
|
||||
}
|
||||
.files h3 {
|
||||
margin: var(--sp-2) 0 0;
|
||||
font-weight: var(--fw-regular);
|
||||
}
|
||||
.files .file-row b {
|
||||
font-weight: var(--fw-medium);
|
||||
}
|
||||
.files .file-row .cap {
|
||||
display: block;
|
||||
}
|
||||
.facts-list {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user