347 lines
8.9 KiB
TypeScript
347 lines
8.9 KiB
TypeScript
import Mock from 'mockjs';
|
|
|
|
/** 文件职责:费用设置页面 Mock 接口。 */
|
|
interface MockRequestOptions {
|
|
body: null | string;
|
|
type: string;
|
|
url: string;
|
|
}
|
|
|
|
type PackagingFeeMode = 'item' | 'order';
|
|
type OrderPackagingFeeMode = 'fixed' | 'tiered';
|
|
|
|
interface PackagingFeeTierMock {
|
|
fee: number;
|
|
id: string;
|
|
maxAmount: null | number;
|
|
minAmount: number;
|
|
sort: number;
|
|
}
|
|
|
|
interface AdditionalFeeItemMock {
|
|
amount: number;
|
|
enabled: boolean;
|
|
}
|
|
|
|
interface StoreFeesState {
|
|
baseDeliveryFee: number;
|
|
fixedPackagingFee: number;
|
|
freeDeliveryThreshold: null | number;
|
|
minimumOrderAmount: number;
|
|
orderPackagingFeeMode: OrderPackagingFeeMode;
|
|
otherFees: {
|
|
cutlery: AdditionalFeeItemMock;
|
|
rush: AdditionalFeeItemMock;
|
|
};
|
|
packagingFeeMode: PackagingFeeMode;
|
|
packagingFeeTiers: PackagingFeeTierMock[];
|
|
}
|
|
|
|
const storeFeesMap = new Map<string, StoreFeesState>();
|
|
|
|
/** 解析 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);
|
|
} catch (error) {
|
|
console.error('[mock-store-fees] parseBody error:', error);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/** 保留两位小数并裁剪为非负数。 */
|
|
function normalizeMoney(value: unknown, fallback = 0) {
|
|
const parsed = Number(value);
|
|
if (!Number.isFinite(parsed)) return fallback;
|
|
return Math.round(Math.max(0, parsed) * 100) / 100;
|
|
}
|
|
|
|
/** 归一化包装费模式。 */
|
|
function normalizePackagingFeeMode(value: unknown, fallback: PackagingFeeMode) {
|
|
return value === 'item' || value === 'order' ? value : fallback;
|
|
}
|
|
|
|
/** 归一化按订单包装费模式。 */
|
|
function normalizeOrderPackagingFeeMode(
|
|
value: unknown,
|
|
fallback: OrderPackagingFeeMode,
|
|
) {
|
|
return value === 'fixed' || value === 'tiered' ? value : fallback;
|
|
}
|
|
|
|
/** 深拷贝阶梯列表。 */
|
|
function cloneTiers(source: PackagingFeeTierMock[]) {
|
|
return source.map((item) => ({ ...item }));
|
|
}
|
|
|
|
/** 深拷贝状态对象。 */
|
|
function cloneStoreState(source: StoreFeesState): StoreFeesState {
|
|
return {
|
|
minimumOrderAmount: source.minimumOrderAmount,
|
|
baseDeliveryFee: source.baseDeliveryFee,
|
|
freeDeliveryThreshold: source.freeDeliveryThreshold,
|
|
packagingFeeMode: source.packagingFeeMode,
|
|
orderPackagingFeeMode: source.orderPackagingFeeMode,
|
|
fixedPackagingFee: source.fixedPackagingFee,
|
|
packagingFeeTiers: cloneTiers(source.packagingFeeTiers),
|
|
otherFees: {
|
|
cutlery: { ...source.otherFees.cutlery },
|
|
rush: { ...source.otherFees.rush },
|
|
},
|
|
};
|
|
}
|
|
|
|
/** 排序并归一化阶梯列表。 */
|
|
function normalizeTiers(
|
|
source: unknown,
|
|
fallback: PackagingFeeTierMock[],
|
|
): PackagingFeeTierMock[] {
|
|
if (!Array.isArray(source) || source.length === 0) {
|
|
return cloneTiers(fallback);
|
|
}
|
|
|
|
const raw = source
|
|
.map((item, index) => {
|
|
const record = item as Partial<PackagingFeeTierMock>;
|
|
const minAmount = normalizeMoney(record.minAmount, 0);
|
|
let maxAmount: null | number = null;
|
|
if (
|
|
record.maxAmount !== null &&
|
|
record.maxAmount !== undefined &&
|
|
String(record.maxAmount) !== ''
|
|
) {
|
|
maxAmount = normalizeMoney(record.maxAmount, minAmount);
|
|
}
|
|
return {
|
|
id:
|
|
typeof record.id === 'string' && record.id.trim()
|
|
? record.id
|
|
: `fee-tier-${Date.now()}-${index}`,
|
|
minAmount,
|
|
maxAmount,
|
|
fee: normalizeMoney(record.fee, 0),
|
|
sort: Math.max(1, Number(record.sort) || index + 1),
|
|
};
|
|
})
|
|
.toSorted((a, b) => {
|
|
if (a.minAmount !== b.minAmount) return a.minAmount - b.minAmount;
|
|
if (a.maxAmount === null) return 1;
|
|
if (b.maxAmount === null) return -1;
|
|
return a.maxAmount - b.maxAmount;
|
|
})
|
|
.slice(0, 10);
|
|
|
|
let hasUnbounded = false;
|
|
return raw.map((item, index) => {
|
|
let maxAmount = item.maxAmount;
|
|
if (hasUnbounded) {
|
|
maxAmount = item.minAmount + 0.01;
|
|
}
|
|
if (maxAmount !== null && maxAmount <= item.minAmount) {
|
|
maxAmount = item.minAmount + 0.01;
|
|
}
|
|
if (maxAmount === null) hasUnbounded = true;
|
|
return {
|
|
...item,
|
|
maxAmount:
|
|
index === raw.length - 1
|
|
? maxAmount
|
|
: (maxAmount ?? item.minAmount + 1),
|
|
sort: index + 1,
|
|
};
|
|
});
|
|
}
|
|
|
|
/** 归一化其他费用。 */
|
|
function normalizeOtherFees(
|
|
source: unknown,
|
|
fallback: StoreFeesState['otherFees'],
|
|
) {
|
|
const record = (source || {}) as Partial<StoreFeesState['otherFees']>;
|
|
return {
|
|
cutlery: {
|
|
enabled: Boolean(record.cutlery?.enabled),
|
|
amount: normalizeMoney(record.cutlery?.amount, fallback.cutlery.amount),
|
|
},
|
|
rush: {
|
|
enabled: Boolean(record.rush?.enabled),
|
|
amount: normalizeMoney(record.rush?.amount, fallback.rush.amount),
|
|
},
|
|
};
|
|
}
|
|
|
|
/** 归一化提交数据。 */
|
|
function normalizeStoreState(source: unknown, fallback: StoreFeesState) {
|
|
const record = (source || {}) as Partial<StoreFeesState>;
|
|
const packagingFeeMode = normalizePackagingFeeMode(
|
|
record.packagingFeeMode,
|
|
fallback.packagingFeeMode,
|
|
);
|
|
|
|
const orderPackagingFeeMode =
|
|
packagingFeeMode === 'order'
|
|
? normalizeOrderPackagingFeeMode(
|
|
record.orderPackagingFeeMode,
|
|
fallback.orderPackagingFeeMode,
|
|
)
|
|
: 'fixed';
|
|
|
|
return {
|
|
minimumOrderAmount: normalizeMoney(
|
|
record.minimumOrderAmount,
|
|
fallback.minimumOrderAmount,
|
|
),
|
|
baseDeliveryFee: normalizeMoney(
|
|
record.baseDeliveryFee,
|
|
fallback.baseDeliveryFee,
|
|
),
|
|
freeDeliveryThreshold:
|
|
record.freeDeliveryThreshold === null ||
|
|
record.freeDeliveryThreshold === undefined ||
|
|
String(record.freeDeliveryThreshold) === ''
|
|
? null
|
|
: normalizeMoney(
|
|
record.freeDeliveryThreshold,
|
|
fallback.freeDeliveryThreshold ?? 0,
|
|
),
|
|
packagingFeeMode,
|
|
orderPackagingFeeMode,
|
|
fixedPackagingFee: normalizeMoney(
|
|
record.fixedPackagingFee,
|
|
fallback.fixedPackagingFee,
|
|
),
|
|
packagingFeeTiers: normalizeTiers(
|
|
record.packagingFeeTiers,
|
|
fallback.packagingFeeTiers,
|
|
),
|
|
otherFees: normalizeOtherFees(record.otherFees, fallback.otherFees),
|
|
} satisfies StoreFeesState;
|
|
}
|
|
|
|
/** 创建默认状态。 */
|
|
function createDefaultState(): StoreFeesState {
|
|
return {
|
|
minimumOrderAmount: 15,
|
|
baseDeliveryFee: 3,
|
|
freeDeliveryThreshold: 30,
|
|
packagingFeeMode: 'order',
|
|
orderPackagingFeeMode: 'tiered',
|
|
fixedPackagingFee: 2,
|
|
packagingFeeTiers: [
|
|
{
|
|
id: `fee-tier-${Date.now()}-1`,
|
|
minAmount: 0,
|
|
maxAmount: 30,
|
|
fee: 2,
|
|
sort: 1,
|
|
},
|
|
{
|
|
id: `fee-tier-${Date.now()}-2`,
|
|
minAmount: 30,
|
|
maxAmount: 60,
|
|
fee: 3,
|
|
sort: 2,
|
|
},
|
|
{
|
|
id: `fee-tier-${Date.now()}-3`,
|
|
minAmount: 60,
|
|
maxAmount: null,
|
|
fee: 5,
|
|
sort: 3,
|
|
},
|
|
],
|
|
otherFees: {
|
|
cutlery: {
|
|
enabled: false,
|
|
amount: 1,
|
|
},
|
|
rush: {
|
|
enabled: false,
|
|
amount: 3,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
/** 确保门店状态存在。 */
|
|
function ensureStoreState(storeId = '') {
|
|
const key = storeId || 'default';
|
|
let state = storeFeesMap.get(key);
|
|
if (!state) {
|
|
state = createDefaultState();
|
|
storeFeesMap.set(key, state);
|
|
}
|
|
return state;
|
|
}
|
|
|
|
Mock.mock(/\/store\/fees(?:\?|$)/, 'get', (options: MockRequestOptions) => {
|
|
const params = parseUrlParams(options.url);
|
|
const storeId = String(params.storeId || '');
|
|
const state = ensureStoreState(storeId);
|
|
|
|
return {
|
|
code: 200,
|
|
data: {
|
|
storeId,
|
|
...cloneStoreState(state),
|
|
},
|
|
};
|
|
});
|
|
|
|
Mock.mock(/\/store\/fees\/save/, 'post', (options: MockRequestOptions) => {
|
|
const body = parseBody(options);
|
|
const storeId = String((body as { storeId?: unknown }).storeId || '');
|
|
const fallback = ensureStoreState(storeId);
|
|
const next = normalizeStoreState(body, fallback);
|
|
storeFeesMap.set(storeId || 'default', next);
|
|
|
|
return {
|
|
code: 200,
|
|
data: {
|
|
storeId,
|
|
...cloneStoreState(next),
|
|
},
|
|
};
|
|
});
|
|
|
|
Mock.mock(/\/store\/fees\/copy/, 'post', (options: MockRequestOptions) => {
|
|
const body = parseBody(options) as {
|
|
sourceStoreId?: string;
|
|
targetStoreIds?: string[];
|
|
};
|
|
const sourceStoreId = String(body.sourceStoreId || '');
|
|
const targetStoreIds = Array.isArray(body.targetStoreIds)
|
|
? body.targetStoreIds.map(String).filter(Boolean)
|
|
: [];
|
|
|
|
if (!sourceStoreId || targetStoreIds.length === 0) {
|
|
return {
|
|
code: 400,
|
|
message: '参数错误',
|
|
};
|
|
}
|
|
|
|
const source = ensureStoreState(sourceStoreId);
|
|
targetStoreIds.forEach((storeId) => {
|
|
storeFeesMap.set(storeId, cloneStoreState(source));
|
|
});
|
|
|
|
return {
|
|
code: 200,
|
|
data: {
|
|
copiedCount: targetStoreIds.length,
|
|
},
|
|
};
|
|
});
|