fix(intake): 调整空间布局字段选项
This commit is contained in:
parent
606dc9612c
commit
a1209cc7f8
@ -1942,6 +1942,9 @@ function getPopulatedPlanningLayoutRowCountForList(
|
||||
row.priceBand,
|
||||
row.brand ?? "",
|
||||
row.shop,
|
||||
...(row.styles ?? []),
|
||||
...(row.styleTones ?? []),
|
||||
...(row.audiences ?? []),
|
||||
...row.brands,
|
||||
...row.spaces,
|
||||
...row.categories,
|
||||
|
||||
@ -3842,7 +3842,7 @@ describe("NonIntegratedDetailClient", () => {
|
||||
screen.getByTestId(`document-workspace-trigger-${planningLayoutCategory}`),
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId("planning-layout-style-sku-planning-permission-1"),
|
||||
screen.getByTestId("planning-layout-styles-draft-sku-planning-permission-1"),
|
||||
).not.toHaveAttribute("readonly");
|
||||
expect(
|
||||
screen.getByTestId(`document-workspace-confirm-${planningLayoutCategory}`),
|
||||
@ -4503,20 +4503,45 @@ describe("NonIntegratedDetailClient", () => {
|
||||
).toHaveValue("500-600");
|
||||
|
||||
fireEvent.change(
|
||||
screen.getByTestId("planning-layout-style-sku-planning-layout-fields-1"),
|
||||
screen.getByTestId("planning-layout-styles-draft-sku-planning-layout-fields-1"),
|
||||
{ target: { value: "治愈风" } },
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByTestId(
|
||||
"planning-layout-styleTone-sku-planning-layout-fields-1",
|
||||
),
|
||||
).toHaveValue("原木色自然&治愈风"),
|
||||
);
|
||||
expect(within(planningWorkspace).getByText("治愈风")).toBeInTheDocument();
|
||||
fireEvent.change(
|
||||
screen.getByTestId("planning-layout-audience-sku-planning-layout-fields-1"),
|
||||
screen.getByTestId("planning-layout-styles-draft-sku-planning-layout-fields-1"),
|
||||
{ target: { value: "日系风" } },
|
||||
);
|
||||
expect(within(planningWorkspace).getByText("日系风")).toBeInTheDocument();
|
||||
fireEvent.change(
|
||||
screen.getByTestId(
|
||||
"planning-layout-styleTones-draft-sku-planning-layout-fields-1",
|
||||
),
|
||||
{ target: { value: "原木色自然&治愈风" } },
|
||||
);
|
||||
expect(
|
||||
within(planningWorkspace).getByText("原木色自然&治愈风"),
|
||||
).toBeInTheDocument();
|
||||
fireEvent.change(
|
||||
screen.getByTestId(
|
||||
"planning-layout-styleTones-draft-sku-planning-layout-fields-1",
|
||||
),
|
||||
{ target: { value: "侘寂风" } },
|
||||
);
|
||||
expect(within(planningWorkspace).getByText("侘寂风")).toBeInTheDocument();
|
||||
fireEvent.change(
|
||||
screen.getByTestId(
|
||||
"planning-layout-audiences-draft-sku-planning-layout-fields-1",
|
||||
),
|
||||
{ target: { value: "有孩家庭" } },
|
||||
);
|
||||
expect(within(planningWorkspace).getByText("有孩家庭")).toBeInTheDocument();
|
||||
fireEvent.change(
|
||||
screen.getByTestId(
|
||||
"planning-layout-audiences-draft-sku-planning-layout-fields-1",
|
||||
),
|
||||
{ target: { value: "独居青年" } },
|
||||
);
|
||||
expect(within(planningWorkspace).getByText("独居青年")).toBeInTheDocument();
|
||||
fireEvent.change(
|
||||
screen.getByTestId("planning-layout-model-sku-planning-layout-fields-1"),
|
||||
{ target: { value: "窄边书桌" } },
|
||||
@ -4594,8 +4619,11 @@ describe("NonIntegratedDetailClient", () => {
|
||||
{
|
||||
skuId: "sku-planning-layout-fields-1",
|
||||
style: "治愈风",
|
||||
styles: ["治愈风", "日系风"],
|
||||
styleTone: "原木色自然&治愈风",
|
||||
styleTones: ["原木色自然&治愈风", "侘寂风"],
|
||||
audience: "有孩家庭",
|
||||
audiences: ["有孩家庭", "独居青年"],
|
||||
spaces: ["客厅", "书房"],
|
||||
categories: ["书柜/架", "书桌/电脑桌"],
|
||||
model: "窄边书桌",
|
||||
|
||||
@ -2375,20 +2375,8 @@ const PLANNING_LAYOUT_SPACE_CATEGORY_OPTIONS: Record<string, string[]> = {
|
||||
卫生间: ["洗脸池柜", "镜柜", "镜子", "置物架", "毛巾架", "矮凳"],
|
||||
厨房: ["桌面置物架", "落地置物架", "储物柜", "矮凳", "推车", "桌面收纳架"],
|
||||
};
|
||||
const PLANNING_LAYOUT_MODEL_OPTIONS = [
|
||||
"窄边书桌",
|
||||
"开放式置物架",
|
||||
"折叠翻板桌",
|
||||
"移动推车",
|
||||
"边柜组合",
|
||||
];
|
||||
const PLANNING_LAYOUT_SHOP_OPTIONS = [
|
||||
"天猫旗舰店",
|
||||
"抖音旗舰店",
|
||||
"京东自营",
|
||||
"拼多多店铺",
|
||||
"线下门店",
|
||||
];
|
||||
const PLANNING_LAYOUT_MODEL_OPTIONS: string[] = [];
|
||||
const PLANNING_LAYOUT_SHOP_OPTIONS: string[] = [];
|
||||
const SINGLE_VALUE_BRAND_FIELD_KEY = "brand";
|
||||
|
||||
function isSingleValueBrandField(key: PropertyKey) {
|
||||
@ -5217,8 +5205,11 @@ type PlanningLayoutSkuRow = {
|
||||
supplierName: string;
|
||||
batchNo: number;
|
||||
style: string;
|
||||
styles: string[];
|
||||
styleTone: string;
|
||||
styleTones: string[];
|
||||
audience: string;
|
||||
audiences: string[];
|
||||
spaces: string[];
|
||||
categories: string[];
|
||||
planningModel: string;
|
||||
@ -5227,14 +5218,14 @@ type PlanningLayoutSkuRow = {
|
||||
shop: string;
|
||||
};
|
||||
|
||||
type PlanningLayoutTextFieldKey =
|
||||
| "style"
|
||||
| "styleTone"
|
||||
| "audience"
|
||||
| "model"
|
||||
| "priceBand"
|
||||
| "shop";
|
||||
type PlanningLayoutListFieldKey = "spaces" | "categories" | "brands";
|
||||
type PlanningLayoutTextFieldKey = "model" | "priceBand" | "shop";
|
||||
type PlanningLayoutListFieldKey =
|
||||
| "styles"
|
||||
| "styleTones"
|
||||
| "audiences"
|
||||
| "spaces"
|
||||
| "categories"
|
||||
| "brands";
|
||||
|
||||
type ProductLimitDisplayRow = NonIntegratedProductLimitRow & {
|
||||
image: string;
|
||||
@ -5348,8 +5339,11 @@ function createEmptyPlanningLayoutRow(
|
||||
return {
|
||||
skuId,
|
||||
style: "",
|
||||
styles: [],
|
||||
styleTone: "",
|
||||
styleTones: [],
|
||||
audience: "",
|
||||
audiences: [],
|
||||
spaces: [],
|
||||
categories: [],
|
||||
model: "",
|
||||
@ -5360,6 +5354,18 @@ function createEmptyPlanningLayoutRow(
|
||||
};
|
||||
}
|
||||
|
||||
function getPlanningLayoutStyleToneOptions(
|
||||
styles: ReadonlyArray<string>,
|
||||
styleTones: ReadonlyArray<string> = [],
|
||||
) {
|
||||
return getUniqueNonEmptyStrings([
|
||||
...styles.flatMap(
|
||||
(style) => PLANNING_LAYOUT_STYLE_TONE_OPTIONS[style] ?? [],
|
||||
),
|
||||
...styleTones,
|
||||
]);
|
||||
}
|
||||
|
||||
function normalizeProductLimitRowsForPayload(
|
||||
rows: NonIntegratedProductLimitRow[],
|
||||
): NonIntegratedProductLimitRow[] {
|
||||
@ -5396,12 +5402,27 @@ function normalizePlanningLayoutRowsForPayload(
|
||||
...row.brands,
|
||||
row.brand,
|
||||
]);
|
||||
const styles = getUniqueNonEmptyStrings([
|
||||
...(row.styles ?? []),
|
||||
row.style,
|
||||
]);
|
||||
const styleTones = getUniqueNonEmptyStrings([
|
||||
...(row.styleTones ?? []),
|
||||
row.styleTone,
|
||||
]);
|
||||
const audiences = getUniqueNonEmptyStrings([
|
||||
...(row.audiences ?? []),
|
||||
row.audience,
|
||||
]);
|
||||
|
||||
return {
|
||||
skuId: sku.id,
|
||||
style: row.style.trim(),
|
||||
styleTone: row.styleTone.trim(),
|
||||
audience: row.audience.trim(),
|
||||
style: styles[0] ?? "",
|
||||
styles,
|
||||
styleTone: styleTones[0] ?? "",
|
||||
styleTones,
|
||||
audience: audiences[0] ?? "",
|
||||
audiences,
|
||||
spaces: getUniqueNonEmptyStrings(row.spaces),
|
||||
categories: getUniqueNonEmptyStrings(row.categories),
|
||||
model: row.model.trim(),
|
||||
@ -5423,6 +5444,9 @@ function normalizePlanningLayoutRowsForPayload(
|
||||
row.priceBand,
|
||||
row.brand ?? "",
|
||||
row.shop,
|
||||
...row.styles,
|
||||
...row.styleTones,
|
||||
...row.audiences,
|
||||
...row.spaces,
|
||||
...row.categories,
|
||||
...row.brands,
|
||||
@ -5476,8 +5500,20 @@ function buildPlanningLayoutSkuRows(
|
||||
supplierName: item.manufacturer.trim(),
|
||||
batchNo: item.batchNo || 1,
|
||||
style: row?.style.trim() ?? "",
|
||||
styles: getUniqueNonEmptyStrings([
|
||||
...(row?.styles ?? []),
|
||||
row?.style,
|
||||
]),
|
||||
styleTone: row?.styleTone.trim() ?? "",
|
||||
styleTones: getUniqueNonEmptyStrings([
|
||||
...(row?.styleTones ?? []),
|
||||
row?.styleTone,
|
||||
]),
|
||||
audience: row?.audience.trim() ?? "",
|
||||
audiences: getUniqueNonEmptyStrings([
|
||||
...(row?.audiences ?? []),
|
||||
row?.audience,
|
||||
]),
|
||||
spaces: row?.spaces ?? [],
|
||||
categories: row?.categories ?? [],
|
||||
planningModel: row?.model.trim() ?? "",
|
||||
@ -14896,6 +14932,48 @@ export default function NonIntegratedDetailClient({
|
||||
}));
|
||||
}
|
||||
|
||||
function applyPlanningLayoutListValues(
|
||||
current: NonIntegratedPlanningLayoutRow,
|
||||
field: PlanningLayoutListFieldKey,
|
||||
values: string[],
|
||||
): NonIntegratedPlanningLayoutRow {
|
||||
const nextValues = getUniqueNonEmptyStrings(values);
|
||||
|
||||
if (field === "styles") {
|
||||
return {
|
||||
...current,
|
||||
style: nextValues[0] ?? "",
|
||||
styles: nextValues,
|
||||
};
|
||||
}
|
||||
if (field === "styleTones") {
|
||||
return {
|
||||
...current,
|
||||
styleTone: nextValues[0] ?? "",
|
||||
styleTones: nextValues,
|
||||
};
|
||||
}
|
||||
if (field === "audiences") {
|
||||
return {
|
||||
...current,
|
||||
audience: nextValues[0] ?? "",
|
||||
audiences: nextValues,
|
||||
};
|
||||
}
|
||||
if (field === "brands") {
|
||||
return {
|
||||
...current,
|
||||
brand: nextValues[0] ?? "",
|
||||
brands: nextValues,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...current,
|
||||
[field]: nextValues,
|
||||
};
|
||||
}
|
||||
|
||||
function addPlanningLayoutListValue(
|
||||
skuId: string,
|
||||
field: PlanningLayoutListFieldKey,
|
||||
@ -14911,10 +14989,16 @@ export default function NonIntegratedDetailClient({
|
||||
.filter(Boolean);
|
||||
if (nextValues.length === 0) return;
|
||||
|
||||
updatePlanningLayoutRow(skuId, (current) => ({
|
||||
...current,
|
||||
[field]: getUniqueNonEmptyStrings([...current[field], ...nextValues]),
|
||||
}));
|
||||
updatePlanningLayoutRow(skuId, (current) => {
|
||||
const currentValues = Array.isArray(current[field])
|
||||
? current[field]
|
||||
: [];
|
||||
|
||||
return applyPlanningLayoutListValues(current, field, [
|
||||
...currentValues,
|
||||
...nextValues,
|
||||
]);
|
||||
});
|
||||
if (explicitValue === undefined) {
|
||||
setPlanningLayoutDraftInputs((current) => ({
|
||||
...current,
|
||||
@ -14930,10 +15014,17 @@ export default function NonIntegratedDetailClient({
|
||||
) {
|
||||
if (isReadOnly) return;
|
||||
|
||||
updatePlanningLayoutRow(skuId, (current) => ({
|
||||
...current,
|
||||
[field]: current[field].filter((item) => item !== value),
|
||||
}));
|
||||
updatePlanningLayoutRow(skuId, (current) => {
|
||||
const currentValues = Array.isArray(current[field])
|
||||
? current[field]
|
||||
: [];
|
||||
|
||||
return applyPlanningLayoutListValues(
|
||||
current,
|
||||
field,
|
||||
currentValues.filter((item) => item !== value),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function togglePlanningLayoutBrand(skuId: string, brand: string) {
|
||||
@ -21155,43 +21246,43 @@ export default function NonIntegratedDetailClient({
|
||||
批次{row.batchNo}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
{renderSearchCell({
|
||||
{renderMultiSelectCell({
|
||||
row,
|
||||
field: "style",
|
||||
field: "styles",
|
||||
label: "风格",
|
||||
value: row.style,
|
||||
options: PLANNING_LAYOUT_STYLE_OPTIONS,
|
||||
onChange: (value) =>
|
||||
updatePlanningLayoutRow(row.skuId, (current) => ({
|
||||
...current,
|
||||
style: value,
|
||||
styleTone:
|
||||
PLANNING_LAYOUT_STYLE_TONE_OPTIONS[value]?.[0] ??
|
||||
current.styleTone,
|
||||
})),
|
||||
})}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
{renderSearchCell({
|
||||
row,
|
||||
field: "styleTone",
|
||||
label: "风格细分/色系",
|
||||
value: row.styleTone,
|
||||
values: row.styles ?? [],
|
||||
options: getUniqueNonEmptyStrings([
|
||||
...(PLANNING_LAYOUT_STYLE_TONE_OPTIONS[row.style] ??
|
||||
[]),
|
||||
row.styleTone,
|
||||
...PLANNING_LAYOUT_STYLE_OPTIONS,
|
||||
...(row.styles ?? []),
|
||||
]),
|
||||
minWidth: "180px",
|
||||
autoAddOnChange: true,
|
||||
})}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
{renderSearchCell({
|
||||
{renderMultiSelectCell({
|
||||
row,
|
||||
field: "audience",
|
||||
field: "styleTones",
|
||||
label: "风格细分/色系",
|
||||
values: row.styleTones ?? [],
|
||||
options: getPlanningLayoutStyleToneOptions(
|
||||
row.styles ?? [],
|
||||
row.styleTones ?? [],
|
||||
),
|
||||
minWidth: "180px",
|
||||
autoAddOnChange: true,
|
||||
})}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
{renderMultiSelectCell({
|
||||
row,
|
||||
field: "audiences",
|
||||
label: "适用人群",
|
||||
value: row.audience,
|
||||
options: PLANNING_LAYOUT_AUDIENCE_OPTIONS,
|
||||
values: row.audiences ?? [],
|
||||
options: getUniqueNonEmptyStrings([
|
||||
...PLANNING_LAYOUT_AUDIENCE_OPTIONS,
|
||||
...(row.audiences ?? []),
|
||||
]),
|
||||
autoAddOnChange: true,
|
||||
})}
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
|
||||
@ -221,6 +221,43 @@ describe('nonIntegratedStorage', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('normalizes legacy planning layout single-select fields into multi-select fields', () => {
|
||||
window.localStorage.setItem(
|
||||
NON_INTEGRATED_STORAGE_KEY,
|
||||
JSON.stringify([
|
||||
{
|
||||
...createProduct(),
|
||||
planningLayoutRows: [
|
||||
{
|
||||
skuId: 'sku-1',
|
||||
style: '中古风',
|
||||
styleTone: '浅色系(轻法式)',
|
||||
audience: '有孩家庭',
|
||||
spaces: ['书房'],
|
||||
categories: ['书桌/电脑桌'],
|
||||
model: '款式一',
|
||||
priceBand: '500-600',
|
||||
brand: '基本盘',
|
||||
brands: ['顾家'],
|
||||
shop: '天猫',
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
)
|
||||
|
||||
const savedProduct = getNonIntegratedProductById('product-1')
|
||||
|
||||
expect(savedProduct?.planningLayoutRows[0]).toMatchObject({
|
||||
style: '中古风',
|
||||
styles: ['中古风'],
|
||||
styleTone: '浅色系(轻法式)',
|
||||
styleTones: ['浅色系(轻法式)'],
|
||||
audience: '有孩家庭',
|
||||
audiences: ['有孩家庭'],
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps bom details when saving the local record', () => {
|
||||
saveNonIntegratedProduct(
|
||||
createProduct({
|
||||
|
||||
@ -372,8 +372,11 @@ export type NonIntegratedProductLimitRow = {
|
||||
export type NonIntegratedPlanningLayoutRow = {
|
||||
skuId: string;
|
||||
style: string;
|
||||
styles: string[];
|
||||
styleTone: string;
|
||||
styleTones: string[];
|
||||
audience: string;
|
||||
audiences: string[];
|
||||
spaces: string[];
|
||||
categories: string[];
|
||||
model: string;
|
||||
@ -762,6 +765,18 @@ function normalizeString(value: unknown) {
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function normalizeStringList(values: unknown) {
|
||||
if (!Array.isArray(values)) return [];
|
||||
|
||||
return Array.from(
|
||||
new Set(
|
||||
values
|
||||
.map((value) => normalizeString(value).trim())
|
||||
.filter((value) => value.length > 0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeNonIntegratedProjectReview(
|
||||
value: unknown,
|
||||
): NonIntegratedProjectReview {
|
||||
@ -1288,6 +1303,24 @@ function normalizePlanningLayoutRow(
|
||||
item: Partial<NonIntegratedPlanningLayoutRow>,
|
||||
): NonIntegratedPlanningLayoutRow | null {
|
||||
if (typeof item.skuId !== "string") return null;
|
||||
const normalizedStyles = Array.from(
|
||||
new Set([
|
||||
...normalizeStringList(item.styles),
|
||||
normalizeString(item.style).trim(),
|
||||
].filter((value) => value.length > 0)),
|
||||
);
|
||||
const normalizedStyleTones = Array.from(
|
||||
new Set([
|
||||
...normalizeStringList(item.styleTones),
|
||||
normalizeString(item.styleTone).trim(),
|
||||
].filter((value) => value.length > 0)),
|
||||
);
|
||||
const normalizedAudiences = Array.from(
|
||||
new Set([
|
||||
...normalizeStringList(item.audiences),
|
||||
normalizeString(item.audience).trim(),
|
||||
].filter((value) => value.length > 0)),
|
||||
);
|
||||
const normalizedBrands = [
|
||||
...(Array.isArray(item.brands) ? item.brands : []),
|
||||
normalizeString(item.brand),
|
||||
@ -1297,19 +1330,14 @@ function normalizePlanningLayoutRow(
|
||||
|
||||
return {
|
||||
skuId: item.skuId,
|
||||
style: normalizeString(item.style),
|
||||
styleTone: normalizeString(item.styleTone),
|
||||
audience: normalizeString(item.audience),
|
||||
spaces: Array.isArray(item.spaces)
|
||||
? item.spaces
|
||||
.map((value) => normalizeString(value))
|
||||
.filter((value) => value.length > 0)
|
||||
: [],
|
||||
categories: Array.isArray(item.categories)
|
||||
? item.categories
|
||||
.map((value) => normalizeString(value))
|
||||
.filter((value) => value.length > 0)
|
||||
: [],
|
||||
style: normalizedStyles[0] ?? "",
|
||||
styles: normalizedStyles,
|
||||
styleTone: normalizedStyleTones[0] ?? "",
|
||||
styleTones: normalizedStyleTones,
|
||||
audience: normalizedAudiences[0] ?? "",
|
||||
audiences: normalizedAudiences,
|
||||
spaces: normalizeStringList(item.spaces),
|
||||
categories: normalizeStringList(item.categories),
|
||||
model: normalizeString(item.model),
|
||||
priceBand: normalizeString(item.priceBand),
|
||||
brand: normalizedBrands[0] ?? "",
|
||||
@ -2353,6 +2381,9 @@ export function createCopiedNonIntegratedProduct(
|
||||
pricingRows: [],
|
||||
planningLayoutRows: sourceProduct.planningLayoutRows.map((row) => ({
|
||||
...row,
|
||||
styles: [...(row.styles ?? [])],
|
||||
styleTones: [...(row.styleTones ?? [])],
|
||||
audiences: [...(row.audiences ?? [])],
|
||||
spaces: [...row.spaces],
|
||||
categories: [...row.categories],
|
||||
brands: [...row.brands],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user