feat(project): align store delivery pages with live APIs and geocode status
This commit is contained in:
879
apps/web-antd/src/mock/product.ts
Normal file
879
apps/web-antd/src/mock/product.ts
Normal file
@@ -0,0 +1,879 @@
|
||||
import Mock from 'mockjs';
|
||||
|
||||
/** 文件职责:商品管理页面 Mock 接口。 */
|
||||
interface MockRequestOptions {
|
||||
body: null | string;
|
||||
type: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
type ProductStatus = 'off_shelf' | 'on_sale' | 'sold_out';
|
||||
type ProductKind = 'combo' | 'single';
|
||||
type ProductSoldoutMode = 'permanent' | 'timed' | 'today';
|
||||
type ShelfMode = 'draft' | 'now' | 'scheduled';
|
||||
type BatchActionType =
|
||||
| 'batch_delete'
|
||||
| 'batch_off'
|
||||
| 'batch_on'
|
||||
| 'batch_soldout';
|
||||
|
||||
interface CategorySeed {
|
||||
id: string;
|
||||
name: string;
|
||||
sort: number;
|
||||
}
|
||||
|
||||
interface ProductSeed {
|
||||
categoryId: string;
|
||||
defaultKind?: ProductKind;
|
||||
name: string;
|
||||
price: number;
|
||||
subtitle: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
interface ProductRecord {
|
||||
categoryId: string;
|
||||
description: string;
|
||||
id: string;
|
||||
imageUrl: string;
|
||||
kind: ProductKind;
|
||||
name: string;
|
||||
notifyManager: boolean;
|
||||
originalPrice: null | number;
|
||||
price: number;
|
||||
recoverAt: null | string;
|
||||
remainStock: number;
|
||||
salesMonthly: number;
|
||||
soldoutMode: null | ProductSoldoutMode;
|
||||
soldoutReason: string;
|
||||
spuCode: string;
|
||||
status: ProductStatus;
|
||||
stock: number;
|
||||
storeId: string;
|
||||
subtitle: string;
|
||||
syncToPlatform: boolean;
|
||||
tags: string[];
|
||||
timedOnShelfAt: null | string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface ProductStoreState {
|
||||
nextSeq: number;
|
||||
products: ProductRecord[];
|
||||
}
|
||||
|
||||
const CATEGORY_SEEDS: CategorySeed[] = [
|
||||
{ id: 'cat-hot', name: '热销推荐', sort: 1 },
|
||||
{ id: 'cat-main', name: '主食', sort: 2 },
|
||||
{ id: 'cat-snack', name: '小吃凉菜', sort: 3 },
|
||||
{ id: 'cat-soup', name: '汤羹粥品', sort: 4 },
|
||||
{ id: 'cat-drink', name: '饮品', sort: 5 },
|
||||
{ id: 'cat-alcohol', name: '酒水', sort: 6 },
|
||||
{ id: 'cat-combo', name: '套餐', sort: 7 },
|
||||
];
|
||||
|
||||
const PRODUCT_SEEDS: ProductSeed[] = [
|
||||
{
|
||||
categoryId: 'cat-main',
|
||||
name: '宫保鸡丁',
|
||||
price: 32,
|
||||
subtitle: '经典川菜,香辣可口',
|
||||
tags: ['招牌'],
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-main',
|
||||
name: '鱼香肉丝',
|
||||
price: 28,
|
||||
subtitle: '酸甜开胃,佐饭佳选',
|
||||
tags: ['必点'],
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-main',
|
||||
name: '麻婆豆腐',
|
||||
price: 22,
|
||||
subtitle: '麻辣鲜香,口感细腻',
|
||||
tags: ['辣'],
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-main',
|
||||
name: '蛋炒饭',
|
||||
price: 15,
|
||||
subtitle: '粒粒分明,锅气十足',
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-main',
|
||||
name: '红烧排骨',
|
||||
price: 42,
|
||||
subtitle: '酱香浓郁,肉质软烂',
|
||||
tags: ['招牌'],
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-main',
|
||||
name: '土豆烧牛腩',
|
||||
price: 38,
|
||||
subtitle: '软糯入味,牛腩足量',
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-main',
|
||||
name: '黑椒牛柳意面',
|
||||
price: 36,
|
||||
subtitle: '西式风味,黑椒浓香',
|
||||
tags: ['新品'],
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-main',
|
||||
name: '照烧鸡腿饭',
|
||||
price: 27,
|
||||
subtitle: '日式酱香,鸡腿多汁',
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-snack',
|
||||
name: '蒜香鸡翅',
|
||||
price: 24,
|
||||
subtitle: '外酥里嫩,蒜香扑鼻',
|
||||
tags: ['热卖'],
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-snack',
|
||||
name: '香辣薯条',
|
||||
price: 12,
|
||||
subtitle: '酥脆爽口,轻微辣感',
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-snack',
|
||||
name: '凉拌黄瓜',
|
||||
price: 10,
|
||||
subtitle: '爽脆开胃,解腻优选',
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-snack',
|
||||
name: '口水鸡',
|
||||
price: 26,
|
||||
subtitle: '麻香鲜辣,肉质嫩滑',
|
||||
tags: ['辣'],
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-snack',
|
||||
name: '椒盐玉米',
|
||||
price: 14,
|
||||
subtitle: '外脆内甜,老少皆宜',
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-soup',
|
||||
name: '酸辣汤',
|
||||
price: 18,
|
||||
subtitle: '开胃暖身,酸辣平衡',
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-soup',
|
||||
name: '番茄蛋花汤',
|
||||
price: 16,
|
||||
subtitle: '清爽酸甜,营养丰富',
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-soup',
|
||||
name: '皮蛋瘦肉粥',
|
||||
price: 19,
|
||||
subtitle: '米香浓郁,口感顺滑',
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-soup',
|
||||
name: '海带排骨汤',
|
||||
price: 21,
|
||||
subtitle: '汤鲜味美,口感醇厚',
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-drink',
|
||||
name: '柠檬气泡水',
|
||||
price: 9,
|
||||
subtitle: '清爽解腻,微甜口感',
|
||||
tags: ['新品'],
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-drink',
|
||||
name: '冰粉',
|
||||
price: 8,
|
||||
subtitle: '夏日必备,冰凉清甜',
|
||||
tags: ['限时'],
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-drink',
|
||||
name: '酸梅汤',
|
||||
price: 11,
|
||||
subtitle: '古法熬制,酸甜适中',
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-drink',
|
||||
name: '杨枝甘露',
|
||||
price: 16,
|
||||
subtitle: '果香浓郁,奶香顺滑',
|
||||
tags: ['推荐'],
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-drink',
|
||||
name: '鲜榨橙汁',
|
||||
price: 13,
|
||||
subtitle: '现榨现卖,清新自然',
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-alcohol',
|
||||
name: '青岛啤酒 500ml',
|
||||
price: 12,
|
||||
subtitle: '经典口感,冰镇更佳',
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-alcohol',
|
||||
name: '哈尔滨啤酒 500ml',
|
||||
price: 11,
|
||||
subtitle: '麦香清爽,口感顺滑',
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-alcohol',
|
||||
name: '乌苏啤酒 620ml',
|
||||
price: 15,
|
||||
subtitle: '劲爽畅饮,聚餐优选',
|
||||
tags: ['热卖'],
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-combo',
|
||||
defaultKind: 'combo',
|
||||
name: '双人午市套餐',
|
||||
price: 69,
|
||||
subtitle: '两荤一素+汤+饮料',
|
||||
tags: ['套餐'],
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-combo',
|
||||
defaultKind: 'combo',
|
||||
name: '单人畅享套餐',
|
||||
price: 39,
|
||||
subtitle: '主食+小吃+饮品',
|
||||
tags: ['套餐'],
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-combo',
|
||||
defaultKind: 'combo',
|
||||
name: '家庭四人餐',
|
||||
price: 168,
|
||||
subtitle: '经典大份组合,聚会优选',
|
||||
tags: ['套餐', '招牌'],
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-hot',
|
||||
name: '招牌牛肉饭',
|
||||
price: 34,
|
||||
subtitle: '高人气单品,复购率高',
|
||||
tags: ['热销'],
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-hot',
|
||||
name: '经典鸡排饭',
|
||||
price: 26,
|
||||
subtitle: '酥脆鸡排,份量十足',
|
||||
tags: ['热销'],
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-hot',
|
||||
name: '藤椒鸡腿饭',
|
||||
price: 29,
|
||||
subtitle: '麻香清爽,口味独特',
|
||||
tags: ['推荐'],
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-hot',
|
||||
name: '番茄牛腩饭',
|
||||
price: 33,
|
||||
subtitle: '酸甜开胃,牛腩软烂',
|
||||
},
|
||||
{
|
||||
categoryId: 'cat-hot',
|
||||
name: '椒麻鸡拌面',
|
||||
price: 24,
|
||||
subtitle: '清爽拌面,椒麻上头',
|
||||
tags: ['新品'],
|
||||
},
|
||||
];
|
||||
|
||||
const productStoreMap = new Map<string, ProductStoreState>();
|
||||
|
||||
/** 解析 URL 查询参数。 */
|
||||
function parseUrlParams(url: string) {
|
||||
const parsed = new URL(url, 'http://localhost');
|
||||
const params: Record<string, string> = {};
|
||||
parsed.searchParams.forEach((value, key) => {
|
||||
params[key] = value;
|
||||
});
|
||||
return params;
|
||||
}
|
||||
|
||||
/** 解析请求体 JSON。 */
|
||||
function parseBody(options: MockRequestOptions) {
|
||||
if (!options.body) return {};
|
||||
try {
|
||||
return JSON.parse(options.body) as Record<string, unknown>;
|
||||
} catch (error) {
|
||||
console.error('[mock-product] parseBody error:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/** 统一 yyyy-MM-dd HH:mm:ss。 */
|
||||
function toDateTimeText(date: Date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hour = String(date.getHours()).padStart(2, '0');
|
||||
const minute = String(date.getMinutes()).padStart(2, '0');
|
||||
const second = String(date.getSeconds()).padStart(2, '0');
|
||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
||||
}
|
||||
|
||||
/** 归一化数值输入。 */
|
||||
function normalizeNumber(value: unknown, fallback = 0, min = 0) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return fallback;
|
||||
return Math.max(min, parsed);
|
||||
}
|
||||
|
||||
/** 归一化正整数。 */
|
||||
function normalizeInt(value: unknown, fallback = 0, min = 0) {
|
||||
return Math.floor(normalizeNumber(value, fallback, min));
|
||||
}
|
||||
|
||||
/** 归一化文本。 */
|
||||
function normalizeText(value: unknown, fallback = '') {
|
||||
const text = String(value ?? '').trim();
|
||||
return text || fallback;
|
||||
}
|
||||
|
||||
/** 归一化状态。 */
|
||||
function normalizeStatus(
|
||||
value: unknown,
|
||||
fallback: ProductStatus,
|
||||
): ProductStatus {
|
||||
const next = String(value || '');
|
||||
if (next === 'on_sale' || next === 'off_shelf' || next === 'sold_out') {
|
||||
return next;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/** 归一化商品类型。 */
|
||||
function normalizeKind(value: unknown, fallback: ProductKind): ProductKind {
|
||||
const next = String(value || '');
|
||||
return next === 'combo' || next === 'single' ? next : fallback;
|
||||
}
|
||||
|
||||
/** 归一化沽清模式。 */
|
||||
function normalizeSoldoutMode(
|
||||
value: unknown,
|
||||
fallback: ProductSoldoutMode,
|
||||
): ProductSoldoutMode {
|
||||
const next = String(value || '');
|
||||
if (next === 'today' || next === 'timed' || next === 'permanent') return next;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/** 标准化标签列表。 */
|
||||
function normalizeTags(value: unknown) {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return [
|
||||
...new Set(
|
||||
value
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 8),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/** 查找分类名。 */
|
||||
function getCategoryName(categoryId: string) {
|
||||
return (
|
||||
CATEGORY_SEEDS.find((item) => item.id === categoryId)?.name ??
|
||||
CATEGORY_SEEDS[0]?.name ??
|
||||
'未分类'
|
||||
);
|
||||
}
|
||||
|
||||
/** 生成默认商品池。 */
|
||||
function createDefaultProducts(storeId: string): ProductRecord[] {
|
||||
const now = new Date();
|
||||
const list: ProductRecord[] = [];
|
||||
let seq = 1;
|
||||
for (const seed of PRODUCT_SEEDS) {
|
||||
const statusSeed = seq % 9;
|
||||
let status: ProductStatus = 'on_sale';
|
||||
if (statusSeed === 0) {
|
||||
status = 'sold_out';
|
||||
} else if (statusSeed === 4 || statusSeed === 7) {
|
||||
status = 'off_shelf';
|
||||
}
|
||||
|
||||
let stock = 20 + (seq % 120);
|
||||
if (status === 'sold_out') {
|
||||
stock = 0;
|
||||
} else if (status === 'off_shelf') {
|
||||
stock = 12 + (seq % 20);
|
||||
}
|
||||
const price = Number(seed.price.toFixed(2));
|
||||
const originalPrice =
|
||||
seq % 3 === 0 ? Number((price + (3 + (seq % 8))).toFixed(2)) : null;
|
||||
const createdAt = new Date(now);
|
||||
createdAt.setDate(now.getDate() - (seq % 30));
|
||||
list.push({
|
||||
id: `prd-${storeId}-${String(seq).padStart(4, '0')}`,
|
||||
storeId,
|
||||
spuCode: `SPU2024${String(1000 + seq).padStart(5, '0')}`,
|
||||
name: seed.name,
|
||||
subtitle: seed.subtitle,
|
||||
description: `${seed.name},${seed.subtitle}。`,
|
||||
categoryId: seed.categoryId,
|
||||
kind: seed.defaultKind ?? 'single',
|
||||
price,
|
||||
originalPrice,
|
||||
stock,
|
||||
salesMonthly: 15 + ((seq * 17) % 260),
|
||||
tags: seed.tags ? [...seed.tags] : [],
|
||||
status,
|
||||
soldoutMode: status === 'sold_out' ? 'today' : null,
|
||||
remainStock: stock,
|
||||
recoverAt: null,
|
||||
soldoutReason: status === 'sold_out' ? '食材用完' : '',
|
||||
syncToPlatform: true,
|
||||
notifyManager: false,
|
||||
timedOnShelfAt: null,
|
||||
imageUrl: '',
|
||||
updatedAt: toDateTimeText(createdAt),
|
||||
});
|
||||
seq += 1;
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 创建默认门店状态。 */
|
||||
function createDefaultState(storeId: string): ProductStoreState {
|
||||
const products = createDefaultProducts(storeId);
|
||||
return {
|
||||
products,
|
||||
nextSeq: products.length + 1,
|
||||
};
|
||||
}
|
||||
|
||||
/** 确保门店状态存在。 */
|
||||
function ensureStoreState(storeId = '') {
|
||||
const key = storeId || 'default';
|
||||
let state = productStoreMap.get(key);
|
||||
if (!state) {
|
||||
state = createDefaultState(key);
|
||||
productStoreMap.set(key, state);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
/** 构建分类结果(全量口径,不受筛选影响)。 */
|
||||
function buildCategoryList(products: ProductRecord[]) {
|
||||
const countMap = new Map<string, number>();
|
||||
for (const item of products) {
|
||||
countMap.set(item.categoryId, (countMap.get(item.categoryId) ?? 0) + 1);
|
||||
}
|
||||
return CATEGORY_SEEDS.toSorted((a, b) => a.sort - b.sort).map((category) => ({
|
||||
id: category.id,
|
||||
name: category.name,
|
||||
sort: category.sort,
|
||||
productCount: countMap.get(category.id) ?? 0,
|
||||
}));
|
||||
}
|
||||
|
||||
/** 列表项映射。 */
|
||||
function toListItem(product: ProductRecord) {
|
||||
return {
|
||||
id: product.id,
|
||||
spuCode: product.spuCode,
|
||||
name: product.name,
|
||||
subtitle: product.subtitle,
|
||||
categoryId: product.categoryId,
|
||||
categoryName: getCategoryName(product.categoryId),
|
||||
kind: product.kind,
|
||||
price: product.price,
|
||||
originalPrice: product.originalPrice,
|
||||
stock: product.stock,
|
||||
salesMonthly: product.salesMonthly,
|
||||
tags: [...product.tags],
|
||||
status: product.status,
|
||||
soldoutMode: product.soldoutMode,
|
||||
imageUrl: product.imageUrl,
|
||||
};
|
||||
}
|
||||
|
||||
/** 详情映射。 */
|
||||
function toDetailItem(product: ProductRecord) {
|
||||
return {
|
||||
...toListItem(product),
|
||||
description: product.description,
|
||||
remainStock: product.remainStock,
|
||||
soldoutReason: product.soldoutReason,
|
||||
recoverAt: product.recoverAt,
|
||||
syncToPlatform: product.syncToPlatform,
|
||||
notifyManager: product.notifyManager,
|
||||
};
|
||||
}
|
||||
|
||||
/** 创建商品 ID。 */
|
||||
function createProductId(storeId: string, seq: number) {
|
||||
return `prd-${storeId}-${String(seq).padStart(4, '0')}`;
|
||||
}
|
||||
|
||||
/** 创建 SPU 编码。 */
|
||||
function createSpuCode(seq: number) {
|
||||
return `SPU2026${String(10_000 + seq).slice(-5)}`;
|
||||
}
|
||||
|
||||
/** 解析上架方式。 */
|
||||
function resolveStatusByShelfMode(
|
||||
mode: ShelfMode,
|
||||
fallback: ProductStatus,
|
||||
): ProductStatus {
|
||||
if (mode === 'now') return 'on_sale';
|
||||
if (mode === 'draft' || mode === 'scheduled') return 'off_shelf';
|
||||
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),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
/** 获取商品列表。 */
|
||||
Mock.mock(/\/product\/list(?:\?|$)/, 'get', (options: MockRequestOptions) => {
|
||||
const params = parseUrlParams(options.url);
|
||||
const storeId = String(params.storeId || '');
|
||||
const state = ensureStoreState(storeId);
|
||||
|
||||
const categoryId = String(params.categoryId || '').trim();
|
||||
const keyword = String(params.keyword || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const status = normalizeStatus(params.status, 'on_sale');
|
||||
const kind = normalizeKind(params.kind, 'single');
|
||||
const hasStatus = Boolean(params.status);
|
||||
const hasKind = Boolean(params.kind);
|
||||
|
||||
const page = Math.max(1, normalizeInt(params.page, 1, 1));
|
||||
const pageSize = Math.max(
|
||||
1,
|
||||
Math.min(200, normalizeInt(params.pageSize, 12, 1)),
|
||||
);
|
||||
|
||||
const filtered = state.products.filter((item) => {
|
||||
if (categoryId && item.categoryId !== categoryId) return false;
|
||||
if (keyword) {
|
||||
const hit =
|
||||
item.name.toLowerCase().includes(keyword) ||
|
||||
item.spuCode.toLowerCase().includes(keyword);
|
||||
if (!hit) return false;
|
||||
}
|
||||
if (hasStatus && item.status !== status) return false;
|
||||
if (hasKind && item.kind !== kind) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const ordered = filtered.toSorted((a, b) =>
|
||||
b.updatedAt.localeCompare(a.updatedAt),
|
||||
);
|
||||
const start = (page - 1) * pageSize;
|
||||
const items = ordered.slice(start, start + pageSize);
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
items: items.map((item) => toListItem(item)),
|
||||
total: ordered.length,
|
||||
page,
|
||||
pageSize,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
/** 获取商品详情。 */
|
||||
Mock.mock(/\/product\/detail(?:\?|$)/, 'get', (options: MockRequestOptions) => {
|
||||
const params = parseUrlParams(options.url);
|
||||
const storeId = String(params.storeId || '');
|
||||
const productId = String(params.productId || '');
|
||||
const state = ensureStoreState(storeId);
|
||||
const product = state.products.find((item) => item.id === productId);
|
||||
if (!product) {
|
||||
return {
|
||||
code: 404,
|
||||
data: null,
|
||||
message: '商品不存在',
|
||||
};
|
||||
}
|
||||
return {
|
||||
code: 200,
|
||||
data: toDetailItem(product),
|
||||
};
|
||||
});
|
||||
|
||||
/** 保存商品(新增/编辑)。 */
|
||||
Mock.mock(/\/product\/save/, 'post', (options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const storeId = normalizeText(body.storeId);
|
||||
if (!storeId) {
|
||||
return { code: 400, data: null, message: '门店不能为空' };
|
||||
}
|
||||
const state = ensureStoreState(storeId);
|
||||
|
||||
const id = normalizeText(body.id);
|
||||
const shelfMode = normalizeText(body.shelfMode, 'now') as ShelfMode;
|
||||
const existingIndex = state.products.findIndex((item) => item.id === id);
|
||||
const fallbackStatus =
|
||||
existingIndex === -1 ? 'on_sale' : state.products[existingIndex]?.status;
|
||||
const statusFromBody = normalizeStatus(
|
||||
body.status,
|
||||
fallbackStatus || 'on_sale',
|
||||
);
|
||||
const nextStatus = resolveStatusByShelfMode(shelfMode, statusFromBody);
|
||||
const categoryId = normalizeText(
|
||||
body.categoryId,
|
||||
CATEGORY_SEEDS[0]?.id ?? 'cat-main',
|
||||
);
|
||||
|
||||
const existingProduct =
|
||||
existingIndex === -1 ? null : state.products[existingIndex];
|
||||
const baseProduct: ProductRecord = existingProduct
|
||||
? { ...existingProduct }
|
||||
: {
|
||||
id: createProductId(storeId, state.nextSeq),
|
||||
storeId,
|
||||
spuCode: createSpuCode(state.nextSeq),
|
||||
name: '',
|
||||
subtitle: '',
|
||||
description: '',
|
||||
categoryId,
|
||||
kind: 'single',
|
||||
price: 0,
|
||||
originalPrice: null,
|
||||
stock: 0,
|
||||
salesMonthly: 0,
|
||||
tags: [],
|
||||
status: 'off_shelf',
|
||||
soldoutMode: null,
|
||||
remainStock: 0,
|
||||
recoverAt: null,
|
||||
soldoutReason: '',
|
||||
syncToPlatform: true,
|
||||
notifyManager: false,
|
||||
timedOnShelfAt: null,
|
||||
imageUrl: '',
|
||||
updatedAt: toDateTimeText(new Date()),
|
||||
};
|
||||
|
||||
baseProduct.name = normalizeText(body.name, baseProduct.name);
|
||||
baseProduct.subtitle = normalizeText(body.subtitle, baseProduct.subtitle);
|
||||
baseProduct.description = normalizeText(
|
||||
body.description,
|
||||
baseProduct.description || `${baseProduct.name},${baseProduct.subtitle}`,
|
||||
);
|
||||
baseProduct.categoryId = CATEGORY_SEEDS.some((item) => item.id === categoryId)
|
||||
? categoryId
|
||||
: baseProduct.categoryId;
|
||||
baseProduct.kind = normalizeKind(body.kind, baseProduct.kind);
|
||||
baseProduct.price = Number(
|
||||
normalizeNumber(body.price, baseProduct.price, 0).toFixed(2),
|
||||
);
|
||||
const originalPrice = normalizeNumber(body.originalPrice, -1, 0);
|
||||
baseProduct.originalPrice =
|
||||
originalPrice > 0 ? Number(originalPrice.toFixed(2)) : null;
|
||||
baseProduct.stock = normalizeInt(body.stock, baseProduct.stock, 0);
|
||||
baseProduct.tags = normalizeTags(body.tags);
|
||||
baseProduct.status = nextStatus;
|
||||
baseProduct.soldoutMode = nextStatus === 'sold_out' ? 'today' : null;
|
||||
baseProduct.recoverAt = null;
|
||||
baseProduct.soldoutReason = nextStatus === 'sold_out' ? '库存售罄' : '';
|
||||
baseProduct.remainStock = baseProduct.stock;
|
||||
baseProduct.syncToPlatform = true;
|
||||
baseProduct.notifyManager = false;
|
||||
baseProduct.timedOnShelfAt =
|
||||
shelfMode === 'scheduled' ? normalizeText(body.timedOnShelfAt) : null;
|
||||
baseProduct.updatedAt = toDateTimeText(new Date());
|
||||
|
||||
if (!baseProduct.name) {
|
||||
return {
|
||||
code: 400,
|
||||
data: null,
|
||||
message: '商品名称不能为空',
|
||||
};
|
||||
}
|
||||
|
||||
if (existingIndex === -1) {
|
||||
state.products.unshift(baseProduct);
|
||||
state.nextSeq += 1;
|
||||
} else {
|
||||
state.products.splice(existingIndex, 1, baseProduct);
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: toDetailItem(baseProduct),
|
||||
};
|
||||
});
|
||||
|
||||
/** 删除商品。 */
|
||||
Mock.mock(/\/product\/delete/, 'post', (options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const storeId = normalizeText(body.storeId);
|
||||
const productId = normalizeText(body.productId);
|
||||
if (!storeId || !productId) {
|
||||
return { code: 400, data: null, message: '参数不完整' };
|
||||
}
|
||||
const state = ensureStoreState(storeId);
|
||||
state.products = state.products.filter((item) => item.id !== productId);
|
||||
return { code: 200, data: null };
|
||||
});
|
||||
|
||||
/** 修改商品状态。 */
|
||||
Mock.mock(
|
||||
/\/product\/status\/change/,
|
||||
'post',
|
||||
(options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const storeId = normalizeText(body.storeId);
|
||||
const productId = normalizeText(body.productId);
|
||||
if (!storeId || !productId) {
|
||||
return { code: 400, data: null, message: '参数不完整' };
|
||||
}
|
||||
const state = ensureStoreState(storeId);
|
||||
const product = state.products.find((item) => item.id === productId);
|
||||
if (!product) return { code: 404, data: null, message: '商品不存在' };
|
||||
|
||||
const nextStatus = normalizeStatus(body.status, product.status);
|
||||
product.status = nextStatus;
|
||||
if (nextStatus === 'sold_out') {
|
||||
product.soldoutMode = 'today';
|
||||
product.soldoutReason = product.soldoutReason || '库存售罄';
|
||||
product.remainStock = 0;
|
||||
product.stock = 0;
|
||||
} else {
|
||||
product.soldoutMode = null;
|
||||
product.recoverAt = null;
|
||||
product.soldoutReason = '';
|
||||
}
|
||||
product.updatedAt = toDateTimeText(new Date());
|
||||
return { code: 200, data: toListItem(product) };
|
||||
},
|
||||
);
|
||||
|
||||
/** 商品沽清。 */
|
||||
Mock.mock(/\/product\/soldout/, 'post', (options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const storeId = normalizeText(body.storeId);
|
||||
const productId = normalizeText(body.productId);
|
||||
if (!storeId || !productId) {
|
||||
return { code: 400, data: null, message: '参数不完整' };
|
||||
}
|
||||
const state = ensureStoreState(storeId);
|
||||
const product = state.products.find((item) => item.id === productId);
|
||||
if (!product) return { code: 404, data: null, message: '商品不存在' };
|
||||
|
||||
const mode = normalizeSoldoutMode(body.mode, 'today');
|
||||
const remainStock = normalizeInt(body.remainStock, 0, 0);
|
||||
product.status = 'sold_out';
|
||||
product.soldoutMode = mode;
|
||||
product.remainStock = remainStock;
|
||||
product.stock = remainStock;
|
||||
product.soldoutReason = normalizeText(body.reason);
|
||||
product.recoverAt = mode === 'timed' ? normalizeText(body.recoverAt) : null;
|
||||
product.syncToPlatform = Boolean(body.syncToPlatform);
|
||||
product.notifyManager = Boolean(body.notifyManager);
|
||||
product.updatedAt = toDateTimeText(new Date());
|
||||
return { code: 200, data: toDetailItem(product) };
|
||||
});
|
||||
|
||||
/** 批量商品动作。 */
|
||||
Mock.mock(/\/product\/batch/, 'post', (options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const storeId = normalizeText(body.storeId);
|
||||
const action = normalizeText(body.action) as BatchActionType;
|
||||
const productIds = Array.isArray(body.productIds)
|
||||
? body.productIds.map((item) => String(item || '').trim()).filter(Boolean)
|
||||
: [];
|
||||
|
||||
if (!storeId || productIds.length === 0) {
|
||||
return {
|
||||
code: 400,
|
||||
data: null,
|
||||
message: '批量操作参数不完整',
|
||||
};
|
||||
}
|
||||
|
||||
const state = ensureStoreState(storeId);
|
||||
const idSet = new Set(productIds);
|
||||
const targets = state.products.filter((item) => idSet.has(item.id));
|
||||
let successCount = 0;
|
||||
|
||||
if (action === 'batch_delete') {
|
||||
const before = state.products.length;
|
||||
state.products = state.products.filter((item) => !idSet.has(item.id));
|
||||
successCount = before - state.products.length;
|
||||
} else {
|
||||
for (const item of targets) {
|
||||
switch (action) {
|
||||
case 'batch_off': {
|
||||
item.status = 'off_shelf';
|
||||
item.soldoutMode = null;
|
||||
item.recoverAt = null;
|
||||
item.soldoutReason = '';
|
||||
|
||||
break;
|
||||
}
|
||||
case 'batch_on': {
|
||||
item.status = 'on_sale';
|
||||
item.soldoutMode = null;
|
||||
item.recoverAt = null;
|
||||
item.soldoutReason = '';
|
||||
|
||||
break;
|
||||
}
|
||||
case 'batch_soldout': {
|
||||
item.status = 'sold_out';
|
||||
item.soldoutMode = normalizeSoldoutMode(body.mode, 'today');
|
||||
item.remainStock = normalizeInt(body.remainStock, 0, 0);
|
||||
item.stock = item.remainStock;
|
||||
item.soldoutReason = normalizeText(body.reason);
|
||||
item.recoverAt =
|
||||
item.soldoutMode === 'timed' ? normalizeText(body.recoverAt) : null;
|
||||
item.syncToPlatform = Boolean(body.syncToPlatform ?? true);
|
||||
item.notifyManager = Boolean(body.notifyManager ?? false);
|
||||
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
item.updatedAt = toDateTimeText(new Date());
|
||||
successCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const totalCount = productIds.length;
|
||||
const failedCount = Math.max(totalCount - successCount, 0);
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
action,
|
||||
totalCount,
|
||||
successCount,
|
||||
failedCount,
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user