refactor(intake): extract product pricing workspace

This commit is contained in:
WuYongJie 2026-06-09 14:23:03 +08:00
parent 3799702d22
commit 1dca970937
2 changed files with 283 additions and 232 deletions

View File

@ -44,6 +44,7 @@ import { DocumentAttachmentWorkspace } from "./DocumentAttachmentWorkspace";
import { OpeningReferenceWorkspace } from "./OpeningReferenceWorkspace";
import { PatentApplicationWorkspace } from "./PatentApplicationWorkspace";
import { PlanningLayoutWorkspace } from "./PlanningLayoutWorkspace";
import { ProductPricingWorkspace } from "./ProductPricingWorkspace";
import { ProjectReviewWorkspace } from "./ProjectReviewWorkspace";
import { ProductDataWorkspace } from "./ProductDataWorkspace";
import { QualityInspectionWorkspace } from "./QualityInspectionWorkspace";
@ -18596,241 +18597,19 @@ export default function NonIntegratedDetailClient({
}
function renderProductPricingWorkspace() {
const renderReadOnlyCell = (value: string) => (
<div className="min-w-[112px] rounded border border-apple-separator bg-apple-bg-secondary px-2 py-1.5 text-xs text-apple-label-primary">
{value || "-"}
</div>
);
const getVirtualInventoryStatusMeta = (row: ProductPricingDisplayRow) => {
const virtualInventorySkipTitle =
row.jstVirtualInventoryError || "无需推虚拟库存";
const normalizedVirtualInventoryError =
row.jstVirtualInventoryError
?.replace(/[\u2010-\u2015\u2212\uFE58\uFE63\uFF0D]/g, "-")
.replace(/\s+/g, "") ?? "";
const isVirtualInventorySkipReason =
normalizedVirtualInventoryError.includes("未包含成品代发仓-虚拟库存") ||
normalizedVirtualInventoryError.includes("未命中成品代发仓-虚拟库存") ||
normalizedVirtualInventoryError.includes("真实库存") ||
normalizedVirtualInventoryError.includes("无需推虚拟库存");
const skippedMeta = {
label: "无需库存",
tone: "bg-apple-bg-secondary text-apple-label-secondary",
title: virtualInventorySkipTitle,
};
switch (row.jstVirtualInventoryStatus) {
case "pending":
return {
label: "库存待推",
tone: "bg-[#e7f1ff] text-[#1764ad]",
title: row.jstVirtualInventoryScheduledAt
? `计划 ${row.jstVirtualInventoryScheduledAt} 推送`
: "已生成虚拟库存待推送",
};
case "pushed":
return {
label: "库存已推",
tone: "bg-[#dff7e8] text-[#137a38]",
title: row.jstVirtualInventoryPushedAt
? `已于 ${row.jstVirtualInventoryPushedAt} 推送`
: "虚拟库存已推送",
};
case "failed":
if (isVirtualInventorySkipReason) return skippedMeta;
return {
label: "库存失败",
tone: "bg-[#ffe3df] text-[#b42318]",
title: row.jstVirtualInventoryError || "虚拟库存推送失败",
};
case "skipped":
return skippedMeta;
default:
return null;
}
};
const adjustCostPrice = (row: ProductPricingDisplayRow, offset: number) => {
if (isReadOnly || isProductPricingNodeLocked) return;
const currentValue = Number(row.costPrice);
const nextValue = Math.max(
0,
(Number.isFinite(currentValue) ? currentValue : 0) + offset,
);
updateProductPricingRow(row.skuId, "costPrice", String(nextValue));
};
return (
<div
data-testid="product-pricing-workspace"
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 px-3 py-2">
<div className="flex flex-wrap items-center gap-2">
{renderInheritedSkuBatchTabs()}
</div>
</div>
<div className="min-h-0 min-w-0 flex-1 overflow-hidden p-3">
{productPricingRows.length === 0 ? (
<div className="flex min-h-[260px] items-center justify-center rounded-apple-xl border border-dashed border-apple-separator text-sm text-apple-label-secondary">
当前还没有 SKU 信息,先维护 SKU 后再核价。
</div>
) : displayedProductPricingRows.length === 0 ? (
<div className="flex min-h-[260px] items-center justify-center rounded-apple-xl border border-dashed border-apple-separator text-sm text-apple-label-secondary">
当前批次还没有 SKU 信息。
</div>
) : (
<div className="non-integrated-table-scroll non-integrated-workspace-table-scroll h-full max-w-full rounded-apple-lg border border-apple-separator">
<table className="min-w-[1280px] w-full border-collapse text-xs">
<thead className="bg-apple-bg-secondary/40 text-apple-label-secondary">
<tr>
{[
"型号",
"货号",
"条码",
"套件量",
"规格名称",
"供应商名称",
"成本价",
"是否录入系统",
].map((label) => (
<th
key={label}
className="border-b border-apple-separator px-3 py-2 text-left font-apple-medium"
>
{label}
</th>
))}
</tr>
</thead>
<tbody>
{displayedProductPricingRows.map((row) => {
const isSystemEntered =
isProductPricingRowSystemEntered(row);
const pricingReadyForFuturePush =
row.costPrice.trim().length > 0;
const virtualInventoryStatusMeta =
getVirtualInventoryStatusMeta(row);
return (
<tr
key={row.skuId}
className="border-b border-apple-separator last:border-b-0"
>
<td className="px-3 py-2">
{renderReadOnlyCell(row.model)}
</td>
<td className="px-3 py-2">
{renderReadOnlyCell(row.skuCode)}
</td>
<td className="px-3 py-2">
{renderReadOnlyCell(row.barcode)}
</td>
<td className="px-3 py-2">
{renderReadOnlyCell(row.setCount)}
</td>
<td className="px-3 py-2">
{renderReadOnlyCell(row.specName)}
</td>
<td className="px-3 py-2">
{renderReadOnlyCell(row.supplierName)}
</td>
<td className="px-3 py-2">
<div className="flex min-w-[136px] overflow-hidden rounded border border-apple-separator bg-apple-bg-secondary text-xs text-apple-label-primary">
<button
type="button"
onClick={() => adjustCostPrice(row, -1)}
disabled={isReadOnly || isProductPricingNodeLocked}
className="h-8 w-9 border-r border-apple-separator text-apple-label-secondary transition hover:bg-apple-bg-primary disabled:opacity-40"
aria-label="减少成本价"
>
-
</button>
<input
value={row.costPrice}
onChange={(event) =>
updateProductPricingRow(
row.skuId,
"costPrice",
event.target.value,
)
}
readOnly={isReadOnly || isProductPricingNodeLocked}
placeholder="0"
className="h-8 w-16 bg-transparent px-2 text-center outline-none placeholder:text-apple-label-tertiary"
/>
<button
type="button"
onClick={() => adjustCostPrice(row, 1)}
disabled={isReadOnly || isProductPricingNodeLocked}
className="h-8 w-9 border-l border-apple-separator text-apple-label-secondary transition hover:bg-apple-bg-primary disabled:opacity-40"
aria-label="增加成本价"
>
+
</button>
</div>
</td>
<td className="px-3 py-2">
<div className="flex min-w-[236px] items-center gap-2">
<span
className={`inline-flex h-6 min-w-[52px] items-center justify-center rounded-full px-2 text-[11px] font-apple-medium ${
isSystemEntered
? "bg-[#dff7e8] text-[#137a38]"
: "bg-[#fff3df] text-[#9b6511]"
}`}
>
{isSystemEntered ? "已录入" : "未录入"}
</span>
{virtualInventoryStatusMeta ? (
<span
className={`inline-flex h-6 min-w-[64px] items-center justify-center rounded-full px-2 text-[11px] font-apple-medium ${virtualInventoryStatusMeta.tone}`}
title={virtualInventoryStatusMeta.title}
>
{virtualInventoryStatusMeta.label}
</span>
) : null}
{canUseProductPricingPushControls ? (
<button
type="button"
onClick={() =>
void handleConfirmProductPricingPushRows([row])
}
disabled={
isReadOnly ||
isProductPricingNodeLocked ||
isSystemEntered ||
!pricingReadyForFuturePush ||
!PRODUCT_PRICING_JST_PUSH_ENABLED
}
title={
isSystemEntered
? "当前 SKU 已录入系统,后续以聚水潭回传状态为准"
: PRODUCT_PRICING_JST_PUSH_ENABLED &&
pricingReadyForFuturePush
? "确认推送当前 SKU 到聚水潭"
: pricingReadyForFuturePush
? "聚水潭接口未接入,暂不可确认推送"
: "请先填写成本价"
}
className="inline-flex h-7 min-w-[52px] items-center justify-center rounded-full bg-[#1f8bff] px-3 text-xs font-apple-medium text-white shadow-sm transition hover:bg-[#1478df] disabled:cursor-not-allowed disabled:bg-apple-bg-tertiary disabled:text-apple-label-tertiary disabled:shadow-none"
>
确认推送
</button>
) : null}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
</div>
<ProductPricingWorkspace
allRows={productPricingRows}
displayedRows={displayedProductPricingRows}
inheritedSkuBatchTabs={renderInheritedSkuBatchTabs()}
isReadOnly={isReadOnly}
isNodeLocked={isProductPricingNodeLocked}
canUsePushControls={canUseProductPricingPushControls}
onUpdateRow={updateProductPricingRow}
onConfirmPushRows={handleConfirmProductPricingPushRows}
/>
);
}
function renderArchiveFileTile({
file,
isActive,

View File

@ -0,0 +1,272 @@
"use client";
import type { ReactNode } from "react";
import type { NonIntegratedPricingRow } from "./nonIntegratedStorage";
import {
isProductPricingRowSystemEntered,
PRODUCT_PRICING_JST_PUSH_ENABLED,
type ProductPricingDisplayRow,
} from "./productWorkflowRowHelpers";
type ProductPricingFieldKey = Exclude<keyof NonIntegratedPricingRow, "skuId">;
type ProductPricingWorkspaceProps = {
allRows: ProductPricingDisplayRow[];
displayedRows: ProductPricingDisplayRow[];
inheritedSkuBatchTabs: ReactNode;
isReadOnly: boolean;
isNodeLocked: boolean;
canUsePushControls: boolean;
onUpdateRow: (
skuId: string,
key: ProductPricingFieldKey,
value: string,
) => void;
onConfirmPushRows: (rows: ProductPricingDisplayRow[]) => void | Promise<void>;
};
function ReadOnlyCell({ value }: { value: string }) {
return (
<div className="min-w-[112px] rounded border border-apple-separator bg-apple-bg-secondary px-2 py-1.5 text-xs text-apple-label-primary">
{value || "-"}
</div>
);
}
function getVirtualInventoryStatusMeta(row: ProductPricingDisplayRow) {
const virtualInventorySkipTitle =
row.jstVirtualInventoryError || "无需推虚拟库存";
const normalizedVirtualInventoryError =
row.jstVirtualInventoryError
?.replace(/[\u2010-\u2015\u2212\uFE58\uFE63\uFF0D]/g, "-")
.replace(/\s+/g, "") ?? "";
const isVirtualInventorySkipReason =
normalizedVirtualInventoryError.includes("未包含成品代发仓-虚拟库存") ||
normalizedVirtualInventoryError.includes("未命中成品代发仓-虚拟库存") ||
normalizedVirtualInventoryError.includes("真实库存") ||
normalizedVirtualInventoryError.includes("无需推虚拟库存");
const skippedMeta = {
label: "无需库存",
tone: "bg-apple-bg-secondary text-apple-label-secondary",
title: virtualInventorySkipTitle,
};
switch (row.jstVirtualInventoryStatus) {
case "pending":
return {
label: "库存待推",
tone: "bg-[#e7f1ff] text-[#1764ad]",
title: row.jstVirtualInventoryScheduledAt
? `计划 ${row.jstVirtualInventoryScheduledAt} 推送`
: "已生成虚拟库存待推送",
};
case "pushed":
return {
label: "库存已推",
tone: "bg-[#dff7e8] text-[#137a38]",
title: row.jstVirtualInventoryPushedAt
? `已于 ${row.jstVirtualInventoryPushedAt} 推送`
: "虚拟库存已推送",
};
case "failed":
if (isVirtualInventorySkipReason) return skippedMeta;
return {
label: "库存失败",
tone: "bg-[#ffe3df] text-[#b42318]",
title: row.jstVirtualInventoryError || "虚拟库存推送失败",
};
case "skipped":
return skippedMeta;
default:
return null;
}
}
export function ProductPricingWorkspace({
allRows,
displayedRows,
inheritedSkuBatchTabs,
isReadOnly,
isNodeLocked,
canUsePushControls,
onUpdateRow,
onConfirmPushRows,
}: ProductPricingWorkspaceProps) {
const adjustCostPrice = (row: ProductPricingDisplayRow, offset: number) => {
if (isReadOnly || isNodeLocked) return;
const currentValue = Number(row.costPrice);
const nextValue = Math.max(
0,
(Number.isFinite(currentValue) ? currentValue : 0) + offset,
);
onUpdateRow(row.skuId, "costPrice", String(nextValue));
};
return (
<div
data-testid="product-pricing-workspace"
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 px-3 py-2">
<div className="flex flex-wrap items-center gap-2">
{inheritedSkuBatchTabs}
</div>
</div>
<div className="min-h-0 min-w-0 flex-1 overflow-hidden p-3">
{allRows.length === 0 ? (
<div className="flex min-h-[260px] items-center justify-center rounded-apple-xl border border-dashed border-apple-separator text-sm text-apple-label-secondary">
当前还没有 SKU 信息,先维护 SKU 后再核价。
</div>
) : displayedRows.length === 0 ? (
<div className="flex min-h-[260px] items-center justify-center rounded-apple-xl border border-dashed border-apple-separator text-sm text-apple-label-secondary">
当前批次还没有 SKU 信息。
</div>
) : (
<div className="non-integrated-table-scroll non-integrated-workspace-table-scroll h-full max-w-full rounded-apple-lg border border-apple-separator">
<table className="min-w-[1280px] w-full border-collapse text-xs">
<thead className="bg-apple-bg-secondary/40 text-apple-label-secondary">
<tr>
{[
"型号",
"货号",
"条码",
"套件量",
"规格名称",
"供应商名称",
"成本价",
"是否录入系统",
].map((label) => (
<th
key={label}
className="border-b border-apple-separator px-3 py-2 text-left font-apple-medium"
>
{label}
</th>
))}
</tr>
</thead>
<tbody>
{displayedRows.map((row) => {
const isSystemEntered = isProductPricingRowSystemEntered(row);
const pricingReadyForFuturePush =
row.costPrice.trim().length > 0;
const virtualInventoryStatusMeta =
getVirtualInventoryStatusMeta(row);
return (
<tr
key={row.skuId}
className="border-b border-apple-separator last:border-b-0"
>
<td className="px-3 py-2">
<ReadOnlyCell value={row.model} />
</td>
<td className="px-3 py-2">
<ReadOnlyCell value={row.skuCode} />
</td>
<td className="px-3 py-2">
<ReadOnlyCell value={row.barcode} />
</td>
<td className="px-3 py-2">
<ReadOnlyCell value={row.setCount} />
</td>
<td className="px-3 py-2">
<ReadOnlyCell value={row.specName} />
</td>
<td className="px-3 py-2">
<ReadOnlyCell value={row.supplierName} />
</td>
<td className="px-3 py-2">
<div className="flex min-w-[136px] overflow-hidden rounded border border-apple-separator bg-apple-bg-secondary text-xs text-apple-label-primary">
<button
type="button"
onClick={() => adjustCostPrice(row, -1)}
disabled={isReadOnly || isNodeLocked}
className="h-8 w-9 border-r border-apple-separator text-apple-label-secondary transition hover:bg-apple-bg-primary disabled:opacity-40"
aria-label="减少成本价"
>
-
</button>
<input
value={row.costPrice}
onChange={(event) =>
onUpdateRow(
row.skuId,
"costPrice",
event.target.value,
)
}
readOnly={isReadOnly || isNodeLocked}
placeholder="0"
className="h-8 w-16 bg-transparent px-2 text-center outline-none placeholder:text-apple-label-tertiary"
/>
<button
type="button"
onClick={() => adjustCostPrice(row, 1)}
disabled={isReadOnly || isNodeLocked}
className="h-8 w-9 border-l border-apple-separator text-apple-label-secondary transition hover:bg-apple-bg-primary disabled:opacity-40"
aria-label="增加成本价"
>
+
</button>
</div>
</td>
<td className="px-3 py-2">
<div className="flex min-w-[236px] items-center gap-2">
<span
className={`inline-flex h-6 min-w-[52px] items-center justify-center rounded-full px-2 text-[11px] font-apple-medium ${
isSystemEntered
? "bg-[#dff7e8] text-[#137a38]"
: "bg-[#fff3df] text-[#9b6511]"
}`}
>
{isSystemEntered ? "已录入" : "未录入"}
</span>
{virtualInventoryStatusMeta ? (
<span
className={`inline-flex h-6 min-w-[64px] items-center justify-center rounded-full px-2 text-[11px] font-apple-medium ${virtualInventoryStatusMeta.tone}`}
title={virtualInventoryStatusMeta.title}
>
{virtualInventoryStatusMeta.label}
</span>
) : null}
{canUsePushControls ? (
<button
type="button"
onClick={() => void onConfirmPushRows([row])}
disabled={
isReadOnly ||
isNodeLocked ||
isSystemEntered ||
!pricingReadyForFuturePush ||
!PRODUCT_PRICING_JST_PUSH_ENABLED
}
title={
isSystemEntered
? "当前 SKU 已录入系统,后续以聚水潭回传状态为准"
: PRODUCT_PRICING_JST_PUSH_ENABLED &&
pricingReadyForFuturePush
? "确认推送当前 SKU 到聚水潭"
: pricingReadyForFuturePush
? "聚水潭接口未接入,暂不可确认推送"
: "请先填写成本价"
}
className="inline-flex h-7 min-w-[52px] items-center justify-center rounded-full bg-[#1f8bff] px-3 text-xs font-apple-medium text-white shadow-sm transition hover:bg-[#1478df] disabled:cursor-not-allowed disabled:bg-apple-bg-tertiary disabled:text-apple-label-tertiary disabled:shadow-none"
>
确认推送
</button>
) : null}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
</div>
);
}