refactor(@vben/web-antd): remove store mocks and fix staff type checks

This commit is contained in:
2026-02-17 15:29:38 +08:00
parent af21caf2a7
commit e868cdbefc
11 changed files with 21 additions and 3066 deletions

View File

@@ -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');
}

View File

@@ -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 数据已启用');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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' });

View File

@@ -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,

View File

@@ -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>

View File

@@ -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: '',
}));
}