fix(intake): streamline workflow node headers

This commit is contained in:
WuYongJie 2026-05-09 13:57:13 +08:00
parent 6a8e2da6d7
commit 94f7c0a302

View File

@ -17,7 +17,7 @@ import {
useRef,
useState,
} from "react";
import { ArrowLeft, Ban, CheckCircle2, Search } from "lucide-react";
import { ArrowLeft, Ban, Search } from "lucide-react";
import CameraScannerModal from "@/components/mobile/CameraScannerModal";
import { ModelPreviewPanel } from "@/components/platform/ModelPreviewPanel";
import Button from "@/components/ui/Button";
@ -1182,8 +1182,6 @@ const PROJECT_STATUS_COPY: Record<
saveMessage: "立项已通过,已展开产品资料、样品评审、基础信息和后续节点",
},
};
const WORKFLOW_DATE_TIME_ZONE = "Asia/Shanghai";
function getAutoProductYear(createdAt?: string) {
const parsedDate = createdAt ? new Date(createdAt) : null;
const sourceDate =
@ -1194,27 +1192,6 @@ function getAutoProductYear(createdAt?: string) {
return String(sourceDate.getFullYear());
}
function formatWorkflowDateTime(value: string) {
const parsedDate = new Date(value);
if (Number.isNaN(parsedDate.getTime())) return value;
const parts = new Intl.DateTimeFormat("zh-CN", {
timeZone: WORKFLOW_DATE_TIME_ZONE,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hourCycle: "h23",
}).formatToParts(parsedDate);
const partMap = Object.fromEntries(
parts.map((part) => [part.type, part.value]),
);
return `${partMap.year}-${partMap.month}-${partMap.day} ${partMap.hour}:${partMap.minute}:${partMap.second}`;
}
const UPLOAD_CATEGORY_OPTIONS: UploadCategoryKey[] = [
"未分类",
@ -8159,21 +8136,6 @@ export default function NonIntegratedDetailClient({
PRODUCT_PRICING_WORKSPACE_CATEGORY,
displayedProductPricingRowsComplete,
);
const activeWorkflowNodeCategory: WorkflowNodeCategory =
documentWorkspaceView === "detail"
? NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY
: activeDocumentWorkspaceCategory;
const activeWorkflowNodeHasContent =
activeWorkflowNodeCategory === NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY
? basicInfoHasContent
: activeDocumentWorkspaceHasContent;
const activeWorkflowNodeApprovedAt =
documentWorkspaceApprovedAt[activeWorkflowNodeCategory] ?? "";
const activeWorkflowNodeIsApproved = isWorkflowNodeConfirmed(
activeWorkflowNodeCategory,
activeWorkflowNodeHasContent,
);
const activeWorkflowNodeNeedsApproval = !activeWorkflowNodeIsApproved;
const basicInfoIsApproved = isWorkflowNodeConfirmed(
NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY,
basicInfoHasContent,
@ -11505,7 +11467,7 @@ export default function NonIntegratedDetailClient({
);
if (!hasSyncedLedgerRecord) {
setSaveMessage("请先点击顶部未确认,确认专利申请后再查看明细");
setSaveMessage("请先确认专利申请后再查看明细");
setSaveMessageType("error");
return;
}
@ -11635,8 +11597,34 @@ export default function NonIntegratedDetailClient({
);
}
async function handleApproveCurrentWorkflowNode() {
await handleConfirmWorkflowNode(activeWorkflowNodeCategory);
function renderBasicInfoConfirmationAction() {
if (mode === "create") return null;
if (basicInfoIsApproved) {
return (
<span
data-testid="basic-info-confirmed"
className={DOCUMENT_WORKSPACE_CONFIRMED_CLASS}
>
已确认
</span>
);
}
return (
<DocumentWorkspaceActionButton
data-testid="basic-info-confirm"
disabled={isReadOnly || !basicInfoHasContent}
title={basicInfoHasContent ? undefined : "基础信息还没有资料,暂不能确认"}
onClick={() =>
void handleConfirmWorkflowNode(
NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY,
)
}
>
确认基础信息
</DocumentWorkspaceActionButton>
);
}
async function handleAbandonCurrentProduct() {
@ -11669,110 +11657,6 @@ export default function NonIntegratedDetailClient({
}
}
function handleReturnToProductDetail() {
if (embedded && onReturnToList) {
onReturnToList();
return;
}
router.push("/intake");
}
function renderWorkflowActionBar() {
if (mode === "create") return null;
const activeCategoryLabel = getDocumentWorkspaceCategoryLabel(
activeWorkflowNodeCategory,
);
const approveDisabled =
isReadOnly ||
!activeWorkflowNodeNeedsApproval ||
!activeWorkflowNodeHasContent;
const approveLabel = activeWorkflowNodeNeedsApproval
? "未确认"
: "已确认";
const approvalTimeText = activeWorkflowNodeApprovedAt
? formatWorkflowDateTime(activeWorkflowNodeApprovedAt)
: activeWorkflowNodeHasContent
? "未确认"
: "无资料";
return (
<div className="sticky top-0 z-20 mb-2 shrink-0 bg-[var(--color-surface-container)]">
<div
className={`grid min-h-[28px] grid-cols-[minmax(0,1fr)_auto] items-center gap-1.5 rounded-apple-lg border px-2.5 py-1 shadow-apple-xs ${
isFlowAbandoned
? "border-zinc-200 bg-zinc-100 text-zinc-500"
: "border-apple-separator bg-apple-bg-primary text-apple-label-primary"
}`}
>
<div className="min-w-0 truncate text-[11px]">
<span className="font-apple-medium">当前节点</span>
<span className="mx-1.5 text-apple-label-tertiary">/</span>
<span>{activeCategoryLabel}</span>
<span className="mx-1.5 text-apple-label-tertiary">/</span>
<span
className={
activeWorkflowNodeApprovedAt
? "text-[#156b3f]"
: "text-apple-label-secondary"
}
>
{approvalTimeText}
</span>
{isFlowAbandoned ? (
<span className="ml-2 rounded-full bg-zinc-200 px-2 py-0.5 text-[11px] text-zinc-600">
已废弃,只能查看和下载
</span>
) : null}
</div>
<div className="flex shrink-0 items-center justify-end gap-1.5 whitespace-nowrap">
<Button
size="small"
data-testid="workflow-approve-current-node"
onClick={() => void handleApproveCurrentWorkflowNode()}
disabled={approveDisabled}
title={
activeWorkflowNodeHasContent
? undefined
: "当前节点还没有资料,暂不能确认"
}
className="!min-h-6 !px-2 !py-0 text-[11px] [&>span]:gap-1"
>
<CheckCircle2 className="h-3 w-3" aria-hidden="true" />
{approveLabel}
</Button>
{canAbandonProduct ? (
<Button
variant="plain"
size="small"
data-testid="workflow-abandon-product"
onClick={() => void handleAbandonCurrentProduct()}
disabled={mode !== "edit" || isFlowAbandoned}
className="!min-h-6 !border-[#f1c4ca] !px-2 !py-0 !text-[#a32135] text-[11px] hover:!bg-[#fff7f8] [&>span]:gap-1"
>
<Ban className="h-3 w-3" aria-hidden="true" />
废弃
</Button>
) : null}
{!linkEntryView ? (
<Button
variant="gray"
size="small"
data-testid="workflow-return-detail"
onClick={handleReturnToProductDetail}
className="!min-h-6 !px-2 !py-0 text-[11px] [&>span]:gap-1"
>
<ArrowLeft className="h-3 w-3" aria-hidden="true" />
返回详情
</Button>
) : null}
</div>
</div>
</div>
);
}
async function handleUploadFilesChange(
event: ChangeEvent<HTMLInputElement>,
forcedCategory?: DocumentWorkspaceCategory,
@ -15545,12 +15429,19 @@ function renderFactoryQualityWorkspace() {
className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-apple-xl border border-apple-separator bg-apple-bg-primary"
>
<div className="border-b border-apple-separator bg-apple-bg-secondary/25 px-3 py-2">
<p className="text-[13px] font-apple-semibold text-apple-label-primary">
开品参考填写
</p>
<p className="mt-0.5 text-[11px] leading-4 text-apple-label-secondary">
开品前先记录参考图、品牌方向、类目、竞品、开品理由和设计卖点。立项通过后再补产品资料、样品评审和完整基础信息。
</p>
<div className="flex flex-wrap items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<p className="text-[13px] font-apple-semibold text-apple-label-primary">
开品参考填写
</p>
<p className="mt-0.5 text-[11px] leading-4 text-apple-label-secondary">
开品前先记录参考图、品牌方向、类目、竞品、开品理由和设计卖点。立项通过后再补产品资料、样品评审和完整基础信息。
</p>
</div>
<div className={DOCUMENT_WORKSPACE_ACTION_GROUP_CLASS}>
{renderDocumentWorkspaceConfirmationAction()}
</div>
</div>
</div>
<div className="min-h-0 flex-1 overflow-y-auto p-2.5">
@ -15801,7 +15692,7 @@ function renderFactoryQualityWorkspace() {
样品评审后判断是否申请专利;申请后记录申请号、结果和申请缴费。
</p>
</div>
<div className="flex w-full shrink-0 items-center justify-end gap-3 sm:w-auto">
<div className="flex w-full shrink-0 flex-wrap items-center justify-end gap-3 sm:w-auto">
<span className="shrink-0 text-xs text-apple-label-secondary">
是否申请专利
</span>
@ -15840,6 +15731,9 @@ function renderFactoryQualityWorkspace() {
);
})}
</div>
<div className={DOCUMENT_WORKSPACE_ACTION_GROUP_CLASS}>
{renderDocumentWorkspaceConfirmationAction()}
</div>
</div>
</div>
</div>
@ -16243,11 +16137,13 @@ function renderFactoryQualityWorkspace() {
先汇总前置资料,再记录会议判断。内容字段后面可以继续按你们真实会议优化。
</p>
</div>
<span
className={`rounded-full px-3 py-1 text-xs font-apple-medium ${statusCopy.badgeClassName}`}
>
{statusCopy.label}
</span>
<div className={DOCUMENT_WORKSPACE_ACTION_GROUP_CLASS}>
<span
className={`inline-flex h-9 items-center rounded-apple-lg border px-3 text-xs font-apple-medium ${statusCopy.badgeClassName}`}
>
{statusCopy.label}
</span>
</div>
</div>
</div>
@ -17540,16 +17436,25 @@ function renderFactoryQualityWorkspace() {
</p>
</div>
{isReadOnly ? null : (
<DocumentWorkspaceActionButton
type="button"
variant="plain"
size="small"
onClick={() =>
handleDeleteReviewIssue(selectedReviewIssue.id)
}
>
删除问题
</DocumentWorkspaceActionButton>
<div className={DOCUMENT_WORKSPACE_ACTION_GROUP_CLASS}>
<DocumentWorkspaceActionButton
type="button"
size="small"
onClick={handleAddReviewIssue}
>
新增问题
</DocumentWorkspaceActionButton>
<DocumentWorkspaceActionButton
type="button"
variant="plain"
size="small"
onClick={() =>
handleDeleteReviewIssue(selectedReviewIssue.id)
}
>
删除问题
</DocumentWorkspaceActionButton>
</div>
)}
</div>
</div>
@ -17669,7 +17574,7 @@ function renderFactoryQualityWorkspace() {
<div className="flex min-h-[260px] flex-1 items-center justify-center">
<WorkspacePreviewEmptyState
title="当前还没有评审问题"
description="点击左侧“新增问题”后,就可以开始填写。"
description="点击当前问题区域的“新增问题”后,就可以开始填写。"
/>
</div>
)}
@ -17774,7 +17679,6 @@ function renderFactoryQualityWorkspace() {
isDocumentWorkspaceMode ? "overflow-hidden p-2" : "overflow-auto p-3"
}`}
>
{renderWorkflowActionBar()}
<div
className={`rounded-apple-xl border border-apple-separator bg-apple-bg-primary shadow-apple-xs ${
isDocumentWorkspaceMode ? "p-2" : "p-4"
@ -17860,6 +17764,22 @@ function renderFactoryQualityWorkspace() {
<SectionCard
title={`${currentProductDisplayName}产品开发明细`}
description="立项后从开品参考和立项评审自动带出基础资料;这里可以继续补正式 SPU、流程和运营配置。"
headerActions={
<div className={DOCUMENT_WORKSPACE_ACTION_GROUP_CLASS}>
{canAbandonProduct ? (
<DocumentWorkspaceActionButton
data-testid="workflow-abandon-product"
onClick={() => void handleAbandonCurrentProduct()}
disabled={mode !== "edit" || isFlowAbandoned}
className="!border-[#f1c4ca] !text-[#a32135] hover:!bg-[#fff7f8]"
>
<Ban className="h-3.5 w-3.5" aria-hidden="true" />
废弃
</DocumentWorkspaceActionButton>
) : null}
{renderBasicInfoConfirmationAction()}
</div>
}
>
<div className="grid gap-3 xl:grid-cols-[220px_minmax(0,1fr)]">
<div className="rounded-apple-lg border border-apple-separator bg-apple-bg-secondary/30 p-3">
@ -20034,12 +19954,6 @@ function renderFactoryQualityWorkspace() {
>
新增轮次
</Button>
<Button
size="small"
onClick={handleAddReviewIssue}
>
新增问题
</Button>
{isCurrentUserAdmin ? (
<Button
variant="plain"