feat: 优化正式入库人员字段与首批质检

This commit is contained in:
WuYongJie 2026-05-13 19:10:48 +08:00
parent 8d76d4ef9d
commit 0f7e647127
6 changed files with 1457 additions and 754 deletions

View File

@ -1859,7 +1859,9 @@ describe('IntakeClient', () => {
).not.toBeInTheDocument()
})
it('shows view-only actions for non-matching designers', () => {
it('hides unrelated products from non-matching designers in the created scope', async () => {
const actor = userEvent.setup()
window.localStorage.setItem(
NON_INTEGRATED_STORAGE_KEY,
JSON.stringify([createNonIntegratedProduct()]),
@ -1871,17 +1873,10 @@ describe('IntakeClient', () => {
userName: 'Designer B',
})
const productRow = screen.getByText('DNZ0001').closest('tr')
expect(productRow).not.toBeNull()
expect(
within(productRow as HTMLElement).getByRole('button', { name: '查看' }),
).toBeInTheDocument()
expect(
within(productRow as HTMLElement).queryByRole('button', { name: '复制' }),
).not.toBeInTheDocument()
expect(
within(productRow as HTMLElement).queryByRole('button', { name: '删除' }),
).not.toBeInTheDocument()
expect(screen.getByText('DNZ0001')).toBeInTheDocument()
await actor.click(screen.getByRole('button', { name: '我创建的' }))
expect(screen.queryByText('DNZ0001')).not.toBeInTheDocument()
})
it('lets matching structure designers open products for editing', () => {
@ -1941,6 +1936,39 @@ describe('IntakeClient', () => {
expect(actionCell?.querySelectorAll('button')).toHaveLength(2)
})
it('filters created products by the current DingTalk account name', async () => {
const actor = userEvent.setup()
window.localStorage.setItem(
NON_INTEGRATED_STORAGE_KEY,
JSON.stringify([
createNonIntegratedProduct({
id: 'caishen-created',
spu: 'CS0001',
createdBy: '财神',
designer: '',
}),
createNonIntegratedProduct({
id: 'xuxiake-created',
spu: 'XXK0001',
createdBy: '徐霞客',
designer: '',
}),
]),
)
renderSelector({
eligibleSkuList: [],
userRole: 'DESIGNER',
userName: '财神',
})
await actor.click(screen.getByRole('button', { name: '我创建的' }))
expect(screen.getByText('CS0001')).toBeInTheDocument()
expect(screen.queryByText('XXK0001')).not.toBeInTheDocument()
})
it('lets planning users open products for opening-reference editing only', async () => {
const actor = userEvent.setup()
@ -2029,7 +2057,7 @@ describe('IntakeClient', () => {
const productRow = screen.getByText('DNZ0001').closest('tr')
expect(productRow).not.toBeNull()
expect(within(productRow as HTMLElement).getByText('已废弃')).toBeInTheDocument()
expect(within(productRow as HTMLElement).getByText('开品参考')).toBeInTheDocument()
const editButton = (productRow as HTMLElement).querySelector(
'button[aria-label="修改"]',
@ -2078,7 +2106,7 @@ describe('IntakeClient', () => {
const abandonedRow = screen.getByText('DNZ0001').closest('tr')
expect(abandonedRow).not.toBeNull()
expect(within(abandonedRow as HTMLElement).getByText('已废弃')).toBeInTheDocument()
expect(within(abandonedRow as HTMLElement).getByText('开品参考')).toBeInTheDocument()
await actor.click(
within(abandonedRow as HTMLElement).getByRole('button', { name: '恢复' }),
@ -2087,7 +2115,7 @@ describe('IntakeClient', () => {
expect(confirmSpy).toHaveBeenCalledWith(expect.stringContaining('重新审批'))
const restoredRow = screen.getByText('DNZ0001').closest('tr')
expect(restoredRow).not.toBeNull()
expect(within(restoredRow as HTMLElement).getByText('流程中')).toBeInTheDocument()
expect(within(restoredRow as HTMLElement).getByText('开品参考')).toBeInTheDocument()
expect(
within(restoredRow as HTMLElement).queryByRole('button', { name: '恢复' }),
).not.toBeInTheDocument()
@ -2234,7 +2262,7 @@ describe('IntakeClient', () => {
await actor.click(screen.getByRole('button', { name: '修改' }))
expect(screen.getByTestId('embedded-workspace-state')).toHaveTextContent(
'detail-基础信息',
'document-开品参考',
)
await actor.click(screen.getByRole('button', { name: '模拟切到专利申请' }))
@ -2399,13 +2427,20 @@ describe('IntakeClient', () => {
expect(checkedRow).not.toBeNull()
expect(pendingRow).not.toBeNull()
expect(within(checkedRow as HTMLElement).getByText('流程中')).toBeInTheDocument()
expect(within(pendingRow as HTMLElement).getByText('流程中')).toBeInTheDocument()
expect(screen.getByRole('columnheader', { name: '产品实拍' })).toBeInTheDocument()
expect(screen.getByRole('columnheader', { name: '产品资料' })).toBeInTheDocument()
expect(within(checkedRow as HTMLElement).getByText('设计卖点')).toBeInTheDocument()
expect(within(pendingRow as HTMLElement).getByText('开品参考')).toBeInTheDocument()
expect(
screen.getByRole('columnheader', { name: '产品实拍 结构设计师' }),
).toBeInTheDocument()
expect(
screen.getByRole('columnheader', { name: '产品资料 外观/结构' }),
).toBeInTheDocument()
expect(
screen.queryByRole('columnheader', { name: '工厂质检' }),
).not.toBeInTheDocument()
expect(
screen.getByRole('columnheader', { name: '首批质检 质检组' }),
).toBeInTheDocument()
expect(
screen.queryByRole('columnheader', { name: '仓库抽检' }),
).not.toBeInTheDocument()
@ -2732,7 +2767,7 @@ describe('IntakeClient', () => {
).toBe(true)
})
it('shows operation and purchaser fields and filters non-integrated products by query button', async () => {
it('shows owner fields and filters non-integrated products by query button', async () => {
const actor = userEvent.setup()
window.localStorage.setItem(
@ -2742,8 +2777,17 @@ describe('IntakeClient', () => {
id: 'DNZ0002-1',
spu: 'GBGJ-DNZ0002',
designer: 'Designer B',
openingReference: {
...createEmptyNonIntegratedOpeningReference(),
designer: 'Designer B',
structureDesigner: 'Structure B',
planningOwner: 'Planning B',
},
operator: 'Operator B',
purchaser: 'Buyer B',
productPlanning: 'Planning B',
visualPlanning: 'Space B',
purchaseOwner: 'Buyer B',
previewImage: 'data:image/png;base64,test-image-2',
previewImageName: 'preview-2.png',
createdAt: '2026-04-14T10:00:00.000Z',
@ -2760,30 +2804,46 @@ describe('IntakeClient', () => {
renderSelector({ eligibleSkuList: [] })
expect(screen.getByRole('button', { name: '全部' })).toHaveAttribute(
'aria-pressed',
'true',
)
expect(screen.getByRole('button', { name: '我创建的' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: '待处理' })).toBeInTheDocument()
expect(screen.queryByRole('button', { name: '待处理的' })).not.toBeInTheDocument()
expect(screen.getByText('DNZ0001')).toBeInTheDocument()
expect(screen.getByText('Operator A')).toBeInTheDocument()
expect(screen.getByText('Buyer A')).toBeInTheDocument()
expect(screen.getByRole('columnheader', { name: '当前节点负责人' })).toBeInTheDocument()
expect(screen.getByRole('columnheader', { name: '创建时间' })).toBeInTheDocument()
expect(screen.getByRole('columnheader', { name: '外观设计师' })).toBeInTheDocument()
expect(screen.getByRole('columnheader', { name: '结构设计师' })).toBeInTheDocument()
expect(screen.getByRole('columnheader', { name: 'SPU负责人' })).toBeInTheDocument()
expect(screen.getByRole('columnheader', { name: '企划负责人' })).toBeInTheDocument()
expect(screen.getByRole('columnheader', { name: '空间设计师' })).toBeInTheDocument()
expect(screen.getByRole('columnheader', { name: '采购负责人' })).toBeInTheDocument()
expect(screen.getByRole('columnheader', { name: '当前节点' })).toBeInTheDocument()
expect(
screen.queryByRole('columnheader', { name: '当前节点负责人' }),
).not.toBeInTheDocument()
expect(screen.queryByRole('columnheader', { name: '创建时间' })).not.toBeInTheDocument()
const defaultProductRow = screen.getByText('DNZ0001').closest('tr')
expect(defaultProductRow).not.toBeNull()
expect(within(defaultProductRow as HTMLElement).getByText('开品参考')).toBeInTheDocument()
expect(
within(defaultProductRow as HTMLElement).getByText('2026-04-15 10:00:00'),
).toBeInTheDocument()
expect(screen.getByLabelText('SPU')).toBeInTheDocument()
expect(screen.getByLabelText('设计师')).toBeInTheDocument()
expect(screen.getByLabelText('运营')).toBeInTheDocument()
expect(screen.getByLabelText('采购')).toBeInTheDocument()
expect(screen.getByLabelText('外观设计师')).toBeInTheDocument()
expect(screen.getByLabelText('结构设计师')).toBeInTheDocument()
expect(screen.getByLabelText('SPU负责人')).toBeInTheDocument()
expect(screen.getByLabelText('企划负责人')).toBeInTheDocument()
expect(screen.getByLabelText('空间设计师')).toBeInTheDocument()
expect(screen.getByLabelText('采购负责人')).toBeInTheDocument()
expect(screen.getByRole('button', { name: '查询' })).toBeInTheDocument()
await actor.type(screen.getByLabelText('运营'), 'Operator B')
await actor.type(screen.getByLabelText('采购负责人'), 'Buyer B')
await actor.click(screen.getByRole('button', { name: '查询' }))
expect(screen.getByText('GBGJ-DNZ0002')).toBeInTheDocument()
expect(screen.queryByText('DNZ0001')).not.toBeInTheDocument()
await actor.clear(screen.getByLabelText('运营'))
await actor.clear(screen.getByLabelText('采购负责人'))
await actor.type(screen.getByLabelText('SPU'), 'DNZ0002')
await actor.click(screen.getByRole('button', { name: '查询' }))

View File

@ -20,6 +20,7 @@ import {
Package,
Palette,
Pencil,
Plus,
RotateCcw,
ShieldCheck,
Trash2,
@ -366,13 +367,13 @@ function DocumentStatusMark({
<span
aria-label={`${label}${statusText}`}
title={statusText}
className={`inline-flex h-6 w-6 items-center justify-center rounded-md border transition-colors ${
className={`inline-flex h-[17px] w-[17px] items-center justify-center rounded border transition-colors ${
ready
? "border-zinc-950 bg-zinc-950 text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.18)]"
: "border-zinc-200 bg-zinc-50 text-zinc-400"
}`}
>
<Icon className="h-3.5 w-3.5" strokeWidth={2.4} aria-hidden="true" />
<Icon className="h-2.5 w-2.5" strokeWidth={2.4} aria-hidden="true" />
</span>
);
}
@ -382,7 +383,6 @@ type NonIntegratedDocumentStatusCategory = NonIntegratedWorkflowNodeCategory;
const NON_INTEGRATED_DOCUMENT_STATUS_HIDDEN_CATEGORIES: ReadonlySet<NonIntegratedDocumentStatusCategory> =
new Set<NonIntegratedDocumentStatusCategory>([
"立项评审",
"工厂质检",
"检测报告",
]);
@ -398,6 +398,109 @@ const NON_INTEGRATED_FLOW_COMPLETE_CATEGORY =
NON_INTEGRATED_DOCUMENT_STATUS_COLUMNS[
NON_INTEGRATED_DOCUMENT_STATUS_COLUMNS.length - 1
];
const NON_INTEGRATED_NODE_OWNER_LABELS: Partial<
Record<NonIntegratedDocumentStatusCategory, string>
> = {
开品参考: "企划负责人",
设计卖点: "外观设计师",
产品模型: "外观/结构",
评审资料: "设计/采购",
基础信息: "结构设计师",
企划布局: "空间设计师",
专利申请: "外观设计师",
产品实拍图: "结构设计师",
安装方式: "结构设计师",
包装细节: "结构设计师",
产品定厂: "采购负责人",
样包测量: "采购/核价组",
产品核价: "采购/核价组",
产品限价: "企划负责人",
工厂质检: "质检组",
};
function getNonIntegratedListNodeLabel(
category: NonIntegratedDocumentStatusCategory,
) {
if (category === "企划布局") return "空间布局";
return getNonIntegratedDocumentWorkspaceCategoryLabel(category);
}
function getNonIntegratedListNodeOwnerLabel(
category: NonIntegratedDocumentStatusCategory,
) {
return NON_INTEGRATED_NODE_OWNER_LABELS[category] ?? "";
}
function safeTrim(value: unknown) {
return typeof value === "string" ? value.trim() : "";
}
function getNonIntegratedListPeople(product: NonIntegratedProductRecord) {
return {
appearanceDesigner:
safeTrim(product.openingReference?.designer) || safeTrim(product.designer),
structureDesigner:
safeTrim(product.openingReference?.structureDesigner),
spuOwner:
safeTrim(product.spuOwner) ||
safeTrim(product.operator) ||
safeTrim(product.operationsOwner),
planningOwner:
safeTrim(product.productPlanning) ||
safeTrim(product.openingReference?.planningOwner),
spaceDesigner:
safeTrim(product.visualPlanning) || safeTrim(product.visualDirector),
purchaseOwner: safeTrim(product.purchaseOwner) || safeTrim(product.purchaser),
};
}
function displayPerson(value: string) {
return value.trim() || "-";
}
function productMatchesText(value: string, keyword: string) {
const normalizedKeyword = keyword.trim().toLowerCase();
return (
normalizedKeyword.length === 0 ||
value.toLowerCase().includes(normalizedKeyword)
);
}
function getLegacyNonIntegratedProductOwnerNames(
product: NonIntegratedProductRecord,
) {
const people = getNonIntegratedListPeople(product);
return getUniqueNonEmptyValues([
people.appearanceDesigner,
people.structureDesigner,
people.spuOwner,
people.planningOwner,
people.spaceDesigner,
people.purchaseOwner,
]);
}
function isProductCreatedByCurrentUser(
product: NonIntegratedProductRecord,
user: IntakeProductPermissionUser,
) {
const userId = user.id.trim();
const userName = user.name.trim();
const creator = product.createdBy?.trim() ?? "";
if (
creator &&
((userId && creator === userId) || (userName && creator === userName))
) {
return true;
}
return Boolean(
!creator &&
userName &&
getLegacyNonIntegratedProductOwnerNames(product).includes(userName),
);
}
const PENDING_PAGE_SIZE = 25;
const TABLE_ACTION_BUTTON_BASE =
@ -406,6 +509,7 @@ const TABLE_ACTION_BUTTON_SECONDARY = `${TABLE_ACTION_BUTTON_BASE} !border-zinc-
const TABLE_ACTION_BUTTON_PRIMARY = `${TABLE_ACTION_BUTTON_BASE} !border-zinc-950 !bg-zinc-950 !text-white hover:!border-zinc-800 hover:!bg-zinc-800 focus:!border-zinc-600 disabled:!border-zinc-200 disabled:!bg-zinc-200 disabled:!text-zinc-400`;
const INTAKE_PRIMARY_BUTTON_CLASS =
"!border-zinc-950 !bg-zinc-950 !bg-none !text-white !shadow-none hover:!border-zinc-800 hover:!bg-zinc-800 focus:!border-zinc-600 disabled:!border-zinc-200 disabled:!bg-zinc-200 disabled:!text-zinc-400";
type NonIntegratedQuickScope = "all" | "created" | "pending";
const BOM_LIBRARY_OPTIONS: {
key: BomLibraryKey;
@ -1020,6 +1124,50 @@ function getPopulatedProductPricingRowCountForList(
).length;
}
function getPopulatedPlanningLayoutRowCountForList(
product: NonIntegratedProductRecord,
) {
return (product.planningLayoutRows ?? []).filter((row) =>
[
row.style,
row.styleTone,
row.audience,
row.model,
row.priceBand,
row.brand ?? "",
row.shop,
...row.brands,
...row.spaces,
...row.categories,
].some((value) => value.trim().length > 0),
).length;
}
function getPopulatedProductLimitRowCountForList(
product: NonIntegratedProductRecord,
) {
return (product.productLimitRows ?? []).filter((row) =>
[
row.expectedDailyLimitPrice,
row.expectedLimitGrossMarginRate,
row.nonSpuOwnerLimitPrice,
].some((value) => value.trim().length > 0),
).length;
}
function getPopulatedFirstQualityRowCountForList(
product: NonIntegratedProductRecord,
) {
return (product.firstQualityRows ?? []).filter((row) => {
const conclusion = row.conclusion.trim();
return Boolean(
row.qualityBatch.trim() ||
row.detail.trim() ||
(conclusion && conclusion !== "待检"),
);
}).length;
}
function hasPatentApplicationContentForList(product: NonIntegratedProductRecord) {
const application = normalizeNonIntegratedPatentApplication(
product.patentApplication,
@ -1087,6 +1235,9 @@ function getDocumentWorkspaceCountByCategory(
检测报告:
uploadFileCountByCategory["检测报告"] +
(qualityInspectionHasContent ? 1 : 0),
企划布局:
uploadFileCountByCategory["企划布局"] +
getPopulatedPlanningLayoutRowCountForList(product),
安装方式:
uploadFileCountByCategory["安装方式"] +
(hasInstallationContentForList(product) ? 1 : 0),
@ -1102,6 +1253,12 @@ function getDocumentWorkspaceCountByCategory(
产品核价:
uploadFileCountByCategory["产品核价"] +
getPopulatedProductPricingRowCountForList(product),
产品限价:
uploadFileCountByCategory["产品限价"] +
getPopulatedProductLimitRowCountForList(product),
工厂质检:
uploadFileCountByCategory["工厂质检"] +
getPopulatedFirstQualityRowCountForList(product),
[NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY]: hasBasicInfoContentForList(
product,
)
@ -1242,14 +1399,9 @@ function getCurrentNodeSummary(
number
>,
) {
const currentCategory = NON_INTEGRATED_DOCUMENT_STATUS_COLUMNS.find(
(category) => {
return !isDocumentWorkspaceReadyForList(
product,
category,
documentWorkspaceCountByCategory,
);
},
const currentCategory = getCurrentNodeCategoryForList(
product,
documentWorkspaceCountByCategory,
);
if (!currentCategory) {
@ -1265,11 +1417,74 @@ function getCurrentNodeSummary(
);
return {
nodeLabel: getNonIntegratedDocumentWorkspaceCategoryLabel(currentCategory),
nodeLabel: getNonIntegratedListNodeLabel(currentCategory),
ownerText: ownerCandidates.length > 0 ? ownerCandidates.join("、") : "-",
};
}
function getCurrentNodeCategoryForList(
product: NonIntegratedProductRecord,
documentWorkspaceCountByCategory: Record<
NonIntegratedDocumentStatusCategory,
number
>,
) {
return (
NON_INTEGRATED_DOCUMENT_STATUS_COLUMNS.find((category) => {
return !isDocumentWorkspaceReadyForList(
product,
category,
documentWorkspaceCountByCategory,
);
}) ?? null
);
}
function isCurrentUserPendingNonIntegratedProduct(
product: NonIntegratedProductRecord,
user: IntakeProductPermissionUser,
documentWorkspaceCountByCategory: Record<
NonIntegratedDocumentStatusCategory,
number
>,
) {
const currentCategory = getCurrentNodeCategoryForList(
product,
documentWorkspaceCountByCategory,
);
if (!currentCategory) return false;
const userName = user.name.trim();
const ownerCandidates = getCurrentNodeOwnerCandidates(product, currentCategory);
if (userName && ownerCandidates.includes(userName)) return true;
const people = getNonIntegratedListPeople(product);
if (
["开品参考", "产品限价"].includes(currentCategory) &&
isPlanningPermissionUser(user)
) {
return true;
}
if (
currentCategory === "企划布局" &&
(user.department?.name.includes("空间视觉") ||
user.department?.name.includes("品牌中心") ||
people.spaceDesigner === userName)
) {
return true;
}
if (
["产品定厂", "样包测量", "产品核价", "工厂质检", "检测报告"].includes(
currentCategory,
) &&
isPurchasingPermissionUser(user)
) {
return true;
}
return false;
}
function isDocumentWorkspaceApprovedForList(
product: NonIntegratedProductRecord,
category: NonIntegratedDocumentStatusCategory,
@ -1743,17 +1958,25 @@ function NonIntegratedPendingDetail({
} | null>(null);
const [copyErrorMessage, setCopyErrorMessage] = useState("");
const [isCopySubmitting, setIsCopySubmitting] = useState(false);
const [activeQuickScope, setActiveQuickScope] =
useState<NonIntegratedQuickScope>("all");
const [queryDraft, setQueryDraft] = useState({
spu: "",
designer: "",
operator: "",
purchaser: "",
appearanceDesigner: "",
structureDesigner: "",
spuOwner: "",
planningOwner: "",
spaceDesigner: "",
purchaseOwner: "",
});
const [appliedQuery, setAppliedQuery] = useState({
spu: "",
designer: "",
operator: "",
purchaser: "",
appearanceDesigner: "",
structureDesigner: "",
spuOwner: "",
planningOwner: "",
spaceDesigner: "",
purchaseOwner: "",
});
const shouldUseSharedNonIntegratedApi = process.env.NODE_ENV !== "test";
@ -1858,24 +2081,6 @@ function NonIntegratedPendingDetail({
});
}
function getProductStatus(
product: NonIntegratedProductRecord,
documentWorkspaceCountByCategory: Record<
NonIntegratedDocumentStatusCategory,
number
>,
) {
if (product.projectStatus === "rejected") {
return getFlowStatusDisplay("scrapped");
}
return getFlowStatusDisplay(
isNonIntegratedFlowCompleted(product, documentWorkspaceCountByCategory)
? "done"
: "running",
);
}
async function handleConfirmCopy() {
if (!copyDialogState) return;
@ -1930,103 +2135,137 @@ function NonIntegratedPendingDetail({
const normalizedSpuKeyword = appliedQuery.spu.trim().toUpperCase();
const productDisplayName =
getNonIntegratedProductDisplayName(product).toUpperCase();
const documentWorkspaceCountByCategory =
getDocumentWorkspaceCountByCategory(product);
const people = getNonIntegratedListPeople(product);
const spuMatched =
normalizedSpuKeyword.length === 0 ||
productDisplayName.includes(normalizedSpuKeyword) ||
getSearchableNonIntegratedSpuKeywords(product.spu).some((keyword) =>
keyword.includes(normalizedSpuKeyword),
);
const designerMatched =
appliedQuery.designer.trim().length === 0 ||
product.designer
.toLowerCase()
.includes(appliedQuery.designer.trim().toLowerCase());
const operatorMatched =
appliedQuery.operator.trim().length === 0 ||
product.operator
.toLowerCase()
.includes(appliedQuery.operator.trim().toLowerCase());
const purchaserMatched =
appliedQuery.purchaser.trim().length === 0 ||
product.purchaser
.toLowerCase()
.includes(appliedQuery.purchaser.trim().toLowerCase());
const peopleMatched =
productMatchesText(
people.appearanceDesigner,
appliedQuery.appearanceDesigner,
) &&
productMatchesText(
people.structureDesigner,
appliedQuery.structureDesigner,
) &&
productMatchesText(people.spuOwner, appliedQuery.spuOwner) &&
productMatchesText(people.planningOwner, appliedQuery.planningOwner) &&
productMatchesText(people.spaceDesigner, appliedQuery.spaceDesigner) &&
productMatchesText(people.purchaseOwner, appliedQuery.purchaseOwner);
const quickScopeMatched =
activeQuickScope === "all" ||
(activeQuickScope === "created"
? isProductCreatedByCurrentUser(product, currentUser)
: isCurrentUserPendingNonIntegratedProduct(
product,
currentUser,
documentWorkspaceCountByCategory,
));
return (
spuMatched && designerMatched && operatorMatched && purchaserMatched
);
return spuMatched && peopleMatched && quickScopeMatched;
});
}, [appliedQuery, products]);
}, [activeQuickScope, appliedQuery, currentUser, products]);
const filterFields: Array<{
key: keyof typeof queryDraft;
label: ReactNode;
ariaLabel: string;
placeholder: string;
}> = [
{ key: "spu", label: <SpuLabel />, ariaLabel: "SPU", placeholder: "搜索 SPU" },
{
key: "appearanceDesigner",
label: "外观设计师",
ariaLabel: "外观设计师",
placeholder: "搜索外观设计师",
},
{
key: "structureDesigner",
label: "结构设计师",
ariaLabel: "结构设计师",
placeholder: "搜索结构设计师",
},
{
key: "spuOwner",
label: "SPU负责人",
ariaLabel: "SPU负责人",
placeholder: "搜索SPU负责人",
},
{
key: "planningOwner",
label: "企划负责人",
ariaLabel: "企划负责人",
placeholder: "搜索企划负责人",
},
{
key: "spaceDesigner",
label: "空间设计师",
ariaLabel: "空间设计师",
placeholder: "搜索空间设计师",
},
{
key: "purchaseOwner",
label: "采购负责人",
ariaLabel: "采购负责人",
placeholder: "搜索采购负责人",
},
];
const quickScopes: Array<{ key: NonIntegratedQuickScope; label: string }> = [
{ key: "all", label: "全部" },
{ key: "created", label: "我创建的" },
{ key: "pending", label: "待处理" },
];
return (
<div className="flex h-full flex-col overflow-hidden rounded-apple-xl border border-apple-separator bg-apple-bg-primary shadow-apple-xs">
<div className="border-b border-apple-separator px-4 py-3">
<div className="flex flex-col gap-3">
<div className="grid gap-2.5 md:grid-cols-[1fr_1fr_1fr_1fr_88px_112px]">
<label className="space-y-1.5 text-xs text-apple-label-secondary">
<SpuLabel />
<input
aria-label="SPU"
value={queryDraft.spu}
onChange={(event) =>
setQueryDraft((current) => ({
...current,
spu: event.target.value,
}))
}
placeholder="搜索 SPU"
className="h-9 w-full rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 text-xs text-apple-label-primary outline-none"
/>
</label>
<label className="space-y-1.5 text-xs text-apple-label-secondary">
<span>设计师</span>
<input
aria-label="设计师"
value={queryDraft.designer}
onChange={(event) =>
setQueryDraft((current) => ({
...current,
designer: event.target.value,
}))
}
placeholder="搜索设计师"
className="h-9 w-full rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 text-xs text-apple-label-primary outline-none"
/>
</label>
<label className="space-y-1.5 text-xs text-apple-label-secondary">
<span>运营</span>
<input
aria-label="运营"
value={queryDraft.operator}
onChange={(event) =>
setQueryDraft((current) => ({
...current,
operator: event.target.value,
}))
}
placeholder="搜索运营"
className="h-9 w-full rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 text-xs text-apple-label-primary outline-none"
/>
</label>
<label className="space-y-1.5 text-xs text-apple-label-secondary">
<span>采购</span>
<input
aria-label="采购"
value={queryDraft.purchaser}
onChange={(event) =>
setQueryDraft((current) => ({
...current,
purchaser: event.target.value,
}))
}
placeholder="搜索采购"
className="h-9 w-full rounded-apple-lg border border-apple-separator bg-apple-bg-secondary px-3 text-xs text-apple-label-primary outline-none"
/>
</label>
<div className="flex flex-wrap items-center gap-1.5">
{quickScopes.map((scope) => (
<button
key={scope.key}
type="button"
aria-pressed={activeQuickScope === scope.key}
onClick={() => setActiveQuickScope(scope.key)}
className={`h-7 rounded-apple-md px-3 text-xs font-apple-medium ${
activeQuickScope === scope.key
? "bg-zinc-950 text-white"
: "border border-apple-separator bg-apple-bg-primary text-apple-label-secondary"
}`}
>
{scope.label}
</button>
))}
</div>
<div className="grid items-end gap-2 md:grid-cols-[repeat(7,minmax(0,1fr))_72px_104px]">
{filterFields.map((filter) => (
<label
key={filter.key}
className="space-y-1.5 text-xs text-apple-label-secondary"
>
<span>{filter.label}</span>
<input
aria-label={filter.ariaLabel}
value={queryDraft[filter.key]}
onChange={(event) =>
setQueryDraft((current) => ({
...current,
[filter.key]: event.target.value,
}))
}
placeholder={filter.placeholder}
className="h-8 w-full rounded-apple-md border border-apple-separator bg-apple-bg-secondary px-2.5 text-xs text-apple-label-primary outline-none"
/>
</label>
))}
<div className="flex items-end">
<Button
size="small"
className={`h-9 w-full text-xs ${INTAKE_PRIMARY_BUTTON_CLASS}`}
className={`h-8 w-full text-xs ${INTAKE_PRIMARY_BUTTON_CLASS}`}
onClick={() => setAppliedQuery(queryDraft)}
>
查询
@ -2035,9 +2274,10 @@ function NonIntegratedPendingDetail({
<div className="flex items-end justify-end">
<Button
size="small"
className={`h-9 w-full px-4 text-xs ${INTAKE_PRIMARY_BUTTON_CLASS}`}
className={`h-8 w-full px-3 text-xs ${INTAKE_PRIMARY_BUTTON_CLASS}`}
onClick={onOpenNewNonIntegratedProductWorkspace}
>
<Plus className="h-3.5 w-3.5" aria-hidden="true" />
新增产品
</Button>
</div>
@ -2048,7 +2288,7 @@ function NonIntegratedPendingDetail({
<div className="min-h-0 flex-1 overflow-auto p-4">
<div className="rounded-apple-lg border border-apple-separator bg-apple-bg-primary">
<div className="overflow-x-auto overflow-y-hidden pb-3">
<table className="w-full min-w-[1920px] text-left text-xs">
<table className="w-full min-w-[1880px] text-left text-xs">
<thead className="bg-apple-bg-primary text-apple-label-secondary">
<tr className="border-b border-apple-separator">
<th className="min-w-[56px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
@ -2061,32 +2301,40 @@ function NonIntegratedPendingDetail({
<SpuLabel />
</th>
<th className="min-w-[96px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
设计师
外观设计师
</th>
<th className="min-w-[96px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
运营
结构设计师
</th>
<th className="min-w-[96px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
采购
SPU负责人
</th>
<th className="min-w-[88px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
状态
<th className="min-w-[96px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
企划负责人
</th>
<th className="min-w-[148px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
当前节点负责人
<th className="min-w-[96px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
空间设计师
</th>
<th className="min-w-[96px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
采购负责人
</th>
<th className="min-w-[96px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
当前节点
</th>
{NON_INTEGRATED_DOCUMENT_STATUS_COLUMNS.map((category) => (
<th
key={category}
className="min-w-[88px] px-3 py-2.5 text-center font-apple-medium whitespace-nowrap"
>
{getNonIntegratedDocumentWorkspaceCategoryLabel(category)}
<div className="mx-auto grid w-[64px] justify-items-center gap-1">
<span>{getNonIntegratedListNodeLabel(category)}</span>
<span className="text-[10px] font-normal leading-none text-apple-label-tertiary">
{getNonIntegratedListNodeOwnerLabel(category)}
</span>
</div>
</th>
))}
<th className="min-w-[144px] px-3 py-2.5 font-apple-medium whitespace-nowrap">
创建时间
</th>
<th className="sticky right-0 z-20 min-w-[184px] border-l border-apple-separator bg-apple-bg-primary px-2.5 py-2.5 text-center font-apple-medium whitespace-nowrap shadow-[-10px_0_14px_-14px_rgba(15,23,42,0.28)]">
<th className="sticky right-0 z-20 min-w-[144px] border-l border-apple-separator bg-apple-bg-primary px-2.5 py-2.5 text-center font-apple-medium whitespace-nowrap shadow-[-10px_0_14px_-14px_rgba(15,23,42,0.28)]">
操作
</th>
</tr>
@ -2096,7 +2344,7 @@ function NonIntegratedPendingDetail({
<tr>
<td
colSpan={
10 + NON_INTEGRATED_DOCUMENT_STATUS_COLUMNS.length
11 + NON_INTEGRATED_DOCUMENT_STATUS_COLUMNS.length
}
className="px-4 py-10 text-center text-xs text-apple-label-secondary"
>
@ -2113,10 +2361,7 @@ function NonIntegratedPendingDetail({
getDocumentWorkspaceCountByCategory(product);
const isFlowAbandoned =
product.projectStatus === "rejected";
const productStatus = getProductStatus(
product,
documentWorkspaceCountByCategory,
);
const people = getNonIntegratedListPeople(product);
const canEditProduct = canEditNonIntegratedProduct(
product,
currentUser,
@ -2181,29 +2426,27 @@ function NonIntegratedPendingDetail({
</button>
</td>
<td className="px-3 py-3 text-apple-label-primary whitespace-nowrap">
{product.designer}
{displayPerson(people.appearanceDesigner)}
</td>
<td className="px-3 py-3 text-apple-label-primary whitespace-nowrap">
{product.operator || "-"}
{displayPerson(people.structureDesigner)}
</td>
<td className="px-3 py-3 text-apple-label-primary whitespace-nowrap">
{product.purchaser || "-"}
</td>
<td className="px-3 py-3 whitespace-nowrap">
<Badge
type={productStatus.badgeType}
variant="subtle"
>
{productStatus.label}
</Badge>
{displayPerson(people.spuOwner)}
</td>
<td className="px-3 py-3 text-apple-label-primary whitespace-nowrap">
<div className="max-w-[168px] truncate">
{currentNodeSummary.ownerText}
</div>
<div className="mt-1 max-w-[168px] truncate text-[11px] text-apple-label-secondary">
{displayPerson(people.planningOwner)}
</td>
<td className="px-3 py-3 text-apple-label-primary whitespace-nowrap">
{displayPerson(people.spaceDesigner)}
</td>
<td className="px-3 py-3 text-apple-label-primary whitespace-nowrap">
{displayPerson(people.purchaseOwner)}
</td>
<td className="px-3 py-3 text-apple-label-primary whitespace-nowrap">
<span className="rounded-apple-md bg-[#eef2f7] px-2 py-1 text-[11px] font-apple-medium text-apple-label-primary">
{currentNodeSummary.nodeLabel}
</div>
</span>
</td>
{NON_INTEGRATED_DOCUMENT_STATUS_COLUMNS.map(
(category) => {
@ -2226,9 +2469,6 @@ function NonIntegratedPendingDetail({
);
},
)}
<td className="px-3 py-3 text-apple-label-primary whitespace-nowrap">
{getFormattedListDateTime(product.createdAt)}
</td>
<td className="sticky right-0 z-10 border-l border-apple-separator bg-apple-bg-primary px-2 py-2 text-center whitespace-nowrap shadow-[-10px_0_14px_-14px_rgba(15,23,42,0.2)]">
<div className="flex flex-nowrap items-center justify-center gap-1.5">
<Button
@ -7342,13 +7582,12 @@ export default function IntakeClient(props: IntakeClientProps) {
[],
);
const handleActiveProductWorkspaceDetailStateChange = useCallback(
(detailState: WorkspaceProductDetailState) => {
if (!activeProductWorkspaceTabId) return;
updateProductWorkspaceDetailState(activeProductWorkspaceTabId, detailState);
},
[activeProductWorkspaceTabId, updateProductWorkspaceDetailState],
);
function handleActiveProductWorkspaceDetailStateChange(
detailState: WorkspaceProductDetailState,
) {
if (!activeProductWorkspaceTabId) return;
updateProductWorkspaceDetailState(activeProductWorkspaceTabId, detailState);
}
useEffect(() => {
setNonIntegratedProducts(props.initialNonIntegratedProducts);
@ -7527,6 +7766,17 @@ export default function IntakeClient(props: IntakeClientProps) {
"non-integrated-product",
`${product.id}:${effectiveMode}`,
);
const currentCategory = getCurrentNodeCategoryForList(
product,
getDocumentWorkspaceCountByCategory(product),
);
const currentDocumentCategory =
currentCategory &&
NON_INTEGRATED_DOCUMENT_WORKSPACE_CATEGORIES.includes(
currentCategory as NonIntegratedDocumentWorkspaceCategory,
)
? (currentCategory as NonIntegratedDocumentWorkspaceCategory)
: null;
const nextTab: IntakeWorkspaceTab = {
id: tabId,
kind: "nonIntegratedProduct",
@ -7534,6 +7784,9 @@ export default function IntakeClient(props: IntakeClientProps) {
productId: product.id,
product,
mode: effectiveMode,
detailState: currentDocumentCategory
? { view: "document", category: currentDocumentCategory }
: undefined,
};
openWorkspaceTab(nextTab);
}

View File

@ -457,7 +457,9 @@ describe("NonIntegratedDetailClient", () => {
expect(screen.getByLabelText("产品图片")).toBeInTheDocument();
expect(screen.getByLabelText("品牌")).toHaveValue("基本盘");
await actor.type(screen.getByLabelText("名称"), "测试开品");
await actor.type(screen.getByLabelText("外观设计师"), "外观A");
await actor.selectOptions(screen.getByLabelText("品牌"), "顾家");
await actor.type(screen.getByLabelText("企划负责人"), "企划A");
const imageFile = new File(["image"], "preview.png", { type: "image/png" });
const fileInputs = container.querySelectorAll('input[type="file"]');
@ -473,13 +475,17 @@ describe("NonIntegratedDetailClient", () => {
expect(savedProducts).toHaveLength(1);
expect(savedProducts[0]?.spu).toBe("PD20260505175418001");
expect(savedProducts[0]?.projectStatus).toBe("draft");
expect(savedProducts[0]?.createdBy).toBe("creator-user-id");
expect(savedProducts[0]?.updatedBy).toBe("creator-user-id");
expect(savedProducts[0]?.createdBy).toBe("Creator");
expect(savedProducts[0]?.updatedBy).toBe("Creator");
expect(savedProducts[0]?.brand).toBe("顾家");
expect(savedProducts[0]?.adaptableBrands).toEqual(["顾家"]);
expect(savedProducts[0]?.openingReference.productName).toBe("测试开品");
expect(savedProducts[0]?.openingReference.brand).toBe("顾家");
expect(savedProducts[0]?.openingReference.adaptableBrands).toEqual(["顾家"]);
expect(savedProducts[0]?.openingReference.designer).toBe("外观A");
expect(savedProducts[0]?.openingReference.planningOwner).toBe("企划A");
expect(savedProducts[0]?.designer).toBe("外观A");
expect(savedProducts[0]?.productPlanning).toBe("企划A");
expect(savedProducts[0]?.openingReference.temporaryCode).toBe(
"PD20260505175418001",
);
@ -506,6 +512,8 @@ describe("NonIntegratedDetailClient", () => {
const { container } = render(<NonIntegratedDetailClient mode="create" />);
await actor.type(screen.getByLabelText("名称"), "远程开品");
await actor.type(screen.getByLabelText("外观设计师"), "外观A");
await actor.type(screen.getByLabelText("企划负责人"), "企划A");
const imageFile = new File(["image"], "remote-preview.png", {
type: "image/png",
@ -903,6 +911,8 @@ describe("NonIntegratedDetailClient", () => {
const { container } = render(<NonIntegratedDetailClient mode="create" />);
await actor.type(screen.getByLabelText("名称"), "仅填写名称的开品");
await actor.type(screen.getByLabelText("外观设计师"), "外观A");
await actor.type(screen.getByLabelText("企划负责人"), "企划A");
const imageFile = new File(["image"], "preview.png", { type: "image/png" });
const fileInputs = container.querySelectorAll('input[type="file"]');
@ -1195,6 +1205,7 @@ describe("NonIntegratedDetailClient", () => {
"document-workspace-trigger-样包测量",
"document-workspace-trigger-产品核价",
"document-workspace-trigger-产品限价",
"document-workspace-trigger-工厂质检",
]);
expect(
screen.queryByTestId("document-workspace-trigger-立项评审"),
@ -1206,8 +1217,8 @@ describe("NonIntegratedDetailClient", () => {
expect(reviewTrigger).toHaveAttribute("data-has-files", "true");
expect(modelTrigger).toHaveAttribute("data-has-files", "true");
expect(
screen.queryByTestId("document-workspace-trigger-工厂质检"),
).not.toBeInTheDocument();
screen.getByTestId("document-workspace-trigger-工厂质检"),
).toHaveTextContent("首批质检");
expect(
screen.queryByTestId("document-workspace-trigger-检测报告"),
).not.toBeInTheDocument();
@ -1275,6 +1286,7 @@ describe("NonIntegratedDetailClient", () => {
"document-workspace-trigger-样包测量",
"document-workspace-trigger-产品核价",
"document-workspace-trigger-产品限价",
"document-workspace-trigger-工厂质检",
]);
expect(
sidebar.querySelector<HTMLButtonElement>(
@ -1437,6 +1449,9 @@ describe("NonIntegratedDetailClient", () => {
await actor.clear(screen.getByLabelText("结构设计师"));
await actor.type(screen.getByLabelText("结构设计师"), "结构B");
expect(
await screen.findByText("已自动保存", undefined, { timeout: 3000 }),
).toBeInTheDocument();
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(
"SZ0230-opening-reference",
@ -2155,6 +2170,7 @@ describe("NonIntegratedDetailClient", () => {
operator: "徐霞客",
profitCenter: "测试利润中心",
goodsType: "测试货品类型",
materialType: "橡胶木",
priceBand: "测试价格段",
outputRequirementType: "测试出图需求",
visualPlanning: "测试视觉策划",
@ -2173,6 +2189,9 @@ describe("NonIntegratedDetailClient", () => {
);
expect(screen.getByLabelText("SPU负责人")).toHaveValue("徐霞客");
expect(screen.getByLabelText("空间设计师")).toHaveValue("测试视觉策划");
expect(screen.getByLabelText("材质类型")).toHaveValue("橡胶木");
expect(screen.getByRole("option", { name: "岩板+钢架" })).toBeInTheDocument();
[
"商品ID",
@ -2195,6 +2214,113 @@ describe("NonIntegratedDetailClient", () => {
});
});
it("persists space designer and material type from basic info", async () => {
const actor = userEvent.setup();
saveNonIntegratedProduct(
createProductRecord({
id: "SZ0230-basic-material-save",
spu: "SZ0230",
openingReference: {
...createEmptyNonIntegratedOpeningReference(),
material: "18ENF级橡胶木",
},
}),
);
const { unmount } = render(
<NonIntegratedDetailClient
mode="edit"
productId="SZ0230-basic-material-save"
currentUserRole="SUPER_ADMIN"
/>,
);
expect(screen.getByLabelText("材质类型")).toHaveValue("");
await actor.clear(screen.getByLabelText("空间设计师"));
await actor.type(screen.getByLabelText("空间设计师"), "空间A");
await actor.selectOptions(screen.getByLabelText("材质类型"), "岩板+钢架");
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(
"SZ0230-basic-material-save",
);
expect(savedProduct?.visualPlanning).toBe("空间A");
expect(savedProduct?.materialType).toBe("岩板+钢架");
expect(savedProduct?.openingReference.material).toBe("18ENF级橡胶木");
});
unmount();
render(
<NonIntegratedDetailClient
mode="edit"
productId="SZ0230-basic-material-save"
currentUserRole="SUPER_ADMIN"
/>,
);
expect(screen.getByLabelText("空间设计师")).toHaveValue("空间A");
expect(screen.getByLabelText("材质类型")).toHaveValue("岩板+钢架");
await actor.clear(screen.getByLabelText("空间设计师"));
await actor.selectOptions(screen.getByLabelText("材质类型"), "");
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(
"SZ0230-basic-material-save",
);
expect(savedProduct?.visualPlanning).toBe("");
expect(savedProduct?.materialType).toBe("");
});
});
it("keeps basic info confirmed while auto-saving non-SKU basic fields", async () => {
const actor = userEvent.setup();
const approvedAt = "2026-05-13T08:00:00.000Z";
saveNonIntegratedProduct(
createProductRecord({
id: "SZ0230-basic-material-reconfirm",
spu: "SZ0230",
materialType: "橡胶木",
visualPlanning: "空间旧",
documentWorkspaceConfirmations: {
...createDefaultDocumentWorkspaceConfirmations(),
[NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY]: true,
},
documentWorkspaceApprovedAt: {
[NON_INTEGRATED_BASIC_INFO_WORKSPACE_CATEGORY]: approvedAt,
},
}),
);
render(
<NonIntegratedDetailClient
mode="edit"
productId="SZ0230-basic-material-reconfirm"
currentUserRole="SUPER_ADMIN"
/>,
);
expect(screen.getByTestId("basic-info-confirmed")).toBeInTheDocument();
await actor.clear(screen.getByLabelText("空间设计师"));
await actor.type(screen.getByLabelText("空间设计师"), "空间新");
await actor.selectOptions(screen.getByLabelText("材质类型"), "亚克力");
expect(screen.getByTestId("basic-info-confirmed")).toBeInTheDocument();
expect(screen.queryByTestId("basic-info-confirm")).not.toBeInTheDocument();
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(
"SZ0230-basic-material-reconfirm",
);
expect(savedProduct?.visualPlanning).toBe("空间新");
expect(savedProduct?.materialType).toBe("亚克力");
});
});
it("renders sample measurement rows from SKU data and persists purchaser fields", async () => {
const actor = userEvent.setup();
@ -2487,7 +2613,7 @@ describe("NonIntegratedDetailClient", () => {
).toHaveAttribute("readonly");
});
it("renders factory quality archive by style and asset type", async () => {
it("renders first quality rows by SKU batch", async () => {
const actor = userEvent.setup();
window.localStorage.setItem(
@ -2501,6 +2627,14 @@ describe("NonIntegratedDetailClient", () => {
model: "SZ0230K01",
}),
],
firstQualityRows: [
{
skuId: "sku-factory-quality-1",
qualityBatch: "FQ-001",
conclusion: "合格",
detail: "首批通过",
},
],
uploadFiles: [
{
id: "factory-quality-image-1",
@ -2532,42 +2666,33 @@ describe("NonIntegratedDetailClient", () => {
/>,
);
await actor.click(screen.getByTestId("document-workspace-trigger-包装细节"));
await actor.click(screen.getByTestId("packaging-workspace-panel-factoryQuality"));
await actor.click(screen.getByTestId("document-workspace-trigger-工厂质检"));
const panel = screen.getByTestId("document-workspace-panel");
expect(screen.getByTestId("factory-quality-workspace")).toBeInTheDocument();
expect(screen.getByTestId("factory-quality-style-K01")).toHaveTextContent(
"SZ0230K01",
expect(screen.getByTestId("first-quality-workspace-panel-firstQuality")).toHaveTextContent(
"首批质检",
);
expect(screen.getByTestId("first-quality-workspace-panel-qualityStandard")).toHaveTextContent(
"质检标准",
);
expect(
screen.getByTestId("factory-quality-asset-type-drop-image"),
).toHaveTextContent("摔包图片");
screen.queryByTestId("packaging-workspace-panel-factoryQuality"),
).not.toBeInTheDocument();
expect(panel).toHaveTextContent("批次1");
expect(panel).toHaveTextContent("SZ0230K01");
expect(panel).toHaveTextContent("质检批次");
expect(panel).toHaveTextContent("质检结论");
expect(panel).toHaveTextContent("质检详细说明");
expect(panel).toHaveTextContent("质检附件");
expect(screen.getByDisplayValue("FQ-001")).toBeInTheDocument();
expect(screen.getByLabelText("SZ0230K01质检结论")).toHaveValue("合格");
expect(screen.getByDisplayValue("首批通过")).toBeInTheDocument();
expect(panel).toHaveTextContent("当前 2 个文件");
expect(screen.queryByTestId("factory-quality-style-K01")).not.toBeInTheDocument();
expect(
screen.getByTestId("factory-quality-asset-type-drop-video"),
).toHaveTextContent("摔包视频");
expect(
screen.getByTestId("factory-quality-workspace-upload-input"),
).toBeInTheDocument();
expect(screen.getByTestId("archive-preview-panel")).toHaveClass("flex-1");
expect(screen.getByTestId("archive-preview-panel")).not.toHaveClass(
"self-start",
);
expect(screen.getByTestId("archive-preview-frame")).toHaveClass("flex-1");
expect(
screen.getByTestId("factory-quality-file-factory-quality-image-1"),
).toHaveTextContent("K01-摔包图片.png");
await actor.click(
screen.getByTestId("factory-quality-asset-type-drop-video"),
);
expect(
screen.getByTestId("factory-quality-file-factory-quality-video-1"),
).toHaveTextContent("K01-摔包视频.mp4");
expect(panel).toHaveTextContent("摔包图片");
expect(panel).toHaveTextContent("摔包视频");
expect(panel).toHaveTextContent("当前文件类型:视频");
screen.queryByTestId("factory-quality-workspace-upload-input"),
).not.toBeInTheDocument();
});
it("renders installation style tabs from SKU models", async () => {

View File

@ -86,6 +86,7 @@ export function getNonIntegratedDocumentWorkspaceCategoryLabel(
if (category === "产品实拍图") return "产品实拍";
if (category === "产品模型") return "产品资料";
if (category === "评审资料") return "样品评审";
if (category === "工厂质检") return "首批质检";
if (category === "检测报告") return "仓库抽检";
return category;
}
@ -270,6 +271,7 @@ const NON_INTEGRATED_UPLOAD_CATEGORY_MAP: Record<string, string> = {
产品核价: "产品核价",
产品限价: "产品限价",
工厂质检: "工厂质检",
首批质检: "工厂质检",
仓库质检: "工厂质检",
仓库抽检: "检测报告",
产品资料: "产品模型",
@ -370,6 +372,13 @@ export type NonIntegratedSampleMeasurementRow = {
productShape: string;
};
export type NonIntegratedFirstQualityRow = {
skuId: string;
qualityBatch: string;
conclusion: string;
detail: string;
};
export type NonIntegratedDetectionAssetType = "attachment" | "quality-standard";
export type NonIntegratedUploadFile = {
@ -570,6 +579,7 @@ export type NonIntegratedProductRecord = {
planningLayoutRows: NonIntegratedPlanningLayoutRow[];
productLimitRows: NonIntegratedProductLimitRow[];
sampleMeasurementRows: NonIntegratedSampleMeasurementRow[];
firstQualityRows?: NonIntegratedFirstQualityRow[];
uploadFiles: NonIntegratedUploadFile[];
documentWorkspaceNotes?: Record<string, string>;
documentWorkspaceConfirmations: NonIntegratedDocumentWorkspaceConfirmationMap;
@ -1246,6 +1256,19 @@ function normalizeSampleMeasurementRow(
};
}
function normalizeFirstQualityRow(
item: Partial<NonIntegratedFirstQualityRow>,
): NonIntegratedFirstQualityRow | null {
if (typeof item.skuId !== "string") return null;
return {
skuId: item.skuId,
qualityBatch: normalizeString(item.qualityBatch),
conclusion: normalizeString(item.conclusion) || "待检",
detail: normalizeString(item.detail),
};
}
function normalizeUploadFile(
item: Partial<NonIntegratedUploadFile>,
): NonIntegratedUploadFile | null {
@ -1850,6 +1873,13 @@ function toProductRecord(
entry !== null,
)
: [],
firstQualityRows: Array.isArray(item.firstQualityRows)
? item.firstQualityRows
.map((entry) => normalizeFirstQualityRow(entry))
.filter(
(entry): entry is NonIntegratedFirstQualityRow => entry !== null,
)
: [],
uploadFiles: Array.isArray(item.uploadFiles)
? item.uploadFiles
.map((entry) => normalizeUploadFile(entry))

View File

@ -165,7 +165,7 @@ function ToolbarButton({
className={cn(
"flex h-10 w-10 flex-col items-center justify-center rounded-[6px] transition-all duration-200",
active
? "bg-[#f1f4f5] text-[#101719] shadow-none ring-1 ring-[#101719]/15"
? "bg-[var(--obs-shell-action-bg)] text-[var(--obs-shell-action-text)] shadow-none ring-1 ring-[var(--obs-shell-action-border)]"
: "text-[#5a6062] hover:bg-[#f1f4f5] hover:text-[#101719] disabled:opacity-30 disabled:hover:scale-100"
)}
>