重构 Wave3 详情与移动体验(05-28-frontend-ux-wave3-business-mobile)
这个提交包含在:
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { fetchEmployees } from "../api/employees";
|
||||
import { archiveProject, createConfidentialDocumentRef, createProjectCommunicationLog, createProjectDelivery, createProjectMember, createProjectMilestone, createProjectRisk, createProjectTask, fetchProject, fetchProjectClosureChecklist, fetchProjectCommunicationLogs, fetchProjectDeliveries, fetchProjectDocumentRefs, fetchProjectExpensesSummary, fetchProjectMembers, fetchProjectMilestones, fetchProjectPurchaseSummary, fetchProjectRisks, fetchProjectTasks, fetchProjectTravelSummary, fetchProjectWeeklyReports, patchProject, patchProjectClosureChecklistItem, patchProjectCommunicationLog, patchProjectMember, patchProjectMilestone, patchProjectRisk, patchProjectTask, type ConfidentialityLevel, type DocumentStorageType, type ProjectCommunicationChannel, type ProjectClosureChecklistStatus, type ProjectDeliveryRecordType, type ProjectMilestoneStatus, type ProjectRiskLevel, type ProjectStatus, type ProjectTaskPriority, type ProjectTaskStatus } from "../api/projects";
|
||||
import { archiveProject, createConfidentialDocumentRef, createProjectCommunicationLog, createProjectDelivery, createProjectMember, createProjectMilestone, createProjectRisk, createProjectTask, fetchProject, fetchProjectClosureChecklist, fetchProjectCommunicationLogs, fetchProjectDeliveries, fetchProjectDocumentRefs, fetchProjectExpensesSummary, fetchProjectMembers, fetchProjectMilestones, fetchProjectPurchaseSummary, fetchProjectRisks, fetchProjectTasks, fetchProjectTravelSummary, fetchProjectWeeklyReports, patchProject, patchProjectClosureChecklistItem, patchProjectCommunicationLog, patchProjectMember, patchProjectMilestone, patchProjectRisk, patchProjectTask, type ConfidentialityLevel, type DocumentStorageType, type ProjectCommunicationChannel, type ProjectClosureChecklistStatus, type ProjectDeliveryRecordType, type ProjectDetail, type ProjectMilestoneStatus, type ProjectRiskLevel, type ProjectStatus, type ProjectTaskPriority, type ProjectTaskStatus } from "../api/projects";
|
||||
import { fetchContractsRegistry } from "../api/contractsRegistry";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { DetailSideMeta } from "../components/patterns/DetailSideMeta";
|
||||
@@ -136,6 +136,30 @@ function formatDateTime(value: string | null) {
|
||||
return formatDisplayDateTime(value);
|
||||
}
|
||||
|
||||
function resolveProjectNextAction(detail: ProjectDetail) {
|
||||
if (detail.status === "archived") {
|
||||
return "项目已归档,主要用于查询合同、交付、费用和资料引用。";
|
||||
}
|
||||
if (detail.closureSummary.pendingCount > 0) {
|
||||
return `优先补齐 ${detail.closureSummary.pendingCount} 项收口清单,再执行完成或归档。`;
|
||||
}
|
||||
if (detail.closureSummary.facts.openRiskCount > 0) {
|
||||
return `当前有 ${detail.closureSummary.facts.openRiskCount} 个未关闭风险,建议指定责任人和截止时间。`;
|
||||
}
|
||||
if (detail.counts.milestones > 0 && detail.status === "active") {
|
||||
return "按里程碑和任务推进项目,客户沟通、交付记录和周报都在本页留痕。";
|
||||
}
|
||||
if (detail.status === "draft") {
|
||||
return "先补齐负责人、项目经理、时间范围和摘要,再进入执行。";
|
||||
}
|
||||
return "当前项目可继续维护执行记录、费用关联和归档资料引用。";
|
||||
}
|
||||
|
||||
function resolveProjectEvidenceText(detail: ProjectDetail) {
|
||||
const facts = detail.closureSummary.facts;
|
||||
return `交付 ${facts.deliveryRecordCount} · 合同 ${facts.contractCount} · 资料引用 ${facts.confidentialDocRefCount}`;
|
||||
}
|
||||
|
||||
export function ProjectDetailPage() {
|
||||
const { projectId = "" } = useParams();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -538,13 +562,15 @@ export function ProjectDetailPage() {
|
||||
const milestoneItems = milestonesQuery.data?.items ?? detail.milestones;
|
||||
const taskItems = tasksQuery.data?.items ?? detail.tasks;
|
||||
const riskItems = risksQuery.data?.items ?? detail.risks;
|
||||
const projectNextAction = resolveProjectNextAction(detail);
|
||||
const projectEvidenceText = resolveProjectEvidenceText(detail);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageScaffold>
|
||||
<EntityOverviewHero
|
||||
title={`${detail.projectNo} · ${detail.name}`}
|
||||
subtitle="项目概览、执行、留痕与归档在同一工作台内查看和处理。"
|
||||
subtitle="项目状态、下一步、执行留痕和归档边界集中在一个工作台内处理。"
|
||||
meta={
|
||||
<>
|
||||
<StatusBadge status={detail.status} />
|
||||
@@ -569,7 +595,31 @@ export function ProjectDetailPage() {
|
||||
}
|
||||
/>
|
||||
|
||||
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-6">
|
||||
<section className="detail-command-panel" data-testid="project-current-command">
|
||||
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.1fr),minmax(260px,0.9fr)]">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs font-semibold text-zinc-500">当前办理焦点</div>
|
||||
<p className="mt-1 text-sm leading-6 text-zinc-700">{projectNextAction}</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<span className="info-chip">状态:{projectStatusLabel(detail.status)}</span>
|
||||
<span className="info-chip">证据:{projectEvidenceText}</span>
|
||||
{detail.isConfidential ? <span className="badge-warning">涉密资料仅登记引用</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="detail-fact-grid">
|
||||
<div className="detail-fact-card">
|
||||
<div className="detail-fact-card__label">收口待处理</div>
|
||||
<div className="detail-fact-card__value">{detail.closureSummary.pendingCount}</div>
|
||||
</div>
|
||||
<div className="detail-fact-card">
|
||||
<div className="detail-fact-card__label">未关闭风险</div>
|
||||
<div className="detail-fact-card__value">{detail.closureSummary.facts.openRiskCount}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="ops-status-strip grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<MetricCard title="成员" value={counts?.members ?? 0} note="执行角色与观察者" />
|
||||
<MetricCard title="里程碑" value={counts?.milestones ?? 0} note="用于跟踪关键节点" />
|
||||
<MetricCard title="任务" value={counts?.tasks ?? 0} note="按优先级与状态推进" />
|
||||
@@ -580,7 +630,7 @@ export function ProjectDetailPage() {
|
||||
</section>
|
||||
|
||||
{(detail.status === "completed" || detail.status === "archived") && detail.closureSummary.pendingCount > 0 ? (
|
||||
<div className="card-std border-amber-200 bg-amber-50 p-4" data-testid="project-closure-warning">
|
||||
<div className="ops-inline-alert border-amber-200 bg-amber-50 text-amber-800" data-testid="project-closure-warning">
|
||||
<div className="font-medium text-zinc-900">项目已进入收口状态,但仍有 {detail.closureSummary.pendingCount} 项待处理</div>
|
||||
<div className="mt-1 text-sm text-zinc-600">本轮是软约束:允许完成或归档,但请尽快补齐交付、费用、合同与交接说明。</div>
|
||||
</div>
|
||||
|
||||
在新工单中引用
屏蔽一个用户