refactor(@vben/web-antd): remove store mocks and fix staff type checks
This commit is contained in:
@@ -3,8 +3,8 @@ import { unmountGlobalLoading } from '@vben/utils';
|
||||
|
||||
import { overridesPreferences } from './preferences';
|
||||
|
||||
// 开发环境启用 Mock 数据
|
||||
if (import.meta.env.DEV) {
|
||||
// 仅在显式开启时启用 Mock 数据
|
||||
if (import.meta.env.DEV && import.meta.env.VITE_NITRO_MOCK === 'true') {
|
||||
import('./mock');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
// Mock 数据入口,仅在开发环境下使用
|
||||
import './store';
|
||||
import './store-dinein';
|
||||
import './store-fees';
|
||||
import './store-hours';
|
||||
import './store-pickup';
|
||||
import './store-staff';
|
||||
// 门店模块已切换真实 TenantApi,此处仅保留其他业务的 mock。
|
||||
import './product';
|
||||
|
||||
console.warn('[Mock] Mock 数据已启用');
|
||||
console.warn('[Mock] 非门店模块 Mock 数据已启用');
|
||||
|
||||
@@ -1,515 +0,0 @@
|
||||
import Mock from 'mockjs';
|
||||
|
||||
/** 文件职责:堂食管理页面 Mock 接口。 */
|
||||
interface MockRequestOptions {
|
||||
body: null | string;
|
||||
type: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
type DineInTableStatus = 'dining' | 'disabled' | 'free' | 'reserved';
|
||||
|
||||
interface DineInBasicSettingsMock {
|
||||
defaultDiningMinutes: number;
|
||||
enabled: boolean;
|
||||
overtimeReminderMinutes: number;
|
||||
}
|
||||
|
||||
interface DineInAreaMock {
|
||||
description: string;
|
||||
id: string;
|
||||
name: string;
|
||||
sort: number;
|
||||
}
|
||||
|
||||
interface DineInTableMock {
|
||||
areaId: string;
|
||||
code: string;
|
||||
id: string;
|
||||
seats: number;
|
||||
status: DineInTableStatus;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
interface StoreDineInState {
|
||||
areas: DineInAreaMock[];
|
||||
basicSettings: DineInBasicSettingsMock;
|
||||
tables: DineInTableMock[];
|
||||
}
|
||||
|
||||
const storeDineInMap = new Map<string, StoreDineInState>();
|
||||
|
||||
/** 解析 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-store-dinein] parseBody error:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/** 深拷贝基础设置。 */
|
||||
function cloneBasicSettings(source: DineInBasicSettingsMock) {
|
||||
return { ...source };
|
||||
}
|
||||
|
||||
/** 深拷贝区域列表。 */
|
||||
function cloneAreas(source: DineInAreaMock[]) {
|
||||
return source.map((item) => ({ ...item }));
|
||||
}
|
||||
|
||||
/** 深拷贝桌位列表。 */
|
||||
function cloneTables(source: DineInTableMock[]) {
|
||||
return source.map((item) => ({ ...item, tags: [...item.tags] }));
|
||||
}
|
||||
|
||||
/** 深拷贝门店配置。 */
|
||||
function cloneStoreState(source: StoreDineInState): StoreDineInState {
|
||||
return {
|
||||
areas: cloneAreas(source.areas),
|
||||
basicSettings: cloneBasicSettings(source.basicSettings),
|
||||
tables: cloneTables(source.tables),
|
||||
};
|
||||
}
|
||||
|
||||
/** 按排序规则稳定排序区域。 */
|
||||
function sortAreas(source: DineInAreaMock[]) {
|
||||
return cloneAreas(source).toSorted((a, b) => {
|
||||
const sortDiff = a.sort - b.sort;
|
||||
if (sortDiff !== 0) return sortDiff;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
|
||||
/** 按编号排序桌位。 */
|
||||
function sortTables(source: DineInTableMock[]) {
|
||||
return cloneTables(source).toSorted((a, b) => a.code.localeCompare(b.code));
|
||||
}
|
||||
|
||||
/** 生成唯一 ID。 */
|
||||
function createDineInId(prefix: 'area' | 'table') {
|
||||
return `dinein-${prefix}-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
||||
}
|
||||
|
||||
/** 规范化桌位编号。 */
|
||||
function normalizeTableCode(value: unknown) {
|
||||
if (typeof value !== 'string') return '';
|
||||
return value.trim().toUpperCase();
|
||||
}
|
||||
|
||||
/** 数值裁剪为整数区间。 */
|
||||
function clampInt(value: unknown, min: number, max: number, fallback: number) {
|
||||
const numberValue = Number(value);
|
||||
if (!Number.isFinite(numberValue)) return fallback;
|
||||
const normalized = Math.floor(numberValue);
|
||||
return Math.max(min, Math.min(max, normalized));
|
||||
}
|
||||
|
||||
/** 归一化基础设置。 */
|
||||
function normalizeBasicSettings(source: unknown): DineInBasicSettingsMock {
|
||||
const record = typeof source === 'object' && source ? source : {};
|
||||
return {
|
||||
enabled: Boolean((record as { enabled?: unknown }).enabled),
|
||||
defaultDiningMinutes: clampInt(
|
||||
(record as { defaultDiningMinutes?: unknown }).defaultDiningMinutes,
|
||||
1,
|
||||
999,
|
||||
90,
|
||||
),
|
||||
overtimeReminderMinutes: clampInt(
|
||||
(record as { overtimeReminderMinutes?: unknown }).overtimeReminderMinutes,
|
||||
0,
|
||||
999,
|
||||
10,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/** 归一化区域输入。 */
|
||||
function normalizeAreaInput(source: unknown) {
|
||||
const record = typeof source === 'object' && source ? source : {};
|
||||
return {
|
||||
id: String((record as { id?: unknown }).id || createDineInId('area')),
|
||||
name: String((record as { name?: unknown }).name || '').trim(),
|
||||
description: String(
|
||||
(record as { description?: unknown }).description || '',
|
||||
).trim(),
|
||||
sort: clampInt((record as { sort?: unknown }).sort, 1, 999, 1),
|
||||
};
|
||||
}
|
||||
|
||||
/** 归一化桌位状态。 */
|
||||
function normalizeTableStatus(status: unknown): DineInTableStatus {
|
||||
if (
|
||||
status === 'free' ||
|
||||
status === 'disabled' ||
|
||||
status === 'dining' ||
|
||||
status === 'reserved'
|
||||
) {
|
||||
return status;
|
||||
}
|
||||
return 'free';
|
||||
}
|
||||
|
||||
/** 归一化桌位输入。 */
|
||||
function normalizeTableInput(source: unknown) {
|
||||
const record = typeof source === 'object' && source ? source : {};
|
||||
const tags = Array.isArray((record as { tags?: unknown }).tags)
|
||||
? ((record as { tags: unknown[] }).tags
|
||||
.map((item) => String(item).trim())
|
||||
.filter(Boolean) as string[])
|
||||
: [];
|
||||
|
||||
return {
|
||||
id: String((record as { id?: unknown }).id || createDineInId('table')),
|
||||
code: normalizeTableCode((record as { code?: unknown }).code),
|
||||
areaId: String((record as { areaId?: unknown }).areaId || ''),
|
||||
seats: clampInt((record as { seats?: unknown }).seats, 1, 20, 4),
|
||||
status: normalizeTableStatus((record as { status?: unknown }).status),
|
||||
tags: [...new Set(tags)],
|
||||
};
|
||||
}
|
||||
|
||||
/** 生成批量桌位编号。 */
|
||||
function generateBatchCodes(payload: {
|
||||
codePrefix: string;
|
||||
count: number;
|
||||
startNumber: number;
|
||||
}) {
|
||||
const prefix = payload.codePrefix.trim().toUpperCase() || 'A';
|
||||
const start = Math.max(1, Math.floor(payload.startNumber));
|
||||
const count = Math.max(1, Math.min(50, Math.floor(payload.count)));
|
||||
const width = Math.max(String(start + count - 1).length, 2);
|
||||
|
||||
return Array.from({ length: count }).map((_, index) => {
|
||||
const codeNumber = String(start + index).padStart(width, '0');
|
||||
return `${prefix}${codeNumber}`;
|
||||
});
|
||||
}
|
||||
|
||||
/** 构建默认状态。 */
|
||||
function createDefaultState(): StoreDineInState {
|
||||
const areas: DineInAreaMock[] = [
|
||||
{
|
||||
id: createDineInId('area'),
|
||||
name: '大厅',
|
||||
description: '主要用餐区域,共12张桌位,可容纳约48人同时用餐',
|
||||
sort: 1,
|
||||
},
|
||||
{
|
||||
id: createDineInId('area'),
|
||||
name: '包间',
|
||||
description: '安静独立区域,适合聚餐与商务接待',
|
||||
sort: 2,
|
||||
},
|
||||
{
|
||||
id: createDineInId('area'),
|
||||
name: '露台',
|
||||
description: '开放式外摆区域,适合休闲场景',
|
||||
sort: 3,
|
||||
},
|
||||
];
|
||||
|
||||
const hallId = areas[0]?.id ?? '';
|
||||
const privateRoomId = areas[1]?.id ?? '';
|
||||
const terraceId = areas[2]?.id ?? '';
|
||||
|
||||
return {
|
||||
basicSettings: {
|
||||
enabled: true,
|
||||
defaultDiningMinutes: 90,
|
||||
overtimeReminderMinutes: 10,
|
||||
},
|
||||
areas: sortAreas(areas),
|
||||
tables: sortTables([
|
||||
{
|
||||
id: createDineInId('table'),
|
||||
code: 'A01',
|
||||
areaId: hallId,
|
||||
seats: 4,
|
||||
status: 'free',
|
||||
tags: ['靠窗'],
|
||||
},
|
||||
{
|
||||
id: createDineInId('table'),
|
||||
code: 'A02',
|
||||
areaId: hallId,
|
||||
seats: 2,
|
||||
status: 'dining',
|
||||
tags: [],
|
||||
},
|
||||
{
|
||||
id: createDineInId('table'),
|
||||
code: 'A03',
|
||||
areaId: hallId,
|
||||
seats: 6,
|
||||
status: 'free',
|
||||
tags: ['VIP', '靠窗'],
|
||||
},
|
||||
{
|
||||
id: createDineInId('table'),
|
||||
code: 'A04',
|
||||
areaId: hallId,
|
||||
seats: 4,
|
||||
status: 'reserved',
|
||||
tags: [],
|
||||
},
|
||||
{
|
||||
id: createDineInId('table'),
|
||||
code: 'A07',
|
||||
areaId: hallId,
|
||||
seats: 4,
|
||||
status: 'disabled',
|
||||
tags: [],
|
||||
},
|
||||
{
|
||||
id: createDineInId('table'),
|
||||
code: 'V01',
|
||||
areaId: privateRoomId,
|
||||
seats: 8,
|
||||
status: 'dining',
|
||||
tags: ['包厢'],
|
||||
},
|
||||
{
|
||||
id: createDineInId('table'),
|
||||
code: 'T01',
|
||||
areaId: terraceId,
|
||||
seats: 4,
|
||||
status: 'free',
|
||||
tags: ['露台'],
|
||||
},
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
/** 确保门店状态存在。 */
|
||||
function ensureStoreState(storeId = '') {
|
||||
const key = storeId || 'default';
|
||||
let state = storeDineInMap.get(key);
|
||||
if (!state) {
|
||||
state = createDefaultState();
|
||||
storeDineInMap.set(key, state);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
// 获取门店堂食设置
|
||||
Mock.mock(/\/store\/dinein(?:\?|$)/, 'get', (options: MockRequestOptions) => {
|
||||
const params = parseUrlParams(options.url);
|
||||
const storeId = String(params.storeId || '');
|
||||
const state = ensureStoreState(storeId);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
storeId,
|
||||
basicSettings: cloneBasicSettings(state.basicSettings),
|
||||
areas: cloneAreas(state.areas),
|
||||
tables: cloneTables(state.tables),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// 保存堂食基础设置
|
||||
Mock.mock(
|
||||
/\/store\/dinein\/basic\/save/,
|
||||
'post',
|
||||
(options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const storeId = String(body.storeId || '');
|
||||
if (!storeId) return { code: 200, data: null };
|
||||
|
||||
const state = ensureStoreState(storeId);
|
||||
state.basicSettings = normalizeBasicSettings(body.basicSettings);
|
||||
return { code: 200, data: null };
|
||||
},
|
||||
);
|
||||
|
||||
// 新增 / 编辑堂食区域
|
||||
Mock.mock(
|
||||
/\/store\/dinein\/area\/save/,
|
||||
'post',
|
||||
(options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const storeId = String(body.storeId || '');
|
||||
if (!storeId) return { code: 200, data: null };
|
||||
|
||||
const state = ensureStoreState(storeId);
|
||||
const area = normalizeAreaInput(body.area);
|
||||
if (!area.name) return { code: 200, data: null };
|
||||
|
||||
const existingIndex = state.areas.findIndex((item) => item.id === area.id);
|
||||
if (existingIndex === -1) {
|
||||
state.areas.push(area);
|
||||
} else {
|
||||
state.areas[existingIndex] = area;
|
||||
}
|
||||
state.areas = sortAreas(state.areas);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: { ...area },
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// 删除堂食区域
|
||||
Mock.mock(
|
||||
/\/store\/dinein\/area\/delete/,
|
||||
'post',
|
||||
(options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const storeId = String(body.storeId || '');
|
||||
const areaId = String(body.areaId || '');
|
||||
if (!storeId || !areaId) return { code: 200, data: null };
|
||||
|
||||
const state = ensureStoreState(storeId);
|
||||
const hasTables = state.tables.some((item) => item.areaId === areaId);
|
||||
if (hasTables) {
|
||||
return {
|
||||
code: 400,
|
||||
data: null,
|
||||
message: '该区域仍有桌位,请先迁移或删除桌位',
|
||||
};
|
||||
}
|
||||
|
||||
state.areas = state.areas.filter((item) => item.id !== areaId);
|
||||
return { code: 200, data: null };
|
||||
},
|
||||
);
|
||||
|
||||
// 新增 / 编辑堂食桌位
|
||||
Mock.mock(
|
||||
/\/store\/dinein\/table\/save/,
|
||||
'post',
|
||||
(options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const storeId = String(body.storeId || '');
|
||||
if (!storeId) return { code: 200, data: null };
|
||||
|
||||
const state = ensureStoreState(storeId);
|
||||
const table = normalizeTableInput(body.table);
|
||||
if (!table.code || !table.areaId) return { code: 200, data: null };
|
||||
|
||||
const existingIndex = state.tables.findIndex(
|
||||
(item) => item.id === table.id,
|
||||
);
|
||||
if (existingIndex === -1) {
|
||||
state.tables.push(table);
|
||||
} else {
|
||||
state.tables[existingIndex] = table;
|
||||
}
|
||||
state.tables = sortTables(state.tables);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: { ...table, tags: [...table.tags] },
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// 删除堂食桌位
|
||||
Mock.mock(
|
||||
/\/store\/dinein\/table\/delete/,
|
||||
'post',
|
||||
(options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const storeId = String(body.storeId || '');
|
||||
const tableId = String(body.tableId || '');
|
||||
if (!storeId || !tableId) return { code: 200, data: null };
|
||||
|
||||
const state = ensureStoreState(storeId);
|
||||
state.tables = state.tables.filter((item) => item.id !== tableId);
|
||||
return { code: 200, data: null };
|
||||
},
|
||||
);
|
||||
|
||||
// 批量生成堂食桌位
|
||||
Mock.mock(
|
||||
/\/store\/dinein\/table\/batch-create/,
|
||||
'post',
|
||||
(options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const storeId = String(body.storeId || '');
|
||||
if (!storeId) return { code: 200, data: { createdTables: [] } };
|
||||
|
||||
const state = ensureStoreState(storeId);
|
||||
const areaId = String(body.areaId || '');
|
||||
if (!areaId) return { code: 200, data: { createdTables: [] } };
|
||||
|
||||
const count = clampInt(body.count, 1, 50, 1);
|
||||
const startNumber = clampInt(body.startNumber, 1, 9999, 1);
|
||||
const codePrefix = String(body.codePrefix || 'A');
|
||||
const seats = clampInt(body.seats, 1, 20, 4);
|
||||
const nextCodes = generateBatchCodes({
|
||||
codePrefix,
|
||||
count,
|
||||
startNumber,
|
||||
});
|
||||
|
||||
const existingCodeSet = new Set(
|
||||
state.tables.map((item) => item.code.toUpperCase()),
|
||||
);
|
||||
const createdTables: DineInTableMock[] = [];
|
||||
for (const code of nextCodes) {
|
||||
if (existingCodeSet.has(code.toUpperCase())) continue;
|
||||
createdTables.push({
|
||||
id: createDineInId('table'),
|
||||
areaId,
|
||||
code,
|
||||
seats,
|
||||
status: 'free',
|
||||
tags: [],
|
||||
});
|
||||
}
|
||||
|
||||
state.tables = sortTables([...state.tables, ...createdTables]);
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
createdTables: cloneTables(createdTables),
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// 复制门店堂食设置
|
||||
Mock.mock(/\/store\/dinein\/copy/, 'post', (options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const sourceStoreId = String(body.sourceStoreId || '');
|
||||
const targetStoreIds: string[] = Array.isArray(body.targetStoreIds)
|
||||
? body.targetStoreIds.map(String).filter(Boolean)
|
||||
: [];
|
||||
|
||||
if (!sourceStoreId || targetStoreIds.length === 0) {
|
||||
return {
|
||||
code: 200,
|
||||
data: { copiedCount: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const sourceState = ensureStoreState(sourceStoreId);
|
||||
const uniqueTargets = [...new Set(targetStoreIds)].filter(
|
||||
(item) => item !== sourceStoreId,
|
||||
);
|
||||
for (const targetId of uniqueTargets) {
|
||||
storeDineInMap.set(targetId, cloneStoreState(sourceState));
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: { copiedCount: uniqueTargets.length },
|
||||
};
|
||||
});
|
||||
@@ -1,346 +0,0 @@
|
||||
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,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -1,417 +0,0 @@
|
||||
import Mock from 'mockjs';
|
||||
|
||||
const Random = Mock.Random;
|
||||
|
||||
/** mockjs 请求回调参数 */
|
||||
interface MockRequestOptions {
|
||||
url: string;
|
||||
type: string;
|
||||
body: null | string;
|
||||
}
|
||||
|
||||
interface TimeSlotMock {
|
||||
id: string;
|
||||
type: number;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
capacity?: number;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
interface DayHoursMock {
|
||||
dayOfWeek: number;
|
||||
isOpen: boolean;
|
||||
slots: TimeSlotMock[];
|
||||
}
|
||||
|
||||
interface HolidayMock {
|
||||
id: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
type: number;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
reason: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
interface StoreHoursState {
|
||||
holidays: HolidayMock[];
|
||||
weeklyHours: DayHoursMock[];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function parseBody(options: MockRequestOptions) {
|
||||
if (!options.body) return {};
|
||||
try {
|
||||
return JSON.parse(options.body);
|
||||
} catch (error) {
|
||||
console.error('[mock-store-hours] parseBody error:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDate(date?: string) {
|
||||
if (!date) return '';
|
||||
return String(date).slice(0, 10);
|
||||
}
|
||||
|
||||
function normalizeTime(time?: string) {
|
||||
if (!time) return '';
|
||||
const matched = /(\d{2}:\d{2})/.exec(time);
|
||||
return matched?.[1] ?? '';
|
||||
}
|
||||
|
||||
function sortSlots(slots: TimeSlotMock[]) {
|
||||
return [...slots].toSorted((a, b) => {
|
||||
const startA = a.startTime;
|
||||
const startB = b.startTime;
|
||||
if (startA !== startB) return startA.localeCompare(startB);
|
||||
return a.type - b.type;
|
||||
});
|
||||
}
|
||||
|
||||
function sortHolidays(holidays: HolidayMock[]) {
|
||||
return [...holidays].toSorted((a, b) => {
|
||||
const dateCompare = a.startDate.localeCompare(b.startDate);
|
||||
if (dateCompare !== 0) return dateCompare;
|
||||
return a.id.localeCompare(b.id);
|
||||
});
|
||||
}
|
||||
|
||||
function cloneWeeklyHours(weeklyHours: DayHoursMock[]) {
|
||||
return weeklyHours.map((day) => ({
|
||||
...day,
|
||||
slots: day.slots.map((slot) => ({ ...slot })),
|
||||
}));
|
||||
}
|
||||
|
||||
function cloneHolidays(holidays: HolidayMock[]) {
|
||||
return holidays.map((holiday) => ({ ...holiday }));
|
||||
}
|
||||
|
||||
function createDefaultWeeklyHours(): DayHoursMock[] {
|
||||
const weekdays = [
|
||||
{
|
||||
dayOfWeek: 0,
|
||||
bizEnd: '22:00',
|
||||
delEnd: '21:30',
|
||||
delCap: 50,
|
||||
pickEnd: '21:00',
|
||||
},
|
||||
{
|
||||
dayOfWeek: 1,
|
||||
bizEnd: '22:00',
|
||||
delEnd: '21:30',
|
||||
delCap: 50,
|
||||
pickEnd: '21:00',
|
||||
},
|
||||
{
|
||||
dayOfWeek: 2,
|
||||
bizEnd: '22:00',
|
||||
delEnd: '21:30',
|
||||
delCap: 50,
|
||||
pickEnd: '21:00',
|
||||
},
|
||||
{
|
||||
dayOfWeek: 3,
|
||||
bizEnd: '22:00',
|
||||
delEnd: '21:30',
|
||||
delCap: 50,
|
||||
pickEnd: '21:00',
|
||||
},
|
||||
{
|
||||
dayOfWeek: 4,
|
||||
bizEnd: '23:00',
|
||||
delEnd: '22:30',
|
||||
delCap: 80,
|
||||
pickEnd: '22:00',
|
||||
},
|
||||
{
|
||||
dayOfWeek: 5,
|
||||
bizEnd: '23:00',
|
||||
delEnd: '22:30',
|
||||
delCap: 80,
|
||||
pickEnd: '22:00',
|
||||
},
|
||||
];
|
||||
|
||||
const result = weekdays.map((day) => ({
|
||||
dayOfWeek: day.dayOfWeek,
|
||||
isOpen: true,
|
||||
slots: [
|
||||
{ id: Random.guid(), type: 1, startTime: '09:00', endTime: day.bizEnd },
|
||||
{
|
||||
id: Random.guid(),
|
||||
type: 2,
|
||||
startTime: '10:00',
|
||||
endTime: day.delEnd,
|
||||
capacity: day.delCap,
|
||||
},
|
||||
{ id: Random.guid(), type: 3, startTime: '09:00', endTime: day.pickEnd },
|
||||
],
|
||||
}));
|
||||
|
||||
result.push({
|
||||
dayOfWeek: 6,
|
||||
isOpen: true,
|
||||
slots: [
|
||||
{ id: Random.guid(), type: 1, startTime: '10:00', endTime: '22:00' },
|
||||
{
|
||||
id: Random.guid(),
|
||||
type: 2,
|
||||
startTime: '10:30',
|
||||
endTime: '21:30',
|
||||
capacity: 60,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function createDefaultHolidays(): HolidayMock[] {
|
||||
return [
|
||||
{
|
||||
id: Random.guid(),
|
||||
startDate: '2026-02-17',
|
||||
endDate: '2026-02-19',
|
||||
type: 1,
|
||||
reason: '春节假期',
|
||||
},
|
||||
{
|
||||
id: Random.guid(),
|
||||
startDate: '2026-04-05',
|
||||
endDate: '2026-04-05',
|
||||
type: 1,
|
||||
reason: '清明节',
|
||||
},
|
||||
{
|
||||
id: Random.guid(),
|
||||
startDate: '2026-02-14',
|
||||
endDate: '2026-02-14',
|
||||
type: 2,
|
||||
startTime: '09:00',
|
||||
endTime: '23:30',
|
||||
reason: '情人节延长营业',
|
||||
},
|
||||
{
|
||||
id: Random.guid(),
|
||||
startDate: '2026-05-01',
|
||||
endDate: '2026-05-01',
|
||||
type: 2,
|
||||
startTime: '10:00',
|
||||
endTime: '20:00',
|
||||
reason: '劳动节缩短营业',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function normalizeWeeklyHoursInput(list: any): DayHoursMock[] {
|
||||
const dayMap = new Map<number, DayHoursMock>();
|
||||
|
||||
if (Array.isArray(list)) {
|
||||
for (const item of list) {
|
||||
const dayOfWeek = Number(item?.dayOfWeek);
|
||||
if (!Number.isInteger(dayOfWeek) || dayOfWeek < 0 || dayOfWeek > 6)
|
||||
continue;
|
||||
|
||||
const slots: TimeSlotMock[] = Array.isArray(item?.slots)
|
||||
? item.slots.map((slot: any) => ({
|
||||
id: String(slot?.id || Random.guid()),
|
||||
type: Number(slot?.type) || 1,
|
||||
startTime: normalizeTime(slot?.startTime) || '09:00',
|
||||
endTime: normalizeTime(slot?.endTime) || '22:00',
|
||||
capacity:
|
||||
Number(slot?.type) === 2 && slot?.capacity !== undefined
|
||||
? Number(slot.capacity)
|
||||
: undefined,
|
||||
remark: slot?.remark || undefined,
|
||||
}))
|
||||
: [];
|
||||
|
||||
dayMap.set(dayOfWeek, {
|
||||
dayOfWeek,
|
||||
isOpen: Boolean(item?.isOpen),
|
||||
slots: sortSlots(slots),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from({ length: 7 }).map((_, dayOfWeek) => {
|
||||
return (
|
||||
dayMap.get(dayOfWeek) ?? {
|
||||
dayOfWeek,
|
||||
isOpen: false,
|
||||
slots: [],
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeHolidayInput(holiday: any): HolidayMock {
|
||||
const type = Number(holiday?.type) === 2 ? 2 : 1;
|
||||
|
||||
return {
|
||||
id: String(holiday?.id || Random.guid()),
|
||||
startDate:
|
||||
normalizeDate(holiday?.startDate) || normalizeDate(holiday?.date),
|
||||
endDate:
|
||||
normalizeDate(holiday?.endDate) ||
|
||||
normalizeDate(holiday?.startDate) ||
|
||||
normalizeDate(holiday?.date),
|
||||
type,
|
||||
startTime:
|
||||
type === 2 ? normalizeTime(holiday?.startTime) || undefined : undefined,
|
||||
endTime:
|
||||
type === 2 ? normalizeTime(holiday?.endTime) || undefined : undefined,
|
||||
reason: holiday?.reason || '',
|
||||
remark: holiday?.remark || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const storeHoursMap = new Map<string, StoreHoursState>();
|
||||
|
||||
function ensureStoreState(storeId = '') {
|
||||
const key = storeId || 'default';
|
||||
let state = storeHoursMap.get(key);
|
||||
if (!state) {
|
||||
state = {
|
||||
weeklyHours: createDefaultWeeklyHours(),
|
||||
holidays: createDefaultHolidays(),
|
||||
};
|
||||
storeHoursMap.set(key, state);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
// 获取门店营业时间
|
||||
Mock.mock(/\/store\/hours(?:\?|$)/, 'get', (options: MockRequestOptions) => {
|
||||
const params = parseUrlParams(options.url);
|
||||
const storeId = params.storeId || '';
|
||||
const state = ensureStoreState(storeId);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
storeId,
|
||||
weeklyHours: cloneWeeklyHours(state.weeklyHours),
|
||||
holidays: cloneHolidays(state.holidays),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// 保存每周营业时间
|
||||
Mock.mock(/\/store\/hours\/weekly/, 'post', (options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const storeId = String(body.storeId || '');
|
||||
const state = ensureStoreState(storeId);
|
||||
state.weeklyHours = normalizeWeeklyHoursInput(body.weeklyHours);
|
||||
return { code: 200, data: null };
|
||||
});
|
||||
|
||||
// 删除特殊日期
|
||||
Mock.mock(
|
||||
/\/store\/hours\/holiday\/delete/,
|
||||
'post',
|
||||
(options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const holidayId = String(body.id || '');
|
||||
if (!holidayId) return { code: 200, data: null };
|
||||
|
||||
for (const [, state] of storeHoursMap) {
|
||||
const index = state.holidays.findIndex(
|
||||
(holiday) => holiday.id === holidayId,
|
||||
);
|
||||
if (index !== -1) {
|
||||
state.holidays.splice(index, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { code: 200, data: null };
|
||||
},
|
||||
);
|
||||
|
||||
// 新增 / 编辑特殊日期
|
||||
Mock.mock(
|
||||
/\/store\/hours\/holiday(?!\/delete)/,
|
||||
'post',
|
||||
(options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const storeId = String(body.storeId || '');
|
||||
const state = ensureStoreState(storeId);
|
||||
const incomingHoliday = normalizeHolidayInput(body.holiday);
|
||||
|
||||
const existingIndex = state.holidays.findIndex(
|
||||
(item) => item.id === incomingHoliday.id,
|
||||
);
|
||||
if (existingIndex === -1) {
|
||||
state.holidays.push(incomingHoliday);
|
||||
} else {
|
||||
state.holidays[existingIndex] = incomingHoliday;
|
||||
}
|
||||
|
||||
state.holidays = sortHolidays(state.holidays);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: { ...incomingHoliday },
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// 复制营业时间
|
||||
Mock.mock(/\/store\/hours\/copy/, 'post', (options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const sourceStoreId = String(body.sourceStoreId || '');
|
||||
const targetStoreIds: string[] = Array.isArray(body.targetStoreIds)
|
||||
? body.targetStoreIds.map(String).filter(Boolean)
|
||||
: [];
|
||||
|
||||
if (!sourceStoreId || targetStoreIds.length === 0) {
|
||||
return {
|
||||
code: 200,
|
||||
data: { copiedCount: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const includeWeeklyHours = body.includeWeeklyHours !== false;
|
||||
const includeHolidays = body.includeHolidays !== false;
|
||||
const sourceState = ensureStoreState(sourceStoreId);
|
||||
|
||||
const uniqueTargets = [...new Set<string>(targetStoreIds)].filter(
|
||||
(id) => id !== sourceStoreId,
|
||||
);
|
||||
|
||||
for (const targetId of uniqueTargets) {
|
||||
const targetState = ensureStoreState(targetId);
|
||||
if (includeWeeklyHours) {
|
||||
targetState.weeklyHours = cloneWeeklyHours(sourceState.weeklyHours);
|
||||
}
|
||||
if (includeHolidays) {
|
||||
targetState.holidays = cloneHolidays(sourceState.holidays).map(
|
||||
(holiday) => ({
|
||||
...holiday,
|
||||
id: Random.guid(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
copiedCount: uniqueTargets.length,
|
||||
includeHolidays,
|
||||
includeWeeklyHours,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -1,582 +0,0 @@
|
||||
import Mock from 'mockjs';
|
||||
|
||||
const Random = Mock.Random;
|
||||
|
||||
/** 文件职责:自提设置页面 Mock 接口。 */
|
||||
interface MockRequestOptions {
|
||||
body: null | string;
|
||||
type: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
type PickupMode = 'big' | 'fine';
|
||||
type PickupWeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
type PickupPreviewStatus = 'almost' | 'available' | 'expired' | 'full';
|
||||
|
||||
interface PickupBasicSettingsMock {
|
||||
allowSameDayPickup: boolean;
|
||||
bookingDays: number;
|
||||
maxItemsPerOrder: null | number;
|
||||
}
|
||||
|
||||
interface PickupSlotMock {
|
||||
capacity: number;
|
||||
cutoffMinutes: number;
|
||||
dayOfWeeks: PickupWeekDay[];
|
||||
enabled: boolean;
|
||||
endTime: string;
|
||||
id: string;
|
||||
name: string;
|
||||
reservedCount: number;
|
||||
startTime: string;
|
||||
}
|
||||
|
||||
interface PickupFineRuleMock {
|
||||
dayEndTime: string;
|
||||
dayOfWeeks: PickupWeekDay[];
|
||||
dayStartTime: string;
|
||||
intervalMinutes: number;
|
||||
minAdvanceHours: number;
|
||||
slotCapacity: number;
|
||||
}
|
||||
|
||||
interface PickupPreviewSlotMock {
|
||||
remainingCount: number;
|
||||
status: PickupPreviewStatus;
|
||||
time: string;
|
||||
}
|
||||
|
||||
interface PickupPreviewDayMock {
|
||||
date: string;
|
||||
label: string;
|
||||
slots: PickupPreviewSlotMock[];
|
||||
subLabel: string;
|
||||
}
|
||||
|
||||
interface StorePickupState {
|
||||
basicSettings: PickupBasicSettingsMock;
|
||||
bigSlots: PickupSlotMock[];
|
||||
fineRule: PickupFineRuleMock;
|
||||
mode: PickupMode;
|
||||
previewDays: PickupPreviewDayMock[];
|
||||
}
|
||||
|
||||
const ALL_WEEK_DAYS: PickupWeekDay[] = [0, 1, 2, 3, 4, 5, 6];
|
||||
const WEEKDAY_ONLY: PickupWeekDay[] = [0, 1, 2, 3, 4];
|
||||
const WEEKEND_ONLY: PickupWeekDay[] = [5, 6];
|
||||
|
||||
const WEEKDAY_LABEL_MAP: Record<PickupWeekDay, string> = {
|
||||
0: '周一',
|
||||
1: '周二',
|
||||
2: '周三',
|
||||
3: '周四',
|
||||
4: '周五',
|
||||
5: '周六',
|
||||
6: '周日',
|
||||
};
|
||||
|
||||
const storePickupMap = new Map<string, StorePickupState>();
|
||||
|
||||
/** 解析 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-pickup] parseBody error:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/** 确保门店状态存在。 */
|
||||
function ensureStoreState(storeId = '') {
|
||||
const key = storeId || 'default';
|
||||
let state = storePickupMap.get(key);
|
||||
if (!state) {
|
||||
state = createDefaultState();
|
||||
storePickupMap.set(key, state);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function createDefaultState(): StorePickupState {
|
||||
const fineRule: PickupFineRuleMock = {
|
||||
intervalMinutes: 30,
|
||||
slotCapacity: 5,
|
||||
dayStartTime: '09:00',
|
||||
dayEndTime: '20:30',
|
||||
minAdvanceHours: 2,
|
||||
dayOfWeeks: [...ALL_WEEK_DAYS],
|
||||
};
|
||||
|
||||
return {
|
||||
mode: 'big',
|
||||
basicSettings: {
|
||||
allowSameDayPickup: true,
|
||||
bookingDays: 3,
|
||||
maxItemsPerOrder: 20,
|
||||
},
|
||||
bigSlots: sortSlots([
|
||||
{
|
||||
id: Random.guid(),
|
||||
name: '上午时段',
|
||||
startTime: '09:00',
|
||||
endTime: '11:30',
|
||||
cutoffMinutes: 30,
|
||||
capacity: 20,
|
||||
reservedCount: 5,
|
||||
dayOfWeeks: [...WEEKDAY_ONLY],
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: Random.guid(),
|
||||
name: '午间时段',
|
||||
startTime: '11:30',
|
||||
endTime: '14:00',
|
||||
cutoffMinutes: 20,
|
||||
capacity: 30,
|
||||
reservedCount: 12,
|
||||
dayOfWeeks: [...ALL_WEEK_DAYS],
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: Random.guid(),
|
||||
name: '下午时段',
|
||||
startTime: '14:00',
|
||||
endTime: '17:00',
|
||||
cutoffMinutes: 30,
|
||||
capacity: 15,
|
||||
reservedCount: 3,
|
||||
dayOfWeeks: [...WEEKDAY_ONLY],
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: Random.guid(),
|
||||
name: '晚间时段',
|
||||
startTime: '17:00',
|
||||
endTime: '20:30',
|
||||
cutoffMinutes: 30,
|
||||
capacity: 25,
|
||||
reservedCount: 8,
|
||||
dayOfWeeks: [...ALL_WEEK_DAYS],
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: Random.guid(),
|
||||
name: '周末特惠',
|
||||
startTime: '10:00',
|
||||
endTime: '15:00',
|
||||
cutoffMinutes: 45,
|
||||
capacity: 40,
|
||||
reservedCount: 18,
|
||||
dayOfWeeks: [...WEEKEND_ONLY],
|
||||
enabled: false,
|
||||
},
|
||||
]),
|
||||
fineRule,
|
||||
previewDays: generatePreviewDays(fineRule),
|
||||
};
|
||||
}
|
||||
|
||||
/** 深拷贝基础设置。 */
|
||||
function cloneBasicSettings(source: PickupBasicSettingsMock) {
|
||||
return { ...source };
|
||||
}
|
||||
|
||||
/** 深拷贝大时段列表。 */
|
||||
function cloneBigSlots(source: PickupSlotMock[]) {
|
||||
return source.map((item) => ({
|
||||
...item,
|
||||
dayOfWeeks: [...item.dayOfWeeks],
|
||||
}));
|
||||
}
|
||||
|
||||
/** 深拷贝精细规则。 */
|
||||
function cloneFineRule(source: PickupFineRuleMock) {
|
||||
return {
|
||||
...source,
|
||||
dayOfWeeks: [...source.dayOfWeeks],
|
||||
};
|
||||
}
|
||||
|
||||
/** 深拷贝预览日列表。 */
|
||||
function clonePreviewDays(source: PickupPreviewDayMock[]) {
|
||||
return source.map((day) => ({
|
||||
...day,
|
||||
slots: day.slots.map((slot) => ({ ...slot })),
|
||||
}));
|
||||
}
|
||||
|
||||
/** 深拷贝门店配置。 */
|
||||
function cloneStoreState(source: StorePickupState): StorePickupState {
|
||||
return {
|
||||
mode: source.mode,
|
||||
basicSettings: cloneBasicSettings(source.basicSettings),
|
||||
bigSlots: cloneBigSlots(source.bigSlots),
|
||||
fineRule: cloneFineRule(source.fineRule),
|
||||
previewDays: clonePreviewDays(source.previewDays),
|
||||
};
|
||||
}
|
||||
|
||||
/** 归一化基础设置提交数据。 */
|
||||
function normalizeBasicSettings(source: any): PickupBasicSettingsMock {
|
||||
return {
|
||||
allowSameDayPickup: Boolean(source?.allowSameDayPickup),
|
||||
bookingDays: clampInt(source?.bookingDays, 1, 30, 3),
|
||||
maxItemsPerOrder:
|
||||
source?.maxItemsPerOrder === null ||
|
||||
source?.maxItemsPerOrder === undefined
|
||||
? null
|
||||
: clampInt(source?.maxItemsPerOrder, 0, 999, 20),
|
||||
};
|
||||
}
|
||||
|
||||
/** 归一化精细规则提交数据。 */
|
||||
function normalizeFineRule(source: any): PickupFineRuleMock {
|
||||
return {
|
||||
intervalMinutes: clampInt(source?.intervalMinutes, 5, 180, 30),
|
||||
slotCapacity: clampInt(source?.slotCapacity, 1, 999, 5),
|
||||
dayStartTime: normalizeTime(source?.dayStartTime, '09:00'),
|
||||
dayEndTime: normalizeTime(source?.dayEndTime, '20:30'),
|
||||
minAdvanceHours: clampInt(source?.minAdvanceHours, 0, 72, 2),
|
||||
dayOfWeeks: normalizeDayOfWeeks(source?.dayOfWeeks, [...ALL_WEEK_DAYS]),
|
||||
};
|
||||
}
|
||||
|
||||
/** 归一化大时段提交数据。 */
|
||||
function normalizeSlots(source: any, previous: PickupSlotMock[]) {
|
||||
const reservedMap = new Map(
|
||||
previous.map((item) => [item.id, item.reservedCount]),
|
||||
);
|
||||
if (!Array.isArray(source) || source.length === 0) return [];
|
||||
|
||||
return sortSlots(
|
||||
source.map((item, index) => {
|
||||
const id = String(item?.id || Random.guid());
|
||||
const capacity = clampInt(item?.capacity, 0, 9999, 0);
|
||||
const existingReserved = reservedMap.get(id) ?? 0;
|
||||
const incomingReserved = clampInt(
|
||||
item?.reservedCount,
|
||||
0,
|
||||
capacity,
|
||||
existingReserved,
|
||||
);
|
||||
|
||||
return {
|
||||
id,
|
||||
name: String(item?.name || `时段${index + 1}`).trim(),
|
||||
startTime: normalizeTime(item?.startTime, '09:00'),
|
||||
endTime: normalizeTime(item?.endTime, '17:00'),
|
||||
cutoffMinutes: clampInt(item?.cutoffMinutes, 0, 720, 30),
|
||||
capacity,
|
||||
reservedCount: Math.min(capacity, incomingReserved),
|
||||
dayOfWeeks: normalizeDayOfWeeks(item?.dayOfWeeks, [...WEEKDAY_ONLY]),
|
||||
enabled: Boolean(item?.enabled),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** 归一化模式字段。 */
|
||||
function normalizeMode(mode: unknown, fallback: PickupMode): PickupMode {
|
||||
return mode === 'fine' || mode === 'big' ? mode : fallback;
|
||||
}
|
||||
|
||||
/** 排序大时段。 */
|
||||
function sortSlots(source: PickupSlotMock[]) {
|
||||
return source.toSorted((a, b) => {
|
||||
const diff =
|
||||
parseTimeToMinutes(a.startTime) - parseTimeToMinutes(b.startTime);
|
||||
if (diff !== 0) return diff;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
|
||||
/** 归一化 HH:mm 时间。 */
|
||||
function normalizeTime(time: unknown, fallback: string) {
|
||||
const value = typeof time === 'string' ? time : '';
|
||||
const matched = /(\d{2}):(\d{2})/.exec(value);
|
||||
if (!matched) return fallback;
|
||||
|
||||
const hour = Number(matched[1]);
|
||||
const minute = Number(matched[2]);
|
||||
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) return fallback;
|
||||
|
||||
return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/** 归一化星期数组。 */
|
||||
function normalizeDayOfWeeks(source: unknown, fallback: PickupWeekDay[]) {
|
||||
if (!Array.isArray(source)) return fallback;
|
||||
const values = source
|
||||
.map(Number)
|
||||
.filter((item) => Number.isInteger(item) && item >= 0 && item <= 6)
|
||||
.map((item) => item as PickupWeekDay);
|
||||
const unique = [...new Set(values)].toSorted((a, b) => a - b);
|
||||
return unique.length > 0 ? unique : fallback;
|
||||
}
|
||||
|
||||
/** 数值裁剪为整数区间。 */
|
||||
function clampInt(value: unknown, min: number, max: number, fallback: number) {
|
||||
const numberValue = Number(value);
|
||||
if (!Number.isFinite(numberValue)) return fallback;
|
||||
const normalized = Math.floor(numberValue);
|
||||
return Math.max(min, Math.min(max, normalized));
|
||||
}
|
||||
|
||||
/** HH:mm 转分钟。 */
|
||||
function parseTimeToMinutes(time: string) {
|
||||
const matched = /^(\d{2}):(\d{2})$/.exec(time);
|
||||
if (!matched) return Number.NaN;
|
||||
return Number(matched[1]) * 60 + Number(matched[2]);
|
||||
}
|
||||
|
||||
/** 生成三天预览数据。 */
|
||||
function generatePreviewDays(
|
||||
fineRule: PickupFineRuleMock,
|
||||
baseDate = new Date(),
|
||||
) {
|
||||
const startMinutes = parseTimeToMinutes(fineRule.dayStartTime);
|
||||
const endMinutes = parseTimeToMinutes(fineRule.dayEndTime);
|
||||
if (!Number.isFinite(startMinutes) || !Number.isFinite(endMinutes)) return [];
|
||||
if (endMinutes <= startMinutes || fineRule.intervalMinutes <= 0) return [];
|
||||
|
||||
return Array.from({ length: 3 }).map((_, index) => {
|
||||
const date = addDays(baseDate, index);
|
||||
const dateKey = toDateOnly(date);
|
||||
const dayOfWeek = toPickupWeekDay(date);
|
||||
const slots = fineRule.dayOfWeeks.includes(dayOfWeek)
|
||||
? generateDaySlots({
|
||||
date,
|
||||
dateKey,
|
||||
fineRule,
|
||||
})
|
||||
: [];
|
||||
|
||||
return {
|
||||
date: dateKey,
|
||||
label: `${date.getMonth() + 1}/${date.getDate()}`,
|
||||
subLabel: resolvePreviewSubLabel(index, dayOfWeek),
|
||||
slots,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** 生成某天时段预览。 */
|
||||
function generateDaySlots(payload: {
|
||||
date: Date;
|
||||
dateKey: string;
|
||||
fineRule: PickupFineRuleMock;
|
||||
}) {
|
||||
const startMinutes = parseTimeToMinutes(payload.fineRule.dayStartTime);
|
||||
const endMinutes = parseTimeToMinutes(payload.fineRule.dayEndTime);
|
||||
const interval = payload.fineRule.intervalMinutes;
|
||||
const total = Math.floor((endMinutes - startMinutes) / interval);
|
||||
|
||||
return Array.from({ length: total + 1 }).map((_, index) => {
|
||||
const minutes = startMinutes + index * interval;
|
||||
const time = `${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(
|
||||
minutes % 60,
|
||||
).padStart(2, '0')}`;
|
||||
const booked = calcMockBookedCount(
|
||||
`${payload.dateKey}|${time}`,
|
||||
payload.fineRule.slotCapacity,
|
||||
);
|
||||
const remainingCount = Math.max(0, payload.fineRule.slotCapacity - booked);
|
||||
|
||||
return {
|
||||
time,
|
||||
remainingCount,
|
||||
status: resolvePreviewStatus({
|
||||
date: payload.date,
|
||||
fineRule: payload.fineRule,
|
||||
remainingCount,
|
||||
time,
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** 计算时段预览状态。 */
|
||||
function resolvePreviewStatus(payload: {
|
||||
date: Date;
|
||||
fineRule: PickupFineRuleMock;
|
||||
remainingCount: number;
|
||||
time: string;
|
||||
}): PickupPreviewStatus {
|
||||
const now = new Date();
|
||||
const today = toDateOnly(now);
|
||||
const dateKey = toDateOnly(payload.date);
|
||||
const slotMinutes = parseTimeToMinutes(payload.time);
|
||||
const nowMinutes = now.getHours() * 60 + now.getMinutes();
|
||||
const minAdvanceMinutes = payload.fineRule.minAdvanceHours * 60;
|
||||
|
||||
if (dateKey < today) return 'expired';
|
||||
if (dateKey === today && slotMinutes - nowMinutes <= minAdvanceMinutes) {
|
||||
return 'expired';
|
||||
}
|
||||
if (payload.remainingCount <= 0) return 'full';
|
||||
if (payload.remainingCount <= 1) return 'almost';
|
||||
return 'available';
|
||||
}
|
||||
|
||||
/** 计算稳定的伪随机已预约量。 */
|
||||
function calcMockBookedCount(seed: string, capacity: number) {
|
||||
if (capacity <= 0) return 0;
|
||||
let hash = 0;
|
||||
for (const char of seed) {
|
||||
hash = (hash * 31 + (char.codePointAt(0) ?? 0)) >>> 0;
|
||||
}
|
||||
if (hash % 7 === 0) return capacity;
|
||||
if (hash % 5 === 0) return Math.max(0, capacity - 1);
|
||||
return hash % (capacity + 1);
|
||||
}
|
||||
|
||||
function toDateOnly(date: Date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function addDays(baseDate: Date, days: number) {
|
||||
const next = new Date(baseDate);
|
||||
next.setDate(baseDate.getDate() + days);
|
||||
return next;
|
||||
}
|
||||
|
||||
function toPickupWeekDay(date: Date): PickupWeekDay {
|
||||
const mapping: PickupWeekDay[] = [6, 0, 1, 2, 3, 4, 5];
|
||||
return mapping[date.getDay()] ?? 0;
|
||||
}
|
||||
|
||||
function resolvePreviewSubLabel(offset: number, dayOfWeek: PickupWeekDay) {
|
||||
const dayText = WEEKDAY_LABEL_MAP[dayOfWeek];
|
||||
if (offset === 0) return `${dayText} 今天`;
|
||||
if (offset === 1) return `${dayText} 明天`;
|
||||
if (offset === 2) return `${dayText} 后天`;
|
||||
return dayText;
|
||||
}
|
||||
|
||||
// 获取门店自提设置
|
||||
Mock.mock(/\/store\/pickup(?:\?|$)/, 'get', (options: MockRequestOptions) => {
|
||||
const params = parseUrlParams(options.url);
|
||||
const storeId = String(params.storeId || '');
|
||||
const state = ensureStoreState(storeId);
|
||||
state.previewDays = generatePreviewDays(state.fineRule);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
storeId,
|
||||
mode: state.mode,
|
||||
basicSettings: cloneBasicSettings(state.basicSettings),
|
||||
bigSlots: cloneBigSlots(state.bigSlots),
|
||||
fineRule: cloneFineRule(state.fineRule),
|
||||
previewDays: clonePreviewDays(state.previewDays),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// 保存自提基础设置
|
||||
Mock.mock(
|
||||
/\/store\/pickup\/basic\/save/,
|
||||
'post',
|
||||
(options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const storeId = String(body.storeId || '');
|
||||
if (!storeId) return { code: 200, data: null };
|
||||
|
||||
const state = ensureStoreState(storeId);
|
||||
state.basicSettings = normalizeBasicSettings(body.basicSettings);
|
||||
state.mode = normalizeMode(body.mode, state.mode);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: null,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// 保存自提大时段
|
||||
Mock.mock(
|
||||
/\/store\/pickup\/slots\/save/,
|
||||
'post',
|
||||
(options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const storeId = String(body.storeId || '');
|
||||
if (!storeId) return { code: 200, data: null };
|
||||
|
||||
const state = ensureStoreState(storeId);
|
||||
state.bigSlots = normalizeSlots(body.slots, state.bigSlots);
|
||||
state.mode = normalizeMode(body.mode, state.mode);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: null,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// 保存自提精细规则
|
||||
Mock.mock(
|
||||
/\/store\/pickup\/fine-rule\/save/,
|
||||
'post',
|
||||
(options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const storeId = String(body.storeId || '');
|
||||
if (!storeId) return { code: 200, data: null };
|
||||
|
||||
const state = ensureStoreState(storeId);
|
||||
state.fineRule = normalizeFineRule(body.fineRule);
|
||||
state.mode = normalizeMode(body.mode, state.mode);
|
||||
state.previewDays = generatePreviewDays(state.fineRule);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: null,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// 复制门店自提设置
|
||||
Mock.mock(/\/store\/pickup\/copy/, 'post', (options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const sourceStoreId = String(body.sourceStoreId || '');
|
||||
const targetStoreIds: string[] = Array.isArray(body.targetStoreIds)
|
||||
? body.targetStoreIds.map(String).filter(Boolean)
|
||||
: [];
|
||||
|
||||
if (!sourceStoreId || targetStoreIds.length === 0) {
|
||||
return {
|
||||
code: 200,
|
||||
data: { copiedCount: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const sourceState = ensureStoreState(sourceStoreId);
|
||||
const uniqueTargets = [...new Set(targetStoreIds)].filter(
|
||||
(storeId) => storeId !== sourceStoreId,
|
||||
);
|
||||
|
||||
for (const targetStoreId of uniqueTargets) {
|
||||
storePickupMap.set(targetStoreId, cloneStoreState(sourceState));
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
copiedCount: uniqueTargets.length,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -1,919 +0,0 @@
|
||||
import Mock from 'mockjs';
|
||||
|
||||
/** 文件职责:员工排班页面 Mock 接口。 */
|
||||
interface MockRequestOptions {
|
||||
body: null | string;
|
||||
type: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
type StaffRoleType = 'cashier' | 'chef' | 'courier' | 'manager';
|
||||
type StaffStatus = 'active' | 'leave' | 'resigned';
|
||||
type ShiftType = 'evening' | 'full' | 'morning' | 'off';
|
||||
|
||||
interface StoreStaffMock {
|
||||
avatarColor: string;
|
||||
email: string;
|
||||
hiredAt: string;
|
||||
id: string;
|
||||
name: string;
|
||||
permissions: string[];
|
||||
phone: string;
|
||||
roleType: StaffRoleType;
|
||||
status: StaffStatus;
|
||||
}
|
||||
|
||||
interface ShiftTemplateItemMock {
|
||||
endTime: string;
|
||||
startTime: string;
|
||||
}
|
||||
|
||||
interface StoreShiftTemplatesMock {
|
||||
evening: ShiftTemplateItemMock;
|
||||
full: ShiftTemplateItemMock;
|
||||
morning: ShiftTemplateItemMock;
|
||||
}
|
||||
|
||||
interface StaffDayShiftMock {
|
||||
dayOfWeek: number;
|
||||
endTime: string;
|
||||
shiftType: ShiftType;
|
||||
startTime: string;
|
||||
}
|
||||
|
||||
interface StaffScheduleMock {
|
||||
shifts: StaffDayShiftMock[];
|
||||
staffId: string;
|
||||
}
|
||||
|
||||
interface StoreStaffState {
|
||||
schedules: StaffScheduleMock[];
|
||||
staffs: StoreStaffMock[];
|
||||
templates: StoreShiftTemplatesMock;
|
||||
weekStartDate: string;
|
||||
}
|
||||
|
||||
const ROLE_VALUES = new Set<StaffRoleType>([
|
||||
'cashier',
|
||||
'chef',
|
||||
'courier',
|
||||
'manager',
|
||||
]);
|
||||
const STATUS_VALUES = new Set<StaffStatus>(['active', 'leave', 'resigned']);
|
||||
const SHIFT_VALUES = new Set<ShiftType>(['evening', 'full', 'morning', 'off']);
|
||||
|
||||
const AVATAR_COLORS = [
|
||||
'#f56a00',
|
||||
'#7265e6',
|
||||
'#52c41a',
|
||||
'#fa8c16',
|
||||
'#1890ff',
|
||||
'#bfbfbf',
|
||||
'#13c2c2',
|
||||
'#eb2f96',
|
||||
];
|
||||
|
||||
const DEFAULT_TEMPLATES: StoreShiftTemplatesMock = {
|
||||
morning: {
|
||||
startTime: '09:00',
|
||||
endTime: '14:00',
|
||||
},
|
||||
evening: {
|
||||
startTime: '14:00',
|
||||
endTime: '21:00',
|
||||
},
|
||||
full: {
|
||||
startTime: '09:00',
|
||||
endTime: '21:00',
|
||||
},
|
||||
};
|
||||
|
||||
const DEFAULT_STAFFS: StoreStaffMock[] = [
|
||||
{
|
||||
id: 'staff-001',
|
||||
name: '张伟',
|
||||
phone: '13800008001',
|
||||
email: 'zhangwei@example.com',
|
||||
roleType: 'manager',
|
||||
status: 'active',
|
||||
permissions: ['全部权限'],
|
||||
hiredAt: '2024-01-15',
|
||||
avatarColor: '#f56a00',
|
||||
},
|
||||
{
|
||||
id: 'staff-002',
|
||||
name: '李娜',
|
||||
phone: '13800008002',
|
||||
email: 'lina@example.com',
|
||||
roleType: 'cashier',
|
||||
status: 'active',
|
||||
permissions: ['收银', '退款'],
|
||||
hiredAt: '2024-03-20',
|
||||
avatarColor: '#7265e6',
|
||||
},
|
||||
{
|
||||
id: 'staff-003',
|
||||
name: '王磊',
|
||||
phone: '13800008003',
|
||||
email: '',
|
||||
roleType: 'courier',
|
||||
status: 'active',
|
||||
permissions: ['配送管理'],
|
||||
hiredAt: '2024-06-01',
|
||||
avatarColor: '#52c41a',
|
||||
},
|
||||
{
|
||||
id: 'staff-004',
|
||||
name: '赵敏',
|
||||
phone: '13800008004',
|
||||
email: '',
|
||||
roleType: 'chef',
|
||||
status: 'active',
|
||||
permissions: ['订单查看'],
|
||||
hiredAt: '2024-08-10',
|
||||
avatarColor: '#fa8c16',
|
||||
},
|
||||
{
|
||||
id: 'staff-005',
|
||||
name: '刘洋',
|
||||
phone: '13800008005',
|
||||
email: 'liuyang@example.com',
|
||||
roleType: 'courier',
|
||||
status: 'leave',
|
||||
permissions: ['配送管理'],
|
||||
hiredAt: '2025-01-05',
|
||||
avatarColor: '#1890ff',
|
||||
},
|
||||
{
|
||||
id: 'staff-006',
|
||||
name: '陈静',
|
||||
phone: '13800008006',
|
||||
email: '',
|
||||
roleType: 'cashier',
|
||||
status: 'resigned',
|
||||
permissions: [],
|
||||
hiredAt: '2024-11-20',
|
||||
avatarColor: '#bfbfbf',
|
||||
},
|
||||
];
|
||||
|
||||
const storeStaffMap = new Map<string, StoreStaffState>();
|
||||
|
||||
/** 解析 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-store-staff] parseBody error:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取当前周一日期。 */
|
||||
function getCurrentWeekStartDate(baseDate = new Date()) {
|
||||
const date = new Date(baseDate);
|
||||
const weekDay = date.getDay();
|
||||
const diff = weekDay === 0 ? -6 : 1 - weekDay;
|
||||
date.setDate(date.getDate() + diff);
|
||||
return toDateOnly(date);
|
||||
}
|
||||
|
||||
/** 日期转 yyyy-MM-dd。 */
|
||||
function toDateOnly(date: Date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
/** 归一化 HH:mm 时间。 */
|
||||
function normalizeTime(value: unknown, fallback: string) {
|
||||
const input = typeof value === 'string' ? value : '';
|
||||
const matched = /^(\d{2}):(\d{2})$/.exec(input);
|
||||
if (!matched) return fallback;
|
||||
const hour = Number(matched[1]);
|
||||
const minute = Number(matched[2]);
|
||||
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
||||
return fallback;
|
||||
}
|
||||
return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/** 归一化角色。 */
|
||||
function normalizeRoleType(value: unknown, fallback: StaffRoleType) {
|
||||
return ROLE_VALUES.has(value as StaffRoleType)
|
||||
? (value as StaffRoleType)
|
||||
: fallback;
|
||||
}
|
||||
|
||||
/** 归一化状态。 */
|
||||
function normalizeStatus(value: unknown, fallback: StaffStatus) {
|
||||
return STATUS_VALUES.has(value as StaffStatus)
|
||||
? (value as StaffStatus)
|
||||
: fallback;
|
||||
}
|
||||
|
||||
/** 归一化班次类型。 */
|
||||
function normalizeShiftType(value: unknown, fallback: ShiftType) {
|
||||
return SHIFT_VALUES.has(value as ShiftType) ? (value as ShiftType) : fallback;
|
||||
}
|
||||
|
||||
/** 深拷贝员工列表。 */
|
||||
function cloneStaffs(source: StoreStaffMock[]) {
|
||||
return source.map((item) => ({
|
||||
...item,
|
||||
permissions: [...item.permissions],
|
||||
}));
|
||||
}
|
||||
|
||||
/** 深拷贝模板。 */
|
||||
function cloneTemplates(
|
||||
source: StoreShiftTemplatesMock,
|
||||
): StoreShiftTemplatesMock {
|
||||
return {
|
||||
morning: { ...source.morning },
|
||||
evening: { ...source.evening },
|
||||
full: { ...source.full },
|
||||
};
|
||||
}
|
||||
|
||||
/** 深拷贝排班。 */
|
||||
function cloneSchedules(source: StaffScheduleMock[]) {
|
||||
return source.map((item) => ({
|
||||
staffId: item.staffId,
|
||||
shifts: item.shifts.map((shift) => ({ ...shift })),
|
||||
}));
|
||||
}
|
||||
|
||||
/** 按入职时间稳定排序员工。 */
|
||||
function sortStaffs(source: StoreStaffMock[]) {
|
||||
return cloneStaffs(source).toSorted((a, b) => {
|
||||
const dateDiff = a.hiredAt.localeCompare(b.hiredAt);
|
||||
if (dateDiff !== 0) return dateDiff;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
|
||||
/** 通过班次类型生成单日排班。 */
|
||||
function createDayShift(
|
||||
dayOfWeek: number,
|
||||
shiftType: ShiftType,
|
||||
templates: StoreShiftTemplatesMock,
|
||||
): StaffDayShiftMock {
|
||||
if (shiftType === 'off') {
|
||||
return {
|
||||
dayOfWeek,
|
||||
shiftType,
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
};
|
||||
}
|
||||
|
||||
const template = templates[shiftType];
|
||||
return {
|
||||
dayOfWeek,
|
||||
shiftType,
|
||||
startTime: template.startTime,
|
||||
endTime: template.endTime,
|
||||
};
|
||||
}
|
||||
|
||||
/** 生成默认 7 天排班。 */
|
||||
function createDefaultWeekByRole(
|
||||
roleType: StaffRoleType,
|
||||
templates: StoreShiftTemplatesMock,
|
||||
): StaffDayShiftMock[] {
|
||||
const rolePatternMap: Record<StaffRoleType, ShiftType[]> = {
|
||||
manager: ['full', 'full', 'full', 'full', 'full', 'morning', 'off'],
|
||||
cashier: [
|
||||
'morning',
|
||||
'morning',
|
||||
'off',
|
||||
'morning',
|
||||
'evening',
|
||||
'full',
|
||||
'full',
|
||||
],
|
||||
courier: [
|
||||
'morning',
|
||||
'evening',
|
||||
'morning',
|
||||
'evening',
|
||||
'morning',
|
||||
'evening',
|
||||
'off',
|
||||
],
|
||||
chef: ['full', 'full', 'evening', 'off', 'full', 'full', 'morning'],
|
||||
};
|
||||
|
||||
const pattern = rolePatternMap[roleType] ?? rolePatternMap.cashier;
|
||||
return Array.from({ length: 7 }).map((_, dayOfWeek) =>
|
||||
createDayShift(dayOfWeek, pattern[dayOfWeek] ?? 'off', templates),
|
||||
);
|
||||
}
|
||||
|
||||
/** 创建默认门店状态。 */
|
||||
function createDefaultState(): StoreStaffState {
|
||||
const templates = cloneTemplates(DEFAULT_TEMPLATES);
|
||||
const staffs = sortStaffs(cloneStaffs(DEFAULT_STAFFS));
|
||||
const schedules = staffs.map((staff) => ({
|
||||
staffId: staff.id,
|
||||
shifts:
|
||||
staff.status === 'resigned'
|
||||
? Array.from({ length: 7 }).map((_, dayOfWeek) =>
|
||||
createDayShift(dayOfWeek, 'off', templates),
|
||||
)
|
||||
: createDefaultWeekByRole(staff.roleType, templates),
|
||||
}));
|
||||
|
||||
return {
|
||||
staffs,
|
||||
templates,
|
||||
schedules,
|
||||
weekStartDate: getCurrentWeekStartDate(),
|
||||
};
|
||||
}
|
||||
|
||||
/** 确保门店状态存在。 */
|
||||
function ensureStoreState(storeId = '') {
|
||||
const key = storeId || 'default';
|
||||
let state = storeStaffMap.get(key);
|
||||
if (!state) {
|
||||
state = createDefaultState();
|
||||
storeStaffMap.set(key, state);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
/** 构建员工排班索引。 */
|
||||
function createScheduleMap(schedules: StaffScheduleMock[]) {
|
||||
const scheduleMap = new Map<string, StaffDayShiftMock[]>();
|
||||
for (const schedule of schedules) {
|
||||
scheduleMap.set(
|
||||
schedule.staffId,
|
||||
schedule.shifts.map((shift) => ({ ...shift })),
|
||||
);
|
||||
}
|
||||
return scheduleMap;
|
||||
}
|
||||
|
||||
/** 同步模板后刷新已有排班的时间段。 */
|
||||
function syncScheduleTimesWithTemplates(
|
||||
schedules: StaffScheduleMock[],
|
||||
templates: StoreShiftTemplatesMock,
|
||||
) {
|
||||
for (const schedule of schedules) {
|
||||
schedule.shifts = schedule.shifts
|
||||
.map((shift) => {
|
||||
const normalizedType = normalizeShiftType(shift.shiftType, 'off');
|
||||
if (normalizedType === 'off') {
|
||||
return {
|
||||
dayOfWeek: shift.dayOfWeek,
|
||||
shiftType: 'off' as const,
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
};
|
||||
}
|
||||
return {
|
||||
dayOfWeek: shift.dayOfWeek,
|
||||
shiftType: normalizedType,
|
||||
startTime: templates[normalizedType].startTime,
|
||||
endTime: templates[normalizedType].endTime,
|
||||
};
|
||||
})
|
||||
.toSorted((a, b) => a.dayOfWeek - b.dayOfWeek);
|
||||
}
|
||||
}
|
||||
|
||||
/** 归一化模板。 */
|
||||
function normalizeTemplates(
|
||||
input: unknown,
|
||||
fallback: StoreShiftTemplatesMock,
|
||||
): StoreShiftTemplatesMock {
|
||||
const record = typeof input === 'object' && input ? input : {};
|
||||
const morning =
|
||||
typeof (record as { morning?: unknown }).morning === 'object' &&
|
||||
(record as { morning?: unknown }).morning
|
||||
? ((record as { morning: Record<string, unknown> }).morning as Record<
|
||||
string,
|
||||
unknown
|
||||
>)
|
||||
: {};
|
||||
const evening =
|
||||
typeof (record as { evening?: unknown }).evening === 'object' &&
|
||||
(record as { evening?: unknown }).evening
|
||||
? ((record as { evening: Record<string, unknown> }).evening as Record<
|
||||
string,
|
||||
unknown
|
||||
>)
|
||||
: {};
|
||||
const full =
|
||||
typeof (record as { full?: unknown }).full === 'object' &&
|
||||
(record as { full?: unknown }).full
|
||||
? ((record as { full: Record<string, unknown> }).full as Record<
|
||||
string,
|
||||
unknown
|
||||
>)
|
||||
: {};
|
||||
|
||||
return {
|
||||
morning: {
|
||||
startTime: normalizeTime(morning.startTime, fallback.morning.startTime),
|
||||
endTime: normalizeTime(morning.endTime, fallback.morning.endTime),
|
||||
},
|
||||
evening: {
|
||||
startTime: normalizeTime(evening.startTime, fallback.evening.startTime),
|
||||
endTime: normalizeTime(evening.endTime, fallback.evening.endTime),
|
||||
},
|
||||
full: {
|
||||
startTime: normalizeTime(full.startTime, fallback.full.startTime),
|
||||
endTime: normalizeTime(full.endTime, fallback.full.endTime),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 归一化单员工 7 天排班。 */
|
||||
function normalizeShifts(
|
||||
input: unknown,
|
||||
templates: StoreShiftTemplatesMock,
|
||||
fallback: StaffDayShiftMock[],
|
||||
): StaffDayShiftMock[] {
|
||||
const byDay = new Map<number, StaffDayShiftMock>();
|
||||
|
||||
if (Array.isArray(input)) {
|
||||
for (const rawShift of input) {
|
||||
const shiftRecord =
|
||||
typeof rawShift === 'object' && rawShift
|
||||
? (rawShift as Record<string, unknown>)
|
||||
: {};
|
||||
const dayOfWeek = Number(shiftRecord.dayOfWeek);
|
||||
if (!Number.isInteger(dayOfWeek) || dayOfWeek < 0 || dayOfWeek > 6) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const shiftType = normalizeShiftType(shiftRecord.shiftType, 'off');
|
||||
if (shiftType === 'off') {
|
||||
byDay.set(dayOfWeek, {
|
||||
dayOfWeek,
|
||||
shiftType,
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
byDay.set(dayOfWeek, {
|
||||
dayOfWeek,
|
||||
shiftType,
|
||||
startTime: normalizeTime(
|
||||
shiftRecord.startTime,
|
||||
templates[shiftType].startTime,
|
||||
),
|
||||
endTime: normalizeTime(
|
||||
shiftRecord.endTime,
|
||||
templates[shiftType].endTime,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from({ length: 7 }).map((_, dayOfWeek) => {
|
||||
const normalized = byDay.get(dayOfWeek);
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const fallbackShift = fallback.find((item) => item.dayOfWeek === dayOfWeek);
|
||||
if (fallbackShift) {
|
||||
return { ...fallbackShift };
|
||||
}
|
||||
|
||||
return createDayShift(dayOfWeek, 'off', templates);
|
||||
});
|
||||
}
|
||||
|
||||
/** 规范手机号格式。 */
|
||||
function normalizePhone(value: unknown) {
|
||||
return String(value || '')
|
||||
.replaceAll(/\D/g, '')
|
||||
.slice(0, 11);
|
||||
}
|
||||
|
||||
/** 规范权限列表。 */
|
||||
function normalizePermissions(value: unknown, roleType: StaffRoleType) {
|
||||
if (!Array.isArray(value)) {
|
||||
return roleType === 'manager' ? ['全部权限'] : [];
|
||||
}
|
||||
const unique = [
|
||||
...new Set(value.map((item) => String(item || '').trim()).filter(Boolean)),
|
||||
];
|
||||
if (roleType === 'manager' && unique.length === 0) {
|
||||
return ['全部权限'];
|
||||
}
|
||||
return unique;
|
||||
}
|
||||
|
||||
/** 生成员工 ID。 */
|
||||
function createStaffId() {
|
||||
return `staff-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
||||
}
|
||||
|
||||
/** 生成头像颜色。 */
|
||||
function resolveAvatarColor(seed: string) {
|
||||
let hash = 0;
|
||||
for (const char of seed) {
|
||||
hash = (hash * 31 + (char.codePointAt(0) ?? 0)) >>> 0;
|
||||
}
|
||||
return AVATAR_COLORS[hash % AVATAR_COLORS.length] ?? '#1677ff';
|
||||
}
|
||||
|
||||
/** 获取员工列表。 */
|
||||
Mock.mock(/\/store\/staff(?:\?|$)/, 'get', (options: MockRequestOptions) => {
|
||||
const params = parseUrlParams(options.url);
|
||||
const storeId = String(params.storeId || '');
|
||||
const state = ensureStoreState(storeId);
|
||||
|
||||
const keyword = String(params.keyword || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const roleType = params.roleType
|
||||
? normalizeRoleType(params.roleType, 'cashier')
|
||||
: '';
|
||||
const status = params.status ? normalizeStatus(params.status, 'active') : '';
|
||||
|
||||
const page = Math.max(1, Number(params.page) || 1);
|
||||
const pageSize = Math.max(1, Math.min(200, Number(params.pageSize) || 10));
|
||||
|
||||
const filtered = state.staffs.filter((staff) => {
|
||||
if (keyword) {
|
||||
const hitKeyword =
|
||||
staff.name.toLowerCase().includes(keyword) ||
|
||||
staff.phone.includes(keyword) ||
|
||||
staff.email.toLowerCase().includes(keyword);
|
||||
if (!hitKeyword) return false;
|
||||
}
|
||||
|
||||
if (roleType && staff.roleType !== roleType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (status && staff.status !== status) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const start = (page - 1) * pageSize;
|
||||
const items = filtered.slice(start, start + pageSize);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
items: cloneStaffs(items),
|
||||
total: filtered.length,
|
||||
page,
|
||||
pageSize,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
/** 新增 / 编辑员工。 */
|
||||
Mock.mock(/\/store\/staff\/save/, 'post', (options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const storeId = String(body.storeId || '');
|
||||
if (!storeId) {
|
||||
return { code: 200, data: null };
|
||||
}
|
||||
|
||||
const state = ensureStoreState(storeId);
|
||||
const id = String(body.id || '').trim();
|
||||
const existingIndex = state.staffs.findIndex((item) => item.id === id);
|
||||
const roleType = normalizeRoleType(body.roleType, 'cashier');
|
||||
const status = normalizeStatus(body.status, 'active');
|
||||
|
||||
const nextStaff: StoreStaffMock = {
|
||||
id: id || createStaffId(),
|
||||
name: String(body.name || '').trim(),
|
||||
phone: normalizePhone(body.phone),
|
||||
email: String(body.email || '').trim(),
|
||||
roleType,
|
||||
status,
|
||||
permissions: normalizePermissions(body.permissions, roleType),
|
||||
hiredAt:
|
||||
existingIndex === -1
|
||||
? toDateOnly(new Date())
|
||||
: state.staffs[existingIndex]?.hiredAt || toDateOnly(new Date()),
|
||||
avatarColor:
|
||||
existingIndex === -1
|
||||
? resolveAvatarColor(String(body.name || Date.now()))
|
||||
: state.staffs[existingIndex]?.avatarColor || '#1677ff',
|
||||
};
|
||||
|
||||
if (!nextStaff.name) {
|
||||
return {
|
||||
code: 400,
|
||||
data: null,
|
||||
message: '员工姓名不能为空',
|
||||
};
|
||||
}
|
||||
|
||||
if (!nextStaff.phone || nextStaff.phone.length < 6) {
|
||||
return {
|
||||
code: 400,
|
||||
data: null,
|
||||
message: '手机号格式不正确',
|
||||
};
|
||||
}
|
||||
|
||||
if (existingIndex === -1) {
|
||||
state.staffs.push(nextStaff);
|
||||
state.staffs = sortStaffs(state.staffs);
|
||||
state.schedules.push({
|
||||
staffId: nextStaff.id,
|
||||
shifts:
|
||||
status === 'resigned'
|
||||
? Array.from({ length: 7 }).map((_, dayOfWeek) =>
|
||||
createDayShift(dayOfWeek, 'off', state.templates),
|
||||
)
|
||||
: createDefaultWeekByRole(roleType, state.templates),
|
||||
});
|
||||
} else {
|
||||
state.staffs[existingIndex] = nextStaff;
|
||||
state.staffs = sortStaffs(state.staffs);
|
||||
|
||||
const schedule = state.schedules.find(
|
||||
(item) => item.staffId === nextStaff.id,
|
||||
);
|
||||
if (schedule && nextStaff.status === 'resigned') {
|
||||
schedule.shifts = Array.from({ length: 7 }).map((_, dayOfWeek) =>
|
||||
createDayShift(dayOfWeek, 'off', state.templates),
|
||||
);
|
||||
}
|
||||
if (!schedule) {
|
||||
state.schedules.push({
|
||||
staffId: nextStaff.id,
|
||||
shifts: createDefaultWeekByRole(nextStaff.roleType, state.templates),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
...nextStaff,
|
||||
permissions: [...nextStaff.permissions],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
/** 删除员工。 */
|
||||
Mock.mock(/\/store\/staff\/delete/, 'post', (options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const storeId = String(body.storeId || '');
|
||||
const staffId = String(body.staffId || '');
|
||||
if (!storeId || !staffId) {
|
||||
return { code: 200, data: null };
|
||||
}
|
||||
|
||||
const state = ensureStoreState(storeId);
|
||||
state.staffs = state.staffs.filter((item) => item.id !== staffId);
|
||||
state.schedules = state.schedules.filter((item) => item.staffId !== staffId);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: null,
|
||||
};
|
||||
});
|
||||
|
||||
/** 获取门店排班配置。 */
|
||||
Mock.mock(
|
||||
/\/store\/staff\/schedule(?:\?|$)/,
|
||||
'get',
|
||||
(options: MockRequestOptions) => {
|
||||
const params = parseUrlParams(options.url);
|
||||
const storeId = String(params.storeId || '');
|
||||
const state = ensureStoreState(storeId);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
storeId,
|
||||
templates: cloneTemplates(state.templates),
|
||||
schedules: cloneSchedules(state.schedules),
|
||||
weekStartDate: String(params.weekStartDate || state.weekStartDate),
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
/** 保存班次模板。 */
|
||||
Mock.mock(
|
||||
/\/store\/staff\/template\/save/,
|
||||
'post',
|
||||
(options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const storeId = String(body.storeId || '');
|
||||
if (!storeId) {
|
||||
return { code: 200, data: cloneTemplates(DEFAULT_TEMPLATES) };
|
||||
}
|
||||
|
||||
const state = ensureStoreState(storeId);
|
||||
state.templates = normalizeTemplates(body.templates, state.templates);
|
||||
syncScheduleTimesWithTemplates(state.schedules, state.templates);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: cloneTemplates(state.templates),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
/** 保存员工个人排班。 */
|
||||
Mock.mock(
|
||||
/\/store\/staff\/schedule\/personal\/save/,
|
||||
'post',
|
||||
(options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const storeId = String(body.storeId || '');
|
||||
const staffId = String(body.staffId || '');
|
||||
if (!storeId || !staffId) {
|
||||
return { code: 200, data: null };
|
||||
}
|
||||
|
||||
const state = ensureStoreState(storeId);
|
||||
const staff = state.staffs.find((item) => item.id === staffId);
|
||||
if (!staff) {
|
||||
return {
|
||||
code: 400,
|
||||
data: null,
|
||||
message: '员工不存在',
|
||||
};
|
||||
}
|
||||
|
||||
const schedule = state.schedules.find((item) => item.staffId === staffId);
|
||||
const fallbackShifts =
|
||||
schedule?.shifts ??
|
||||
createDefaultWeekByRole(staff.roleType, state.templates);
|
||||
|
||||
const nextShifts =
|
||||
staff.status === 'resigned'
|
||||
? Array.from({ length: 7 }).map((_, dayOfWeek) =>
|
||||
createDayShift(dayOfWeek, 'off', state.templates),
|
||||
)
|
||||
: normalizeShifts(body.shifts, state.templates, fallbackShifts);
|
||||
|
||||
if (schedule) {
|
||||
schedule.shifts = nextShifts;
|
||||
} else {
|
||||
state.schedules.push({
|
||||
staffId,
|
||||
shifts: nextShifts,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
staffId,
|
||||
shifts: nextShifts.map((item) => ({ ...item })),
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
/** 保存周排班。 */
|
||||
Mock.mock(
|
||||
/\/store\/staff\/schedule\/weekly\/save/,
|
||||
'post',
|
||||
(options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const storeId = String(body.storeId || '');
|
||||
if (!storeId) {
|
||||
return { code: 200, data: null };
|
||||
}
|
||||
|
||||
const state = ensureStoreState(storeId);
|
||||
const incomingList = Array.isArray(body.schedules) ? body.schedules : [];
|
||||
const scheduleMap = createScheduleMap(state.schedules);
|
||||
|
||||
for (const incoming of incomingList) {
|
||||
const record =
|
||||
typeof incoming === 'object' && incoming
|
||||
? (incoming as Record<string, unknown>)
|
||||
: {};
|
||||
const staffId = String(record.staffId || '');
|
||||
if (!staffId) continue;
|
||||
|
||||
const staff = state.staffs.find((item) => item.id === staffId);
|
||||
if (!staff || staff.status === 'resigned') continue;
|
||||
|
||||
const fallbackShifts =
|
||||
scheduleMap.get(staffId) ??
|
||||
createDefaultWeekByRole(staff.roleType, state.templates);
|
||||
|
||||
const nextShifts = normalizeShifts(
|
||||
record.shifts,
|
||||
state.templates,
|
||||
fallbackShifts,
|
||||
);
|
||||
scheduleMap.set(staffId, nextShifts);
|
||||
}
|
||||
|
||||
state.schedules = state.staffs.map((staff) => {
|
||||
const baseShifts =
|
||||
scheduleMap.get(staff.id) ??
|
||||
createDefaultWeekByRole(staff.roleType, state.templates);
|
||||
return {
|
||||
staffId: staff.id,
|
||||
shifts:
|
||||
staff.status === 'resigned'
|
||||
? Array.from({ length: 7 }).map((_, dayOfWeek) =>
|
||||
createDayShift(dayOfWeek, 'off', state.templates),
|
||||
)
|
||||
: baseShifts.map((shift) => ({ ...shift })),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
storeId,
|
||||
templates: cloneTemplates(state.templates),
|
||||
schedules: cloneSchedules(state.schedules),
|
||||
weekStartDate: state.weekStartDate,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
/** 复制门店模板与排班。 */
|
||||
Mock.mock(/\/store\/staff\/copy/, 'post', (options: MockRequestOptions) => {
|
||||
const body = parseBody(options);
|
||||
const sourceStoreId = String(body.sourceStoreId || '');
|
||||
const copyScope = String(body.copyScope || '');
|
||||
const targetStoreIds = Array.isArray(body.targetStoreIds)
|
||||
? body.targetStoreIds.map(String).filter(Boolean)
|
||||
: [];
|
||||
|
||||
if (
|
||||
!sourceStoreId ||
|
||||
copyScope !== 'template_and_schedule' ||
|
||||
targetStoreIds.length === 0
|
||||
) {
|
||||
return {
|
||||
code: 200,
|
||||
data: { copiedCount: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const sourceState = ensureStoreState(sourceStoreId);
|
||||
const sourceScheduleMap = createScheduleMap(sourceState.schedules);
|
||||
const uniqueTargets = [...new Set(targetStoreIds)].filter(
|
||||
(id) => id !== sourceStoreId,
|
||||
);
|
||||
|
||||
for (const targetStoreId of uniqueTargets) {
|
||||
const targetState = ensureStoreState(targetStoreId);
|
||||
targetState.templates = cloneTemplates(sourceState.templates);
|
||||
|
||||
targetState.schedules = targetState.staffs.map((staff) => {
|
||||
const targetShiftFallback =
|
||||
sourceScheduleMap.get(staff.id) ??
|
||||
createDefaultWeekByRole(staff.roleType, targetState.templates);
|
||||
|
||||
return {
|
||||
staffId: staff.id,
|
||||
shifts:
|
||||
staff.status === 'resigned'
|
||||
? Array.from({ length: 7 }).map((_, dayOfWeek) =>
|
||||
createDayShift(dayOfWeek, 'off', targetState.templates),
|
||||
)
|
||||
: normalizeShifts(
|
||||
targetShiftFallback,
|
||||
targetState.templates,
|
||||
targetShiftFallback,
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
copiedCount: uniqueTargets.length,
|
||||
copyScope: 'template_and_schedule',
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -1,257 +0,0 @@
|
||||
import Mock from 'mockjs';
|
||||
|
||||
const Random = Mock.Random;
|
||||
|
||||
/** mockjs 请求回调参数 */
|
||||
interface MockRequestOptions {
|
||||
url: string;
|
||||
type: string;
|
||||
body: null | string;
|
||||
}
|
||||
|
||||
/** 门店筛选参数 */
|
||||
interface StoreFilterParams {
|
||||
keyword?: string;
|
||||
businessStatus?: string;
|
||||
auditStatus?: string;
|
||||
serviceType?: string;
|
||||
page?: string;
|
||||
pageSize?: string;
|
||||
}
|
||||
|
||||
// 预定义门店数据,保证每次请求返回一致的数据
|
||||
const storePool = generateStores(23);
|
||||
|
||||
function generateStores(count: number) {
|
||||
const districts = [
|
||||
'朝阳区建国路88号',
|
||||
'海淀区中关村大街66号',
|
||||
'朝阳区望京西路50号',
|
||||
'通州区新华大街120号',
|
||||
'丰台区丰台路18号',
|
||||
'西城区西单北大街100号',
|
||||
'东城区王府井大街200号',
|
||||
'大兴区黄村镇兴华路30号',
|
||||
'昌平区回龙观东大街15号',
|
||||
'顺义区府前街8号',
|
||||
'石景山区石景山路22号',
|
||||
'房山区良乡拱辰大街55号',
|
||||
'密云区鼓楼东大街10号',
|
||||
'怀柔区青春路6号',
|
||||
'平谷区府前街12号',
|
||||
'门头沟区新桥大街3号',
|
||||
'延庆区妫水北街9号',
|
||||
'亦庄经济开发区荣华南路1号',
|
||||
'望京SOHO T1-2层',
|
||||
'三里屯太古里南区B1',
|
||||
'国贸商城3层',
|
||||
'五道口华联商厦1层',
|
||||
'中关村食宝街B1层',
|
||||
];
|
||||
|
||||
const managerNames = [
|
||||
'张伟',
|
||||
'李娜',
|
||||
'王磊',
|
||||
'赵敏',
|
||||
'刘洋',
|
||||
'陈静',
|
||||
'杨帆',
|
||||
'周杰',
|
||||
'吴芳',
|
||||
'孙涛',
|
||||
'马丽',
|
||||
'朱军',
|
||||
'胡明',
|
||||
'郭强',
|
||||
'何欢',
|
||||
'林峰',
|
||||
'徐婷',
|
||||
'高远',
|
||||
'罗斌',
|
||||
'梁宇',
|
||||
'宋佳',
|
||||
'唐亮',
|
||||
'韩雪',
|
||||
];
|
||||
|
||||
const storeNames = [
|
||||
'老三家外卖(朝阳店)',
|
||||
'老三家外卖(海淀店)',
|
||||
'老三家外卖(望京店)',
|
||||
'老三家外卖(通州店)',
|
||||
'老三家外卖(丰台店)',
|
||||
'老三家外卖(西单店)',
|
||||
'老三家外卖(王府井店)',
|
||||
'老三家外卖(大兴店)',
|
||||
'老三家外卖(回龙观店)',
|
||||
'老三家外卖(顺义店)',
|
||||
'老三家外卖(石景山店)',
|
||||
'老三家外卖(良乡店)',
|
||||
'老三家外卖(密云店)',
|
||||
'老三家外卖(怀柔店)',
|
||||
'老三家外卖(平谷店)',
|
||||
'老三家外卖(门头沟店)',
|
||||
'老三家外卖(延庆店)',
|
||||
'老三家外卖(亦庄店)',
|
||||
'老三家外卖(望京SOHO店)',
|
||||
'老三家外卖(三里屯店)',
|
||||
'老三家外卖(国贸店)',
|
||||
'老三家外卖(五道口店)',
|
||||
'老三家外卖(中关村店)',
|
||||
];
|
||||
|
||||
const avatarColors = [
|
||||
'#3b82f6',
|
||||
'#f59e0b',
|
||||
'#8b5cf6',
|
||||
'#ef4444',
|
||||
'#22c55e',
|
||||
'#06b6d4',
|
||||
'#ec4899',
|
||||
'#f97316',
|
||||
'#14b8a6',
|
||||
'#6366f1',
|
||||
];
|
||||
|
||||
const stores = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
// 1. 按索引分配营业状态,模拟真实分布
|
||||
let businessStatus = 0;
|
||||
if (i >= 21) {
|
||||
businessStatus = Random.pick([0, 1, 2]);
|
||||
} else if (i >= 18) {
|
||||
businessStatus = 2;
|
||||
} else if (i >= 14) {
|
||||
businessStatus = 1;
|
||||
}
|
||||
|
||||
// 2. 按索引分配审核状态
|
||||
let auditStatus = 2;
|
||||
if (i < 20) {
|
||||
auditStatus = 1;
|
||||
} else if (i < 22) {
|
||||
auditStatus = 0;
|
||||
}
|
||||
|
||||
// 3. 循环分配服务方式组合
|
||||
const serviceTypeCombos = [[1], [1, 2], [1, 2, 3], [1, 3], [2, 3]];
|
||||
|
||||
stores.push({
|
||||
id: Random.guid(),
|
||||
name: storeNames[i] || `老三家外卖(分店${i + 1})`,
|
||||
code: `ST2025${String(i + 1).padStart(4, '0')}`,
|
||||
contactPhone: `138****${String(8001 + i).slice(-4)}`,
|
||||
managerName: managerNames[i] || Random.cname(),
|
||||
address: `北京市${districts[i] || `朝阳区某路${i + 1}号`}`,
|
||||
coverImage: '',
|
||||
businessStatus,
|
||||
auditStatus,
|
||||
serviceTypes: serviceTypeCombos[i % serviceTypeCombos.length],
|
||||
createdAt: Random.datetime('yyyy-MM-dd'),
|
||||
_avatarColor: avatarColors[i % avatarColors.length],
|
||||
});
|
||||
}
|
||||
return stores;
|
||||
}
|
||||
|
||||
function filterStores(params: StoreFilterParams) {
|
||||
let list = [...storePool];
|
||||
|
||||
// 1. 关键词模糊匹配(名称/编码/电话)
|
||||
if (params.keyword) {
|
||||
const kw = params.keyword.toLowerCase();
|
||||
list = list.filter(
|
||||
(s) =>
|
||||
s.name.toLowerCase().includes(kw) ||
|
||||
s.code.toLowerCase().includes(kw) ||
|
||||
s.contactPhone.includes(kw),
|
||||
);
|
||||
}
|
||||
|
||||
// 2. 营业状态筛选
|
||||
if (params.businessStatus) {
|
||||
const status = Number(params.businessStatus);
|
||||
list = list.filter((s) => s.businessStatus === status);
|
||||
}
|
||||
|
||||
// 3. 审核状态筛选
|
||||
if (params.auditStatus !== undefined && params.auditStatus !== '') {
|
||||
const status = Number(params.auditStatus);
|
||||
list = list.filter((s) => s.auditStatus === status);
|
||||
}
|
||||
|
||||
// 4. 服务方式筛选
|
||||
if (params.serviceType) {
|
||||
const type = Number(params.serviceType);
|
||||
list = list.filter((s) => (s.serviceTypes ?? []).includes(type));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 从 URL 中解析查询参数 */
|
||||
function parseUrlParams(url: string): StoreFilterParams {
|
||||
const parsed = new URL(url, 'http://localhost');
|
||||
const params: Record<string, string> = {};
|
||||
parsed.searchParams.forEach((value, key) => {
|
||||
params[key] = value;
|
||||
});
|
||||
return params;
|
||||
}
|
||||
|
||||
const enableStoreCrudMock = import.meta.env.VITE_STORE_CRUD_MOCK === 'true';
|
||||
|
||||
if (enableStoreCrudMock) {
|
||||
// 门店列表
|
||||
Mock.mock(/\/store\/list/, 'get', (options: MockRequestOptions) => {
|
||||
const params = parseUrlParams(options.url);
|
||||
|
||||
const page = Number(params.page) || 1;
|
||||
const pageSize = Number(params.pageSize) || 10;
|
||||
const filtered = filterStores(params);
|
||||
const start = (page - 1) * pageSize;
|
||||
const items = filtered.slice(start, start + pageSize);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
items,
|
||||
total: filtered.length,
|
||||
page,
|
||||
pageSize,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// 门店统计
|
||||
Mock.mock(/\/store\/stats/, 'get', () => {
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
total: storePool.length,
|
||||
operating: storePool.filter((s) => s.businessStatus === 0).length,
|
||||
resting: storePool.filter((s) => s.businessStatus === 1).length,
|
||||
pendingAudit: storePool.filter((s) => s.auditStatus === 1).length,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// 创建门店
|
||||
Mock.mock(/\/store\/create/, 'post', () => {
|
||||
return { code: 200, data: null };
|
||||
});
|
||||
|
||||
// 更新门店
|
||||
Mock.mock(/\/store\/update/, 'post', () => {
|
||||
return { code: 200, data: null };
|
||||
});
|
||||
|
||||
// 删除门店
|
||||
Mock.mock(/\/store\/delete/, 'post', () => {
|
||||
return { code: 200, data: null };
|
||||
});
|
||||
}
|
||||
|
||||
// 设置 mock 响应延迟
|
||||
Mock.setup({ timeout: '200-400' });
|
||||
@@ -40,12 +40,14 @@ const templateRows: Array<{
|
||||
|
||||
/** 将 HH:mm 字符串转换为时间组件值。 */
|
||||
function toPickerValue(time: string) {
|
||||
if (!time) return null;
|
||||
if (!time) return undefined;
|
||||
return dayjs(`2000-01-01 ${time}`);
|
||||
}
|
||||
|
||||
/** 将时间组件值转换为 HH:mm 字符串。 */
|
||||
function toTimeText(value: Dayjs | null) {
|
||||
function toTimeText(value: Dayjs | null | string | undefined) {
|
||||
if (!value) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
return value ? value.format('HH:mm') : '';
|
||||
}
|
||||
|
||||
@@ -53,7 +55,7 @@ function toTimeText(value: Dayjs | null) {
|
||||
function handleTemplateTimeChange(payload: {
|
||||
field: 'endTime' | 'startTime';
|
||||
shiftType: Exclude<ShiftType, 'off'>;
|
||||
value: Dayjs | null;
|
||||
value: Dayjs | null | string;
|
||||
}) {
|
||||
props.onSetTemplateTime({
|
||||
shiftType: payload.shiftType,
|
||||
|
||||
@@ -42,13 +42,15 @@ function getDayShift(dayOfWeek: number) {
|
||||
|
||||
/** 转换时间组件值。 */
|
||||
function toPickerValue(time: string) {
|
||||
if (!time) return null;
|
||||
if (!time) return undefined;
|
||||
return dayjs(`2000-01-01 ${time}`);
|
||||
}
|
||||
|
||||
/** 时间组件值转字符串。 */
|
||||
function toTimeText(value: Dayjs | null) {
|
||||
return value ? value.format('HH:mm') : '';
|
||||
function toTimeText(value: Dayjs | null | string | undefined) {
|
||||
if (!value) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
return value.format('HH:mm');
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -70,22 +70,13 @@ export function cloneStaffForm(
|
||||
|
||||
/** 按模板创建空白一周(默认休息)。 */
|
||||
export function createEmptyWeekShifts(
|
||||
templates: StoreShiftTemplatesDto,
|
||||
_templates: StoreShiftTemplatesDto,
|
||||
): StaffDayShiftDto[] {
|
||||
return DAY_OPTIONS.map((day) => {
|
||||
const fallbackShift = templates.morning;
|
||||
return {
|
||||
dayOfWeek: day.dayOfWeek,
|
||||
shiftType: 'off',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
...fallbackShift,
|
||||
};
|
||||
}).map((item) => ({
|
||||
dayOfWeek: item.dayOfWeek,
|
||||
shiftType: item.shiftType,
|
||||
startTime: item.shiftType === 'off' ? '' : item.startTime,
|
||||
endTime: item.shiftType === 'off' ? '' : item.endTime,
|
||||
return DAY_OPTIONS.map((day) => ({
|
||||
dayOfWeek: day.dayOfWeek,
|
||||
shiftType: 'off' as const,
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user