feat: 新增堂食管理页面并对齐后端dine-in路由

This commit is contained in:
2026-02-16 15:29:52 +08:00
parent 8d1325edf0
commit 2aceb8b662
26 changed files with 3516 additions and 0 deletions

View File

@@ -1,5 +1,6 @@
// Mock 数据入口,仅在开发环境下使用
import './store';
import './store-dinein';
import './store-hours';
import './store-pickup';

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