chore: force category management to use real tenant APIs

This commit is contained in:
2026-02-20 18:46:11 +08:00
parent c6d0694737
commit 6145eec825
4 changed files with 282 additions and 269 deletions

View File

@@ -12,6 +12,9 @@ VITE_TENANT_ID=806357433394921472
# 是否开启 Nitro Mock服务true 为开启false 为关闭
VITE_NITRO_MOCK=false
# 是否开启商品分类管理 mock默认 false分类管理强制走真实 API
VITE_MOCK_PRODUCT_CATEGORY=false
# 是否打开 devtoolstrue 为打开false 为关闭
VITE_DEVTOOLS=false

View File

@@ -1,6 +1,6 @@
// Mock 数据入口,仅在开发环境下使用
// 门店模块已切换真实 TenantApi此处仅保留其他业务 mock。
// 门店模块与商品分类管理已切换真实 TenantApi此处仅保留其他业务 mock。
import './product';
import './product-extensions';
console.warn('[Mock] 非门店模块 Mock 数据已启用');
console.warn('[Mock] 已启用非门店/非分类管理 Mock 数据(分类管理强制走真实 API');

View File

@@ -150,6 +150,8 @@ const PRODUCT_SEEDS = [
const storeMap = new Map<string, ProductExtensionStoreState>();
let idSeed = 10_000;
const ENABLE_PRODUCT_CATEGORY_MOCK =
import.meta.env.VITE_MOCK_PRODUCT_CATEGORY === 'true';
function parseUrlParams(url: string) {
const parsed = new URL(url, 'http://localhost');
@@ -592,287 +594,291 @@ function ensureStoreState(storeId = '') {
return state;
}
Mock.mock(
/\/product\/category\/manage\/list(?:\?|$)/,
'get',
(options: MockRequestOptions) => {
const params = parseUrlParams(options.url);
const storeId = normalizeText(params.storeId);
const keyword = normalizeText(params.keyword).toLowerCase();
const status = normalizeText(params.status);
const state = ensureStoreState(storeId);
// 分类管理与商品选择器默认走真实 TenantApi需手动开启才启用 mock。
if (ENABLE_PRODUCT_CATEGORY_MOCK) {
Mock.mock(
/\/product\/category\/manage\/list(?:\?|$)/,
'get',
(options: MockRequestOptions) => {
const params = parseUrlParams(options.url);
const storeId = normalizeText(params.storeId);
const keyword = normalizeText(params.keyword).toLowerCase();
const status = normalizeText(params.status);
const state = ensureStoreState(storeId);
const list = state.categories
.toSorted((a, b) => a.sort - b.sort)
.filter((item) => {
if (status && item.status !== status) return false;
if (!keyword) return true;
return (
item.name.toLowerCase().includes(keyword) ||
item.description.toLowerCase().includes(keyword)
);
})
.map((item) => toCategoryManageItem(state, item));
const list = state.categories
.toSorted((a, b) => a.sort - b.sort)
.filter((item) => {
if (status && item.status !== status) return false;
if (!keyword) return true;
return (
item.name.toLowerCase().includes(keyword) ||
item.description.toLowerCase().includes(keyword)
);
})
.map((item) => toCategoryManageItem(state, item));
return { code: 200, data: list };
},
);
return { code: 200, data: list };
},
);
Mock.mock(
/\/product\/category\/manage\/save/,
'post',
(options: MockRequestOptions) => {
const body = parseBody(options);
const storeId = normalizeText(body.storeId);
const id = normalizeText(body.id);
const name = normalizeText(body.name);
if (!storeId || !name) {
return { code: 400, data: null, message: '参数不完整' };
}
Mock.mock(
/\/product\/category\/manage\/save/,
'post',
(options: MockRequestOptions) => {
const body = parseBody(options);
const storeId = normalizeText(body.storeId);
const id = normalizeText(body.id);
const name = normalizeText(body.name);
if (!storeId || !name) {
return { code: 400, data: null, message: '参数不完整' };
}
const state = ensureStoreState(storeId);
const duplicate = state.categories.find(
(item) => item.name === name && item.id !== id,
);
if (duplicate) {
return { code: 400, data: null, message: '分类名称已存在' };
}
const state = ensureStoreState(storeId);
const duplicate = state.categories.find(
(item) => item.name === name && item.id !== id,
);
if (duplicate) {
return { code: 400, data: null, message: '分类名称已存在' };
}
const currentSortMax =
state.categories.reduce((max, item) => Math.max(max, item.sort), 0) + 1;
const existingIndex = state.categories.findIndex((item) => item.id === id);
const currentSortMax =
state.categories.reduce((max, item) => Math.max(max, item.sort), 0) +
1;
const existingIndex = state.categories.findIndex((item) => item.id === id);
const next =
existingIndex === -1
? {
id: createId('cat', storeId),
name,
description: normalizeText(body.description),
icon: normalizeText(body.icon, 'lucide:folder'),
sort: normalizeInt(body.sort, currentSortMax, 1),
status: normalizeSwitchStatus(body.status, 'enabled'),
channels: normalizeChannels(body.channels, ['wm']),
}
: {
...state.categories[existingIndex],
name,
description: normalizeText(
body.description,
state.categories[existingIndex].description,
),
icon: normalizeText(
body.icon,
state.categories[existingIndex].icon,
),
sort: normalizeInt(
body.sort,
state.categories[existingIndex].sort,
1,
),
status: normalizeSwitchStatus(
body.status,
state.categories[existingIndex].status,
),
channels: normalizeChannels(
body.channels,
state.categories[existingIndex].channels,
),
};
const next =
existingIndex === -1
? {
id: createId('cat', storeId),
name,
description: normalizeText(body.description),
icon: normalizeText(body.icon, 'lucide:folder'),
sort: normalizeInt(body.sort, currentSortMax, 1),
status: normalizeSwitchStatus(body.status, 'enabled'),
channels: normalizeChannels(body.channels, ['wm']),
}
: {
...state.categories[existingIndex],
name,
description: normalizeText(
body.description,
state.categories[existingIndex].description,
),
icon: normalizeText(
body.icon,
state.categories[existingIndex].icon,
),
sort: normalizeInt(
body.sort,
state.categories[existingIndex].sort,
1,
),
status: normalizeSwitchStatus(
body.status,
state.categories[existingIndex].status,
),
channels: normalizeChannels(
body.channels,
state.categories[existingIndex].channels,
),
};
if (existingIndex === -1) {
state.categories.push(next);
} else {
state.categories.splice(existingIndex, 1, next);
}
if (existingIndex === -1) {
state.categories.push(next);
} else {
state.categories.splice(existingIndex, 1, next);
}
return { code: 200, data: toCategoryManageItem(state, next) };
},
);
return { code: 200, data: toCategoryManageItem(state, next) };
},
);
Mock.mock(
/\/product\/category\/manage\/delete/,
'post',
(options: MockRequestOptions) => {
const body = parseBody(options);
const storeId = normalizeText(body.storeId);
const categoryId = normalizeText(body.categoryId);
if (!storeId || !categoryId) {
return { code: 400, data: null, message: '参数不完整' };
}
Mock.mock(
/\/product\/category\/manage\/delete/,
'post',
(options: MockRequestOptions) => {
const body = parseBody(options);
const storeId = normalizeText(body.storeId);
const categoryId = normalizeText(body.categoryId);
if (!storeId || !categoryId) {
return { code: 400, data: null, message: '参数不完整' };
}
const state = ensureStoreState(storeId);
const hasProducts = state.products.some(
(item) => item.categoryId === categoryId,
);
if (hasProducts) {
return { code: 400, data: null, message: '分类下仍有商品,不能删除' };
}
const state = ensureStoreState(storeId);
const hasProducts = state.products.some(
(item) => item.categoryId === categoryId,
);
if (hasProducts) {
return { code: 400, data: null, message: '分类下仍有商品,不能删除' };
}
if (state.categories.length <= 1) {
return { code: 400, data: null, message: '至少保留一个分类' };
}
if (state.categories.length <= 1) {
return { code: 400, data: null, message: '至少保留一个分类' };
}
state.categories = state.categories.filter(
(item) => item.id !== categoryId,
);
return { code: 200, data: null };
},
);
state.categories = state.categories.filter(
(item) => item.id !== categoryId,
);
return { code: 200, data: null };
},
);
Mock.mock(
/\/product\/category\/manage\/status/,
'post',
(options: MockRequestOptions) => {
const body = parseBody(options);
const storeId = normalizeText(body.storeId);
const categoryId = normalizeText(body.categoryId);
if (!storeId || !categoryId) {
return { code: 400, data: null, message: '参数不完整' };
}
const state = ensureStoreState(storeId);
const target = state.categories.find((item) => item.id === categoryId);
if (!target) return { code: 404, data: null, message: '分类不存在' };
target.status = normalizeSwitchStatus(body.status, target.status);
return { code: 200, data: toCategoryManageItem(state, target) };
},
);
Mock.mock(
/\/product\/category\/manage\/status/,
'post',
(options: MockRequestOptions) => {
const body = parseBody(options);
const storeId = normalizeText(body.storeId);
const categoryId = normalizeText(body.categoryId);
if (!storeId || !categoryId) {
return { code: 400, data: null, message: '参数不完整' };
}
const state = ensureStoreState(storeId);
const target = state.categories.find((item) => item.id === categoryId);
if (!target) return { code: 404, data: null, message: '分类不存在' };
target.status = normalizeSwitchStatus(body.status, target.status);
return { code: 200, data: toCategoryManageItem(state, target) };
},
);
Mock.mock(
/\/product\/category\/manage\/sort/,
'post',
(options: MockRequestOptions) => {
const body = parseBody(options);
const storeId = normalizeText(body.storeId);
const items = Array.isArray(body.items) ? body.items : [];
if (!storeId) return { code: 400, data: null, message: '参数不完整' };
Mock.mock(
/\/product\/category\/manage\/sort/,
'post',
(options: MockRequestOptions) => {
const body = parseBody(options);
const storeId = normalizeText(body.storeId);
const items = Array.isArray(body.items) ? body.items : [];
if (!storeId) return { code: 400, data: null, message: '参数不完整' };
const sortMap = new Map<string, number>();
for (const item of items) {
if (!item || typeof item !== 'object') continue;
const current = item as Record<string, unknown>;
const id = normalizeText(current.categoryId);
const sort = normalizeInt(current.sort, 0, 1);
if (!id || sort <= 0) continue;
sortMap.set(id, sort);
}
const sortMap = new Map<string, number>();
for (const item of items) {
if (!item || typeof item !== 'object') continue;
const current = item as Record<string, unknown>;
const id = normalizeText(current.categoryId);
const sort = normalizeInt(current.sort, 0, 1);
if (!id || sort <= 0) continue;
sortMap.set(id, sort);
}
const state = ensureStoreState(storeId);
state.categories = state.categories
.map((item) => ({
...item,
sort: sortMap.get(item.id) ?? item.sort,
}))
.toSorted((a, b) => a.sort - b.sort)
.map((item, index) => ({ ...item, sort: index + 1 }));
const state = ensureStoreState(storeId);
state.categories = state.categories
.map((item) => ({
...item,
sort: sortMap.get(item.id) ?? item.sort,
}))
.toSorted((a, b) => a.sort - b.sort)
.map((item, index) => ({ ...item, sort: index + 1 }));
return {
code: 200,
data: state.categories.map((item) => toCategoryManageItem(state, item)),
};
},
);
return {
code: 200,
data: state.categories.map((item) => toCategoryManageItem(state, item)),
};
},
);
Mock.mock(
/\/product\/category\/manage\/products\/bind/,
'post',
(options: MockRequestOptions) => {
const body = parseBody(options);
const storeId = normalizeText(body.storeId);
const categoryId = normalizeText(body.categoryId);
const productIds = normalizeIdList(body.productIds);
if (!storeId || !categoryId || productIds.length === 0) {
return { code: 400, data: null, message: '参数不完整' };
}
Mock.mock(
/\/product\/category\/manage\/products\/bind/,
'post',
(options: MockRequestOptions) => {
const body = parseBody(options);
const storeId = normalizeText(body.storeId);
const categoryId = normalizeText(body.categoryId);
const productIds = normalizeIdList(body.productIds);
if (!storeId || !categoryId || productIds.length === 0) {
return { code: 400, data: null, message: '参数不完整' };
}
const state = ensureStoreState(storeId);
if (!hasCategory(state, categoryId)) {
return { code: 404, data: null, message: '分类不存在' };
}
const state = ensureStoreState(storeId);
if (!hasCategory(state, categoryId)) {
return { code: 404, data: null, message: '分类不存在' };
}
const idSet = new Set(productIds);
let successCount = 0;
for (const item of state.products) {
if (!idSet.has(item.id)) continue;
item.categoryId = categoryId;
successCount += 1;
}
const idSet = new Set(productIds);
let successCount = 0;
for (const item of state.products) {
if (!idSet.has(item.id)) continue;
item.categoryId = categoryId;
successCount += 1;
}
return {
code: 200,
data: {
totalCount: productIds.length,
successCount,
failedCount: Math.max(productIds.length - successCount, 0),
},
};
},
);
return {
code: 200,
data: {
totalCount: productIds.length,
successCount,
failedCount: Math.max(productIds.length - successCount, 0),
},
};
},
);
Mock.mock(
/\/product\/category\/manage\/products\/unbind/,
'post',
(options: MockRequestOptions) => {
const body = parseBody(options);
const storeId = normalizeText(body.storeId);
const categoryId = normalizeText(body.categoryId);
const productId = normalizeText(body.productId);
if (!storeId || !categoryId || !productId) {
return { code: 400, data: null, message: '参数不完整' };
}
Mock.mock(
/\/product\/category\/manage\/products\/unbind/,
'post',
(options: MockRequestOptions) => {
const body = parseBody(options);
const storeId = normalizeText(body.storeId);
const categoryId = normalizeText(body.categoryId);
const productId = normalizeText(body.productId);
if (!storeId || !categoryId || !productId) {
return { code: 400, data: null, message: '参数不完整' };
}
const state = ensureStoreState(storeId);
const fallbackCategory = state.categories.find(
(item) => item.id !== categoryId,
);
if (!fallbackCategory) {
return { code: 400, data: null, message: '无可用目标分类' };
}
const state = ensureStoreState(storeId);
const fallbackCategory = state.categories.find(
(item) => item.id !== categoryId,
);
if (!fallbackCategory) {
return { code: 400, data: null, message: '无可用目标分类' };
}
const target = state.products.find((item) => item.id === productId);
if (!target) return { code: 404, data: null, message: '商品不存在' };
target.categoryId = fallbackCategory.id;
const target = state.products.find((item) => item.id === productId);
if (!target) return { code: 404, data: null, message: '商品不存在' };
target.categoryId = fallbackCategory.id;
return { code: 200, data: target };
},
);
return { code: 200, data: target };
},
);
Mock.mock(
/\/product\/picker\/list(?:\?|$)/,
'get',
(options: MockRequestOptions) => {
const params = parseUrlParams(options.url);
const storeId = normalizeText(params.storeId);
const keyword = normalizeText(params.keyword).toLowerCase();
const categoryId = normalizeText(params.categoryId);
const limit = Math.max(
1,
Math.min(500, normalizeInt(params.limit, 200, 1)),
);
const state = ensureStoreState(storeId);
Mock.mock(
/\/product\/picker\/list(?:\?|$)/,
'get',
(options: MockRequestOptions) => {
const params = parseUrlParams(options.url);
const storeId = normalizeText(params.storeId);
const keyword = normalizeText(params.keyword).toLowerCase();
const categoryId = normalizeText(params.categoryId);
const limit = Math.max(
1,
Math.min(500, normalizeInt(params.limit, 200, 1)),
);
const state = ensureStoreState(storeId);
const list = state.products
.filter((item) => {
if (categoryId && item.categoryId !== categoryId) return false;
if (!keyword) return true;
return (
item.name.toLowerCase().includes(keyword) ||
item.spuCode.toLowerCase().includes(keyword)
);
})
.slice(0, limit)
.map((item) => ({
id: item.id,
name: item.name,
spuCode: item.spuCode,
categoryId: item.categoryId,
categoryName: getCategoryName(state, item.categoryId),
price: item.price,
status: item.status,
}));
const list = state.products
.filter((item) => {
if (categoryId && item.categoryId !== categoryId) return false;
if (!keyword) return true;
return (
item.name.toLowerCase().includes(keyword) ||
item.spuCode.toLowerCase().includes(keyword)
);
})
.slice(0, limit)
.map((item) => ({
id: item.id,
name: item.name,
spuCode: item.spuCode,
categoryId: item.categoryId,
categoryName: getCategoryName(state, item.categoryId),
price: item.price,
status: item.status,
}));
return { code: 200, data: list };
},
);
return { code: 200, data: list };
},
);
}
Mock.mock(
/\/product\/spec\/list(?:\?|$)/,

View File

@@ -296,6 +296,8 @@ const PRODUCT_SEEDS: ProductSeed[] = [
];
const productStoreMap = new Map<string, ProductStoreState>();
const ENABLE_PRODUCT_CATEGORY_MOCK =
import.meta.env.VITE_MOCK_PRODUCT_CATEGORY === 'true';
/** 解析 URL 查询参数。 */
function parseUrlParams(url: string) {
@@ -540,20 +542,22 @@ function resolveStatusByShelfMode(
return fallback;
}
/** 获取商品分类。 */
Mock.mock(
/\/product\/category\/list(?:\?|$)/,
'get',
(options: MockRequestOptions) => {
const params = parseUrlParams(options.url);
const storeId = String(params.storeId || '');
const state = ensureStoreState(storeId);
return {
code: 200,
data: buildCategoryList(state.products),
};
},
);
/** 获取商品分类(默认走真实 TenantApi需手动开启才启用 mock。 */
if (ENABLE_PRODUCT_CATEGORY_MOCK) {
Mock.mock(
/\/product\/category\/list(?:\?|$)/,
'get',
(options: MockRequestOptions) => {
const params = parseUrlParams(options.url);
const storeId = String(params.storeId || '');
const state = ensureStoreState(storeId);
return {
code: 200,
data: buildCategoryList(state.products),
};
},
);
}
/** 获取商品列表。 */
Mock.mock(/\/product\/list(?:\?|$)/, 'get', (options: MockRequestOptions) => {