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 为关闭 # 是否开启 Nitro Mock服务true 为开启false 为关闭
VITE_NITRO_MOCK=false VITE_NITRO_MOCK=false
# 是否开启商品分类管理 mock默认 false分类管理强制走真实 API
VITE_MOCK_PRODUCT_CATEGORY=false
# 是否打开 devtoolstrue 为打开false 为关闭 # 是否打开 devtoolstrue 为打开false 为关闭
VITE_DEVTOOLS=false VITE_DEVTOOLS=false

View File

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

View File

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