- report 模块:导出 → worker 用 Gotenberg 把 HTML 转成 A4 PDF → 按项目数据密钥加密存储 → report
(不可改、存哈希);report_job 记生成过程(迁移 0009),完成推 report.ready、发通知
- 报告首页:项目信息、投标文件版本、检查依据、规则版本、导出人、检查覆盖范围、汇总、免责声明;
问题明细带两侧出处和处理人;附录为需人工核对的勾选结果;模板文字不出现「通过 / 合格 / 保证」
- 前端「报告」标签页:导出选项、内嵌预览、历次报告下载;工作台「导出检查报告」直达
- 项目状态流转 POST /projects/{id}/transitions:致命未处理也能递交(确认框说明),
递交后只读,撤销归档回到归档前的状态,都写审计
- 通知补上检查、报告的标题
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
66 lines
3.4 KiB
Vue
66 lines
3.4 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* 工作台项目行(设计规范 §7.9,原型 app.css B20):整行可点,「下一步」单独可点。
|
||
* 列:项目 · 投标截止 · 状态(含就绪)· 待确认 · 下一步。
|
||
*/
|
||
import { ChevronRight } from "@lucide/vue";
|
||
import { computed } from "vue";
|
||
import { RouterLink, type RouteLocationRaw } from "vue-router";
|
||
import { deadline, nextAction, projectStatus, READY_TIP } from "@/lib/labels";
|
||
import type { ProjectListItem } from "@/lib/queries";
|
||
|
||
const props = defineProps<{ project: ProjectListItem }>();
|
||
|
||
const p = computed(() => props.project);
|
||
const due = computed(() => deadline(p.value.deadline_at));
|
||
const sub = computed(() => [p.value.purchaser, p.value.code].filter(Boolean).join(" · "));
|
||
const job = computed(() => p.value.active_job ?? null);
|
||
|
||
/** 项目的默认入口:解析中去进度页;还没上传招标文件去上传;要检查去上传投标文件;检查过去查标;其余去清单 */
|
||
const home = computed<RouteLocationRaw>(() => {
|
||
const id = p.value.id;
|
||
if (job.value?.kind === "parse_job") return { name: "parse", params: { projectId: id, jobId: job.value.id } };
|
||
if (p.value.next_action === "upload_tender") return { name: "project-new", query: { project: id } };
|
||
if (p.value.next_action === "upload_bid" || p.value.next_action === "run_check") return { name: "bid-check", params: { projectId: id } };
|
||
if (p.value.latest_check_run_id && (p.value.next_action === "handle_fatal" || p.value.next_action === "export_report"))
|
||
return { name: "results", params: { projectId: id, runId: p.value.latest_check_run_id } };
|
||
return { name: "checklist", params: { projectId: id } };
|
||
});
|
||
/** 「下一步」:导出报告去报告页,其余同默认入口 */
|
||
const next = computed<RouteLocationRaw>(() =>
|
||
p.value.next_action === "export_report" ? { name: "report", params: { projectId: p.value.id } } : home.value,
|
||
);
|
||
</script>
|
||
|
||
<template>
|
||
<li class="prow">
|
||
<span class="pt">
|
||
<RouterLink class="pname" :to="home" :title="p.name">{{ p.name }}</RouterLink>
|
||
<span class="psub">
|
||
<template v-if="job">{{ job.label || "任务进行中" }} <span class="fig">{{ job.progress ?? 0 }}%</span><template v-if="sub"> · </template></template>{{ sub }}<template v-if="!job && !sub">招标文件信息待解析</template>
|
||
</span>
|
||
</span>
|
||
<span class="dl">
|
||
<template v-if="due"><b :class="due.cls">{{ due.main }}</b><small>{{ due.sub }}</small></template>
|
||
<span v-else class="dash" aria-label="未填写">—</span>
|
||
</span>
|
||
<span class="st">
|
||
<span class="tag">{{ projectStatus(p.status) }}</span>
|
||
<template v-if="p.readiness.ready === true">
|
||
<span class="hint-u tip-below" tabindex="0" :data-tip="READY_TIP">就绪</span>
|
||
</template>
|
||
<template v-else-if="p.readiness.ready === false">
|
||
<span class="hint-u tip-below" tabindex="0" :data-tip="`还有 ${p.readiness.open_fatal_count} 个致命问题没处理。不影响导出,决定权在你。`">未就绪</span>
|
||
</template>
|
||
</span>
|
||
<span class="c">
|
||
<span v-if="p.pending_item_count" class="fig" :aria-label="`待确认 ${p.pending_item_count} 条`">{{ p.pending_item_count }}</span>
|
||
<span v-else class="dash" aria-label="无">—</span>
|
||
</span>
|
||
<span>
|
||
<RouterLink class="nx" :to="next"><span>{{ nextAction(p.next_action) }}</span><ChevronRight class="ic" :stroke-width="1.5" /></RouterLink>
|
||
</span>
|
||
<span></span>
|
||
</li>
|
||
</template>
|