feat: 新增堂食管理页面并对齐后端dine-in路由
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
// Mock 数据入口,仅在开发环境下使用
|
||||
import './store';
|
||||
import './store-dinein';
|
||||
import './store-hours';
|
||||
import './store-pickup';
|
||||
|
||||
|
||||
515
apps/web-antd/src/mock/store-dinein.ts
Normal file
515
apps/web-antd/src/mock/store-dinein.ts
Normal file
@@ -0,0 +1,515 @@
|
||||
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 },
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user