feat: 新增费用设置页面并对齐原型交互

This commit is contained in:
2026-02-16 16:39:27 +08:00
parent c71c8f1b09
commit aebd0c285b
23 changed files with 2568 additions and 0 deletions

View 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,
},
};
});