feat(web): 用户端骨架——登录、工作台、新建项目、解析进度、读标清单
- packages/api-client:从 openapi.yaml 生成类型;cookie + CSRF / Bearer 两种鉴权、 分片上传、fetch 读事件流(API2) - apps/web:Vue 3 + Vite + TanStack Query,样式只来自 tokens.css / components.css(AR10); 扣点说明放在「开始解析」旁边(红线 9);接口地址、路由方式运行时可配(架构 §5.2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
60
apps/web/src/components/ProjectRow.vue
Normal file
60
apps/web/src/components/ProjectRow.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<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 } };
|
||||
return { name: "checklist", params: { projectId: id } };
|
||||
});
|
||||
/** 「下一步」:骨架阶段只有上传、解析进度、清单三个去处,其余动作先落到清单页 */
|
||||
const next = computed<RouteLocationRaw>(() => 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>
|
||||
Reference in New Issue
Block a user