Files
bid-doctor/apps/web/src/components/ProjectRow.vue
YANG JIANKUAN e7fb797925
Some checks failed
ci / server (push) Has been cancelled
ci / web (push) Has been cancelled
ci / contract (push) Has been cancelled
feat(report): 检查报告 PDF 导出和归档;项目状态流转(递交 / 归档)
- 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>
2026-11-04 20:51:43 +08:00

66 lines
3.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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