feat: 新增费用设置页面并对齐原型交互
This commit is contained in:
346
apps/web-antd/src/mock/store-fees.ts
Normal file
346
apps/web-antd/src/mock/store-fees.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
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,
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user