feat: 新增堂食管理页面并对齐后端dine-in路由
This commit is contained in:
152
apps/web-antd/src/api/store-dinein/index.ts
Normal file
152
apps/web-antd/src/api/store-dinein/index.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
/**
|
||||||
|
* 文件职责:堂食管理模块 API 与 DTO 定义。
|
||||||
|
* 1. 维护区域、桌位、堂食设置类型。
|
||||||
|
* 2. 提供查询、保存、删除、批量生成与复制接口。
|
||||||
|
*/
|
||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
/** 桌位状态 */
|
||||||
|
export type DineInTableStatus = 'dining' | 'disabled' | 'free' | 'reserved';
|
||||||
|
|
||||||
|
/** 可编辑桌位状态(业务态由系统驱动,不在管理端直接设置) */
|
||||||
|
export type DineInEditableStatus = 'disabled' | 'free';
|
||||||
|
|
||||||
|
/** 堂食基础设置 */
|
||||||
|
export interface DineInBasicSettingsDto {
|
||||||
|
/** 是否开启堂食 */
|
||||||
|
enabled: boolean;
|
||||||
|
/** 默认用餐时长(分钟) */
|
||||||
|
defaultDiningMinutes: number;
|
||||||
|
/** 超时提醒阈值(分钟) */
|
||||||
|
overtimeReminderMinutes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 堂食区域 */
|
||||||
|
export interface DineInAreaDto {
|
||||||
|
/** 区域描述 */
|
||||||
|
description: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
/** 数字越小越靠前 */
|
||||||
|
sort: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 堂食桌位 */
|
||||||
|
export interface DineInTableDto {
|
||||||
|
areaId: string;
|
||||||
|
code: string;
|
||||||
|
id: string;
|
||||||
|
seats: number;
|
||||||
|
status: DineInTableStatus;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 门店堂食设置聚合 */
|
||||||
|
export interface StoreDineInSettingsDto {
|
||||||
|
areas: DineInAreaDto[];
|
||||||
|
basicSettings: DineInBasicSettingsDto;
|
||||||
|
storeId: string;
|
||||||
|
tables: DineInTableDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存堂食基础设置参数 */
|
||||||
|
export interface SaveStoreDineInBasicSettingsParams {
|
||||||
|
basicSettings: DineInBasicSettingsDto;
|
||||||
|
storeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存堂食区域参数 */
|
||||||
|
export interface SaveDineInAreaParams {
|
||||||
|
area: Omit<DineInAreaDto, 'id'> & { id?: string };
|
||||||
|
storeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除堂食区域参数 */
|
||||||
|
export interface DeleteDineInAreaParams {
|
||||||
|
areaId: string;
|
||||||
|
storeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存堂食桌位参数 */
|
||||||
|
export interface SaveDineInTableParams {
|
||||||
|
storeId: string;
|
||||||
|
table: Omit<DineInTableDto, 'id'> & { id?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除堂食桌位参数 */
|
||||||
|
export interface DeleteDineInTableParams {
|
||||||
|
storeId: string;
|
||||||
|
tableId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量生成桌位参数 */
|
||||||
|
export interface BatchCreateDineInTablesParams {
|
||||||
|
areaId: string;
|
||||||
|
codePrefix: string;
|
||||||
|
count: number;
|
||||||
|
seats: number;
|
||||||
|
startNumber: number;
|
||||||
|
storeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量生成桌位结果 */
|
||||||
|
export interface BatchCreateDineInTablesResultDto {
|
||||||
|
createdTables: DineInTableDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 复制堂食设置参数 */
|
||||||
|
export interface CopyStoreDineInSettingsParams {
|
||||||
|
sourceStoreId: string;
|
||||||
|
targetStoreIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取门店堂食设置 */
|
||||||
|
export async function getStoreDineInSettingsApi(storeId: string) {
|
||||||
|
return requestClient.get<StoreDineInSettingsDto>('/store/dinein', {
|
||||||
|
params: { storeId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存门店堂食基础设置 */
|
||||||
|
export async function saveStoreDineInBasicSettingsApi(
|
||||||
|
data: SaveStoreDineInBasicSettingsParams,
|
||||||
|
) {
|
||||||
|
return requestClient.post('/store/dinein/basic/save', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增或编辑堂食区域 */
|
||||||
|
export async function saveDineInAreaApi(data: SaveDineInAreaParams) {
|
||||||
|
return requestClient.post<DineInAreaDto>('/store/dinein/area/save', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除堂食区域 */
|
||||||
|
export async function deleteDineInAreaApi(data: DeleteDineInAreaParams) {
|
||||||
|
return requestClient.post('/store/dinein/area/delete', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增或编辑堂食桌位 */
|
||||||
|
export async function saveDineInTableApi(data: SaveDineInTableParams) {
|
||||||
|
return requestClient.post<DineInTableDto>('/store/dinein/table/save', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除堂食桌位 */
|
||||||
|
export async function deleteDineInTableApi(data: DeleteDineInTableParams) {
|
||||||
|
return requestClient.post('/store/dinein/table/delete', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量生成堂食桌位 */
|
||||||
|
export async function batchCreateDineInTablesApi(
|
||||||
|
data: BatchCreateDineInTablesParams,
|
||||||
|
) {
|
||||||
|
return requestClient.post<BatchCreateDineInTablesResultDto>(
|
||||||
|
'/store/dinein/table/batch-create',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 复制堂食设置到其他门店 */
|
||||||
|
export async function copyStoreDineInSettingsApi(
|
||||||
|
data: CopyStoreDineInSettingsParams,
|
||||||
|
) {
|
||||||
|
return requestClient.post('/store/dinein/copy', data);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// Mock 数据入口,仅在开发环境下使用
|
// Mock 数据入口,仅在开发环境下使用
|
||||||
import './store';
|
import './store';
|
||||||
|
import './store-dinein';
|
||||||
import './store-hours';
|
import './store-hours';
|
||||||
import './store-pickup';
|
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 },
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -46,6 +46,15 @@ const routes: RouteRecordRaw[] = [
|
|||||||
title: '自提设置',
|
title: '自提设置',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'StoreDineIn',
|
||||||
|
path: '/store/dine-in',
|
||||||
|
component: () => import('#/views/store/dine-in/index.vue'),
|
||||||
|
meta: {
|
||||||
|
icon: 'lucide:utensils',
|
||||||
|
title: '堂食管理',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 文件职责:堂食区域新增/编辑抽屉。
|
||||||
|
* 1. 展示区域基础字段。
|
||||||
|
* 2. 通过回调更新父级状态并提交。
|
||||||
|
*/
|
||||||
|
import type { DineInAreaFormState } from '#/views/store/dine-in/types';
|
||||||
|
|
||||||
|
import { Button, Drawer, Input, InputNumber } from 'ant-design-vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
form: DineInAreaFormState;
|
||||||
|
isSaving: boolean;
|
||||||
|
onSetDescription: (value: string) => void;
|
||||||
|
onSetName: (value: string) => void;
|
||||||
|
onSetSort: (value: number) => void;
|
||||||
|
open: boolean;
|
||||||
|
submitText: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'submit'): void;
|
||||||
|
(event: 'update:open', value: boolean): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function readInputValue(event: Event) {
|
||||||
|
const target = event.target as HTMLInputElement | HTMLTextAreaElement | null;
|
||||||
|
return target?.value ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function toNumber(value: null | number | string, fallback = 1) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Drawer
|
||||||
|
class="dinein-area-drawer-wrap"
|
||||||
|
:open="props.open"
|
||||||
|
:title="props.title"
|
||||||
|
:width="460"
|
||||||
|
:mask-closable="true"
|
||||||
|
@update:open="(value) => emit('update:open', value)"
|
||||||
|
>
|
||||||
|
<div class="drawer-form-block">
|
||||||
|
<label class="drawer-form-label required">区域名称</label>
|
||||||
|
<Input
|
||||||
|
:value="props.form.name"
|
||||||
|
:maxlength="20"
|
||||||
|
placeholder="如:大厅、包间、露台"
|
||||||
|
@input="(event) => props.onSetName(readInputValue(event))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-form-block">
|
||||||
|
<label class="drawer-form-label">区域描述</label>
|
||||||
|
<Input.TextArea
|
||||||
|
:value="props.form.description"
|
||||||
|
:maxlength="120"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="可选,如:主要用餐区域,可容纳约48人"
|
||||||
|
@input="(event) => props.onSetDescription(readInputValue(event))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-form-block">
|
||||||
|
<label class="drawer-form-label">排序</label>
|
||||||
|
<div class="drawer-input-with-unit">
|
||||||
|
<InputNumber
|
||||||
|
:value="props.form.sort"
|
||||||
|
:min="1"
|
||||||
|
:precision="0"
|
||||||
|
:controls="false"
|
||||||
|
class="drawer-input"
|
||||||
|
@update:value="(value) => props.onSetSort(toNumber(value, 1))"
|
||||||
|
/>
|
||||||
|
<span class="drawer-form-hint">数字越小越靠前</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="drawer-footer">
|
||||||
|
<Button @click="emit('update:open', false)">取消</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
:loading="props.isSaving"
|
||||||
|
@click="emit('submit')"
|
||||||
|
>
|
||||||
|
{{ props.submitText }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Drawer>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 文件职责:堂食区域管理区块。
|
||||||
|
* 1. 展示区域 pills 与当前区域说明。
|
||||||
|
* 2. 抛出区域新增、编辑、删除与切换事件。
|
||||||
|
*/
|
||||||
|
import type { DineInAreaDto } from '#/api/store-dinein';
|
||||||
|
|
||||||
|
import { Button, Card, Empty, Popconfirm } from 'ant-design-vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
getAreaTableCount: (areaId: string) => number;
|
||||||
|
isSaving: boolean;
|
||||||
|
selectedArea?: DineInAreaDto;
|
||||||
|
selectedAreaId: string;
|
||||||
|
areas: DineInAreaDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'add'): void;
|
||||||
|
(event: 'delete', area: DineInAreaDto): void;
|
||||||
|
(event: 'edit', area: DineInAreaDto): void;
|
||||||
|
(event: 'selectArea', areaId: string): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :bordered="false" class="dinein-card">
|
||||||
|
<template #title>
|
||||||
|
<span class="section-title">区域管理</span>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<Button type="primary" size="small" @click="emit('add')">添加区域</Button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="props.areas.length > 0">
|
||||||
|
<div class="dinein-area-pills">
|
||||||
|
<button
|
||||||
|
v-for="area in props.areas"
|
||||||
|
:key="area.id"
|
||||||
|
type="button"
|
||||||
|
class="dinein-area-pill"
|
||||||
|
:class="{ active: props.selectedAreaId === area.id }"
|
||||||
|
@click="emit('selectArea', area.id)"
|
||||||
|
>
|
||||||
|
{{ area.name }} ({{ props.getAreaTableCount(area.id) }}桌)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="props.selectedArea" class="dinein-area-info">
|
||||||
|
<span class="dinein-area-description">
|
||||||
|
{{ props.selectedArea.name }} -
|
||||||
|
{{ props.selectedArea.description || '暂无描述' }}
|
||||||
|
</span>
|
||||||
|
<div class="dinein-area-actions">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
@click="emit('edit', props.selectedArea)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Popconfirm
|
||||||
|
title="确认删除该区域吗?"
|
||||||
|
ok-text="确认"
|
||||||
|
cancel-text="取消"
|
||||||
|
@confirm="emit('delete', props.selectedArea)"
|
||||||
|
>
|
||||||
|
<Button type="link" danger size="small" :loading="props.isSaving">
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<Empty v-else description="暂无区域,请先添加" />
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 文件职责:堂食基础设置区块。
|
||||||
|
* 1. 展示堂食开关、默认时长、超时提醒配置。
|
||||||
|
* 2. 抛出保存与重置事件。
|
||||||
|
*/
|
||||||
|
import type { DineInBasicSettingsDto } from '#/api/store-dinein';
|
||||||
|
|
||||||
|
import { Button, Card, InputNumber, Switch } from 'ant-design-vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isSaving: boolean;
|
||||||
|
onSetDefaultDiningMinutes: (value: number) => void;
|
||||||
|
onSetEnabled: (value: boolean) => void;
|
||||||
|
onSetOvertimeReminderMinutes: (value: number) => void;
|
||||||
|
settings: DineInBasicSettingsDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'reset'): void;
|
||||||
|
(event: 'save'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function toNumber(value: null | number | string, fallback = 0) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :bordered="false" class="dinein-card">
|
||||||
|
<template #title>
|
||||||
|
<span class="section-title">堂食设置</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="dinein-form-row">
|
||||||
|
<label class="dinein-form-label">是否开启堂食</label>
|
||||||
|
<div class="dinein-form-control">
|
||||||
|
<Switch
|
||||||
|
:checked="props.settings.enabled"
|
||||||
|
@update:checked="(value) => props.onSetEnabled(Boolean(value))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dinein-form-row">
|
||||||
|
<label class="dinein-form-label">默认用餐时长</label>
|
||||||
|
<div class="dinein-form-control">
|
||||||
|
<InputNumber
|
||||||
|
:value="props.settings.defaultDiningMinutes"
|
||||||
|
:min="1"
|
||||||
|
:precision="0"
|
||||||
|
:controls="false"
|
||||||
|
class="dinein-number-input"
|
||||||
|
@update:value="
|
||||||
|
(value) => props.onSetDefaultDiningMinutes(toNumber(value, 90))
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<span class="dinein-form-unit">分钟</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dinein-form-row">
|
||||||
|
<label class="dinein-form-label">超时提醒</label>
|
||||||
|
<div class="dinein-form-control">
|
||||||
|
<InputNumber
|
||||||
|
:value="props.settings.overtimeReminderMinutes"
|
||||||
|
:min="0"
|
||||||
|
:precision="0"
|
||||||
|
:controls="false"
|
||||||
|
class="dinein-number-input"
|
||||||
|
@update:value="
|
||||||
|
(value) => props.onSetOvertimeReminderMinutes(toNumber(value, 10))
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<span class="dinein-form-unit">分钟</span>
|
||||||
|
<span class="dinein-form-hint">超过默认用餐时长后提醒</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dinein-form-actions">
|
||||||
|
<Button :disabled="props.isSaving" @click="emit('reset')">重置</Button>
|
||||||
|
<Button type="primary" :loading="props.isSaving" @click="emit('save')">
|
||||||
|
保存设置
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 文件职责:堂食桌位批量生成弹窗。
|
||||||
|
* 1. 展示批量参数并实时预览编号。
|
||||||
|
* 2. 通过回调更新父级状态并提交。
|
||||||
|
*/
|
||||||
|
import type { DineInSeatsOption } from '#/views/store/dine-in/types';
|
||||||
|
|
||||||
|
import { Input, InputNumber, Modal, Select, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
areaOptions: Array<{ label: string; value: string }>;
|
||||||
|
form: {
|
||||||
|
areaId: string;
|
||||||
|
codePrefix: string;
|
||||||
|
count: number;
|
||||||
|
seats: number;
|
||||||
|
startNumber: number;
|
||||||
|
};
|
||||||
|
isSaving: boolean;
|
||||||
|
onSetAreaId: (value: string) => void;
|
||||||
|
onSetCodePrefix: (value: string) => void;
|
||||||
|
onSetCount: (value: number) => void;
|
||||||
|
onSetSeats: (value: number) => void;
|
||||||
|
onSetStartNumber: (value: number) => void;
|
||||||
|
open: boolean;
|
||||||
|
previewCodes: string[];
|
||||||
|
seatsOptions: DineInSeatsOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'submit'): void;
|
||||||
|
(event: 'update:open', value: boolean): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function readInputValue(event: Event) {
|
||||||
|
const target = event.target as HTMLInputElement | null;
|
||||||
|
return target?.value ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function readString(value: unknown) {
|
||||||
|
if (typeof value === 'number' || typeof value === 'string')
|
||||||
|
return String(value);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function readNumber(value: unknown, fallback = 1) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
:open="props.open"
|
||||||
|
title="批量生成桌位"
|
||||||
|
:width="520"
|
||||||
|
:confirm-loading="props.isSaving"
|
||||||
|
ok-text="确认生成"
|
||||||
|
cancel-text="取消"
|
||||||
|
:mask-closable="true"
|
||||||
|
wrap-class-name="dinein-batch-modal-wrap"
|
||||||
|
@update:open="(value) => emit('update:open', value)"
|
||||||
|
@ok="emit('submit')"
|
||||||
|
@cancel="emit('update:open', false)"
|
||||||
|
>
|
||||||
|
<div class="batch-form-grid">
|
||||||
|
<div class="batch-form-item full">
|
||||||
|
<label>所属区域</label>
|
||||||
|
<Select
|
||||||
|
:value="props.form.areaId"
|
||||||
|
:options="props.areaOptions"
|
||||||
|
placeholder="请选择区域"
|
||||||
|
@update:value="(value) => props.onSetAreaId(readString(value))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="batch-form-item">
|
||||||
|
<label>编号前缀</label>
|
||||||
|
<Input
|
||||||
|
:value="props.form.codePrefix"
|
||||||
|
:maxlength="6"
|
||||||
|
placeholder="如:A"
|
||||||
|
@input="(event) => props.onSetCodePrefix(readInputValue(event))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="batch-form-item">
|
||||||
|
<label>起始编号</label>
|
||||||
|
<InputNumber
|
||||||
|
:value="props.form.startNumber"
|
||||||
|
:min="1"
|
||||||
|
:precision="0"
|
||||||
|
:controls="false"
|
||||||
|
class="batch-input-number"
|
||||||
|
@update:value="
|
||||||
|
(value) => props.onSetStartNumber(readNumber(value, 1))
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="batch-form-item">
|
||||||
|
<label>生成数量</label>
|
||||||
|
<InputNumber
|
||||||
|
:value="props.form.count"
|
||||||
|
:min="1"
|
||||||
|
:max="50"
|
||||||
|
:precision="0"
|
||||||
|
:controls="false"
|
||||||
|
class="batch-input-number"
|
||||||
|
@update:value="(value) => props.onSetCount(readNumber(value, 1))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="batch-form-item">
|
||||||
|
<label>座位数</label>
|
||||||
|
<Select
|
||||||
|
:value="props.form.seats"
|
||||||
|
:options="props.seatsOptions"
|
||||||
|
@update:value="(value) => props.onSetSeats(readNumber(value, 4))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="batch-preview-wrap">
|
||||||
|
<div class="batch-preview-title">预览:将生成以下桌位</div>
|
||||||
|
<div class="batch-preview-tags">
|
||||||
|
<Tag v-for="code in props.previewCodes" :key="code" color="processing">
|
||||||
|
{{ code }}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 文件职责:堂食桌位新增/编辑抽屉。
|
||||||
|
* 1. 展示桌位编号、区域、座位、停用状态、标签。
|
||||||
|
* 2. 通过回调更新父级状态并提交。
|
||||||
|
*/
|
||||||
|
import type { DineInTableStatus } from '#/api/store-dinein';
|
||||||
|
import type {
|
||||||
|
DineInSeatsOption,
|
||||||
|
DineInTableFormState,
|
||||||
|
} from '#/views/store/dine-in/types';
|
||||||
|
|
||||||
|
import { Button, Drawer, Input, Select, Switch, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
areaOptions: Array<{ label: string; value: string }>;
|
||||||
|
form: DineInTableFormState;
|
||||||
|
isSaving: boolean;
|
||||||
|
onSetAreaId: (value: string) => void;
|
||||||
|
onSetCode: (value: string) => void;
|
||||||
|
onSetDisabled: (value: boolean) => void;
|
||||||
|
onSetSeats: (value: number) => void;
|
||||||
|
onSetTags: (value: string[]) => void;
|
||||||
|
open: boolean;
|
||||||
|
seatsOptions: DineInSeatsOption[];
|
||||||
|
statusLabelMap: Record<DineInTableStatus, string>;
|
||||||
|
submitText: string;
|
||||||
|
tagSuggestions: string[];
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'submit'): void;
|
||||||
|
(event: 'update:open', value: boolean): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const statusHintText = '就餐中/已预约由业务驱动,管理端仅控制是否停用';
|
||||||
|
|
||||||
|
function readInputValue(event: Event) {
|
||||||
|
const target = event.target as HTMLInputElement | null;
|
||||||
|
return target?.value ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function readString(value: unknown) {
|
||||||
|
if (typeof value === 'number' || typeof value === 'string')
|
||||||
|
return String(value);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function readNumber(value: unknown, fallback = 4) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readTagValues(value: unknown) {
|
||||||
|
if (!Array.isArray(value)) return [];
|
||||||
|
return value
|
||||||
|
.map((item) => (typeof item === 'string' ? item : String(item)))
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Drawer
|
||||||
|
class="dinein-table-drawer-wrap"
|
||||||
|
:open="props.open"
|
||||||
|
:title="props.title"
|
||||||
|
:width="460"
|
||||||
|
:mask-closable="true"
|
||||||
|
@update:open="(value) => emit('update:open', value)"
|
||||||
|
>
|
||||||
|
<div class="drawer-form-block">
|
||||||
|
<label class="drawer-form-label required">桌位编号</label>
|
||||||
|
<Input
|
||||||
|
:value="props.form.code"
|
||||||
|
:maxlength="20"
|
||||||
|
placeholder="如:A09"
|
||||||
|
@input="(event) => props.onSetCode(readInputValue(event))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-form-block">
|
||||||
|
<label class="drawer-form-label">所属区域</label>
|
||||||
|
<Select
|
||||||
|
:value="props.form.areaId"
|
||||||
|
:options="props.areaOptions"
|
||||||
|
placeholder="请选择区域"
|
||||||
|
@update:value="(value) => props.onSetAreaId(readString(value))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-form-grid">
|
||||||
|
<div class="drawer-form-block">
|
||||||
|
<label class="drawer-form-label required">座位数</label>
|
||||||
|
<Select
|
||||||
|
:value="props.form.seats"
|
||||||
|
:options="props.seatsOptions"
|
||||||
|
@update:value="(value) => props.onSetSeats(readNumber(value, 4))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-form-block">
|
||||||
|
<label class="drawer-form-label">停用状态</label>
|
||||||
|
<div class="drawer-switch-row">
|
||||||
|
<Switch
|
||||||
|
:checked="props.form.isDisabled"
|
||||||
|
@update:checked="(value) => props.onSetDisabled(Boolean(value))"
|
||||||
|
/>
|
||||||
|
<span class="drawer-form-hint">打开后桌位不可被分配</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-form-block">
|
||||||
|
<label class="drawer-form-label">业务状态</label>
|
||||||
|
<div class="drawer-status-preview">
|
||||||
|
<Tag color="processing">
|
||||||
|
{{ props.statusLabelMap[props.form.sourceStatus] ?? '--' }}
|
||||||
|
</Tag>
|
||||||
|
<span class="drawer-form-hint">{{ statusHintText }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-form-block">
|
||||||
|
<label class="drawer-form-label">标签</label>
|
||||||
|
<Select
|
||||||
|
mode="tags"
|
||||||
|
:value="props.form.tags"
|
||||||
|
:options="
|
||||||
|
props.tagSuggestions.map((item) => ({ label: item, value: item }))
|
||||||
|
"
|
||||||
|
:max-tag-count="4"
|
||||||
|
placeholder="输入后回车添加,如:靠窗、VIP"
|
||||||
|
@update:value="(value) => props.onSetTags(readTagValues(value))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="drawer-footer">
|
||||||
|
<Button @click="emit('update:open', false)">取消</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
:loading="props.isSaving"
|
||||||
|
@click="emit('submit')"
|
||||||
|
>
|
||||||
|
{{ props.submitText }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Drawer>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 文件职责:堂食桌位列表区块。
|
||||||
|
* 1. 展示当前区域桌位卡片。
|
||||||
|
* 2. 抛出二维码、编辑、删除、批量生成、新增事件。
|
||||||
|
*/
|
||||||
|
import type { DineInTableDto } from '#/api/store-dinein';
|
||||||
|
import type { DineInStatusOption } from '#/views/store/dine-in/types';
|
||||||
|
|
||||||
|
import { Button, Card, Empty, Popconfirm, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isSaving: boolean;
|
||||||
|
resolveStatusClassName: (status: DineInTableDto['status']) => string;
|
||||||
|
statusMap: Record<DineInTableDto['status'], DineInStatusOption>;
|
||||||
|
tables: DineInTableDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'add'): void;
|
||||||
|
(event: 'batch'): void;
|
||||||
|
(event: 'delete', tableId: string): void;
|
||||||
|
(event: 'edit', table: DineInTableDto): void;
|
||||||
|
(event: 'qrcode', table: DineInTableDto): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :bordered="false" class="dinein-card">
|
||||||
|
<template #title>
|
||||||
|
<span class="section-title">桌位列表</span>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<div class="dinein-table-header-actions">
|
||||||
|
<Button size="small" @click="emit('batch')">批量生成</Button>
|
||||||
|
<Button type="primary" size="small" @click="emit('add')">
|
||||||
|
添加桌位
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="props.tables.length > 0">
|
||||||
|
<div class="dinein-table-grid">
|
||||||
|
<div
|
||||||
|
v-for="table in props.tables"
|
||||||
|
:key="table.id"
|
||||||
|
class="dinein-table-card"
|
||||||
|
:class="{
|
||||||
|
disabled: table.status === 'disabled',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="dinein-table-code">{{ table.code }}</div>
|
||||||
|
<div class="dinein-table-seat">{{ table.seats }}人桌</div>
|
||||||
|
<div
|
||||||
|
class="dinein-table-status"
|
||||||
|
:class="props.resolveStatusClassName(table.status)"
|
||||||
|
>
|
||||||
|
<span class="status-dot"></span>
|
||||||
|
{{ props.statusMap[table.status]?.label ?? '--' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dinein-table-tags">
|
||||||
|
<Tag v-for="tag in table.tags" :key="`${table.id}-${tag}`">
|
||||||
|
{{ tag }}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dinein-table-footer">
|
||||||
|
<Button size="small" type="text" @click="emit('qrcode', table)">
|
||||||
|
二维码
|
||||||
|
</Button>
|
||||||
|
<Button size="small" type="text" @click="emit('edit', table)">
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Popconfirm
|
||||||
|
title="确认删除该桌位吗?"
|
||||||
|
ok-text="确认"
|
||||||
|
cancel-text="取消"
|
||||||
|
@confirm="emit('delete', table.id)"
|
||||||
|
>
|
||||||
|
<Button size="small" type="text" danger :loading="props.isSaving">
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<Empty v-else description="当前区域暂无桌位" />
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件职责:堂食区域动作。
|
||||||
|
* 1. 管理区域新增/编辑抽屉状态与字段。
|
||||||
|
* 2. 处理区域新增、编辑、删除流程。
|
||||||
|
*/
|
||||||
|
import type { DineInAreaDto, DineInTableDto } from '#/api/store-dinein';
|
||||||
|
import type {
|
||||||
|
DineInAreaDrawerMode,
|
||||||
|
DineInAreaFormState,
|
||||||
|
} from '#/views/store/dine-in/types';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { deleteDineInAreaApi, saveDineInAreaApi } from '#/api/store-dinein';
|
||||||
|
|
||||||
|
import {
|
||||||
|
countAreaTables,
|
||||||
|
createDineInId,
|
||||||
|
sortAreas,
|
||||||
|
validateAreaForm,
|
||||||
|
} from './helpers';
|
||||||
|
|
||||||
|
interface CreateAreaActionsOptions {
|
||||||
|
areaDrawerMode: Ref<DineInAreaDrawerMode>;
|
||||||
|
areaForm: DineInAreaFormState;
|
||||||
|
areas: Ref<DineInAreaDto[]>;
|
||||||
|
fixSelectedArea: () => void;
|
||||||
|
isAreaDrawerOpen: Ref<boolean>;
|
||||||
|
isSavingArea: Ref<boolean>;
|
||||||
|
selectedAreaId: Ref<string>;
|
||||||
|
selectedStoreId: Ref<string>;
|
||||||
|
tables: Ref<DineInTableDto[]>;
|
||||||
|
updateSnapshot: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAreaActions(options: CreateAreaActionsOptions) {
|
||||||
|
/** 打开区域抽屉并初始化表单。 */
|
||||||
|
function openAreaDrawer(mode: DineInAreaDrawerMode, area?: DineInAreaDto) {
|
||||||
|
options.areaDrawerMode.value = mode;
|
||||||
|
if (mode === 'edit' && area) {
|
||||||
|
options.areaForm.id = area.id;
|
||||||
|
options.areaForm.name = area.name;
|
||||||
|
options.areaForm.description = area.description;
|
||||||
|
options.areaForm.sort = area.sort;
|
||||||
|
options.isAreaDrawerOpen.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSort =
|
||||||
|
options.areas.value.length === 0
|
||||||
|
? 1
|
||||||
|
: Math.max(...options.areas.value.map((item) => item.sort)) + 1;
|
||||||
|
options.areaForm.id = '';
|
||||||
|
options.areaForm.name = '';
|
||||||
|
options.areaForm.description = '';
|
||||||
|
options.areaForm.sort = nextSort;
|
||||||
|
options.isAreaDrawerOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 控制区域抽屉显隐。 */
|
||||||
|
function setAreaDrawerOpen(value: boolean) {
|
||||||
|
options.isAreaDrawerOpen.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAreaName(value: string) {
|
||||||
|
options.areaForm.name = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAreaDescription(value: string) {
|
||||||
|
options.areaForm.description = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAreaSort(value: number) {
|
||||||
|
options.areaForm.sort = Math.max(1, Math.floor(Number(value || 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交区域表单。 */
|
||||||
|
async function handleSubmitArea() {
|
||||||
|
const validateMessage = validateAreaForm({
|
||||||
|
areaId: options.areaForm.id,
|
||||||
|
areas: options.areas.value,
|
||||||
|
form: options.areaForm,
|
||||||
|
});
|
||||||
|
if (validateMessage) {
|
||||||
|
message.error(validateMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!options.selectedStoreId.value) return;
|
||||||
|
|
||||||
|
options.isSavingArea.value = true;
|
||||||
|
try {
|
||||||
|
const areaId = options.areaForm.id || createDineInId('area');
|
||||||
|
const areaPayload: DineInAreaDto = {
|
||||||
|
id: areaId,
|
||||||
|
name: options.areaForm.name.trim(),
|
||||||
|
description: options.areaForm.description.trim(),
|
||||||
|
sort: Math.max(1, Math.floor(options.areaForm.sort)),
|
||||||
|
};
|
||||||
|
|
||||||
|
await saveDineInAreaApi({
|
||||||
|
storeId: options.selectedStoreId.value,
|
||||||
|
area: areaPayload,
|
||||||
|
});
|
||||||
|
|
||||||
|
options.areas.value =
|
||||||
|
options.areaDrawerMode.value === 'edit' && options.areaForm.id
|
||||||
|
? sortAreas(
|
||||||
|
options.areas.value.map((item) =>
|
||||||
|
item.id === options.areaForm.id ? areaPayload : item,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: sortAreas([...options.areas.value, areaPayload]);
|
||||||
|
|
||||||
|
if (!options.selectedAreaId.value) {
|
||||||
|
options.selectedAreaId.value = areaPayload.id;
|
||||||
|
}
|
||||||
|
options.fixSelectedArea();
|
||||||
|
options.updateSnapshot();
|
||||||
|
options.isAreaDrawerOpen.value = false;
|
||||||
|
message.success(
|
||||||
|
options.areaDrawerMode.value === 'edit' ? '区域已保存' : '区域已添加',
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
options.isSavingArea.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除区域。 */
|
||||||
|
async function handleDeleteArea(area: DineInAreaDto) {
|
||||||
|
if (!options.selectedStoreId.value) return;
|
||||||
|
|
||||||
|
const tableCount = countAreaTables(area.id, options.tables.value);
|
||||||
|
if (tableCount > 0) {
|
||||||
|
message.error('该区域仍有桌位,请先迁移或删除桌位');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.isSavingArea.value = true;
|
||||||
|
try {
|
||||||
|
await deleteDineInAreaApi({
|
||||||
|
storeId: options.selectedStoreId.value,
|
||||||
|
areaId: area.id,
|
||||||
|
});
|
||||||
|
options.areas.value = options.areas.value.filter(
|
||||||
|
(item) => item.id !== area.id,
|
||||||
|
);
|
||||||
|
options.fixSelectedArea();
|
||||||
|
options.updateSnapshot();
|
||||||
|
message.success('区域已删除');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
options.isSavingArea.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleDeleteArea,
|
||||||
|
handleSubmitArea,
|
||||||
|
openAreaDrawer,
|
||||||
|
setAreaDescription,
|
||||||
|
setAreaDrawerOpen,
|
||||||
|
setAreaName,
|
||||||
|
setAreaSort,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
/**
|
||||||
|
* 文件职责:堂食管理页面静态常量。
|
||||||
|
* 1. 维护默认区域、桌位、基础设置。
|
||||||
|
* 2. 提供状态、座位数等选项映射。
|
||||||
|
*/
|
||||||
|
import type {
|
||||||
|
DineInAreaDto,
|
||||||
|
DineInBasicSettingsDto,
|
||||||
|
DineInEditableStatus,
|
||||||
|
DineInTableDto,
|
||||||
|
DineInTableStatus,
|
||||||
|
} from '#/api/store-dinein';
|
||||||
|
import type {
|
||||||
|
DineInSeatsOption,
|
||||||
|
DineInStatusOption,
|
||||||
|
} from '#/views/store/dine-in/types';
|
||||||
|
|
||||||
|
export const DINE_IN_SEATS_OPTIONS: DineInSeatsOption[] = [
|
||||||
|
{ label: '2人桌', value: 2 },
|
||||||
|
{ label: '4人桌', value: 4 },
|
||||||
|
{ label: '6人桌', value: 6 },
|
||||||
|
{ label: '8人桌', value: 8 },
|
||||||
|
{ label: '10人桌', value: 10 },
|
||||||
|
{ label: '12人桌', value: 12 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DINE_IN_STATUS_MAP: Record<DineInTableStatus, DineInStatusOption> =
|
||||||
|
{
|
||||||
|
free: { value: 'free', label: '空闲', color: '#22c55e', className: 'free' },
|
||||||
|
dining: {
|
||||||
|
value: 'dining',
|
||||||
|
label: '就餐中',
|
||||||
|
color: '#f59e0b',
|
||||||
|
className: 'dining',
|
||||||
|
},
|
||||||
|
reserved: {
|
||||||
|
value: 'reserved',
|
||||||
|
label: '已预约',
|
||||||
|
color: '#1677ff',
|
||||||
|
className: 'reserved',
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
value: 'disabled',
|
||||||
|
label: '停用',
|
||||||
|
color: '#9ca3af',
|
||||||
|
className: 'disabled',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DINE_IN_EDITABLE_STATUS_OPTIONS: Array<{
|
||||||
|
label: string;
|
||||||
|
value: DineInEditableStatus;
|
||||||
|
}> = [
|
||||||
|
{ label: '空闲', value: 'free' },
|
||||||
|
{ label: '停用', value: 'disabled' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DEFAULT_DINE_IN_BASIC_SETTINGS: DineInBasicSettingsDto = {
|
||||||
|
enabled: true,
|
||||||
|
defaultDiningMinutes: 90,
|
||||||
|
overtimeReminderMinutes: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_DINE_IN_AREAS: DineInAreaDto[] = [
|
||||||
|
{
|
||||||
|
id: 'dinein-area-hall',
|
||||||
|
name: '大厅',
|
||||||
|
description: '主要用餐区域,共12张桌位,可容纳约48人同时用餐',
|
||||||
|
sort: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dinein-area-private-room',
|
||||||
|
name: '包间',
|
||||||
|
description: '安静独立区域,适合聚餐与商务接待',
|
||||||
|
sort: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dinein-area-terrace',
|
||||||
|
name: '露台',
|
||||||
|
description: '开放式外摆区域,适合休闲场景',
|
||||||
|
sort: 3,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DEFAULT_DINE_IN_TABLES: DineInTableDto[] = [
|
||||||
|
{
|
||||||
|
id: 'dinein-table-a01',
|
||||||
|
code: 'A01',
|
||||||
|
areaId: 'dinein-area-hall',
|
||||||
|
seats: 4,
|
||||||
|
status: 'free',
|
||||||
|
tags: ['靠窗'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dinein-table-a02',
|
||||||
|
code: 'A02',
|
||||||
|
areaId: 'dinein-area-hall',
|
||||||
|
seats: 2,
|
||||||
|
status: 'dining',
|
||||||
|
tags: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dinein-table-a03',
|
||||||
|
code: 'A03',
|
||||||
|
areaId: 'dinein-area-hall',
|
||||||
|
seats: 6,
|
||||||
|
status: 'free',
|
||||||
|
tags: ['VIP', '靠窗'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dinein-table-a04',
|
||||||
|
code: 'A04',
|
||||||
|
areaId: 'dinein-area-hall',
|
||||||
|
seats: 4,
|
||||||
|
status: 'reserved',
|
||||||
|
tags: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dinein-table-a07',
|
||||||
|
code: 'A07',
|
||||||
|
areaId: 'dinein-area-hall',
|
||||||
|
seats: 4,
|
||||||
|
status: 'disabled',
|
||||||
|
tags: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dinein-table-v01',
|
||||||
|
code: 'V01',
|
||||||
|
areaId: 'dinein-area-private-room',
|
||||||
|
seats: 8,
|
||||||
|
status: 'dining',
|
||||||
|
tags: ['包厢'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dinein-table-v02',
|
||||||
|
code: 'V02',
|
||||||
|
areaId: 'dinein-area-private-room',
|
||||||
|
seats: 6,
|
||||||
|
status: 'free',
|
||||||
|
tags: ['VIP'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dinein-table-t01',
|
||||||
|
code: 'T01',
|
||||||
|
areaId: 'dinein-area-terrace',
|
||||||
|
seats: 4,
|
||||||
|
status: 'free',
|
||||||
|
tags: ['露台'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TABLE_TAG_SUGGESTIONS = [
|
||||||
|
'VIP',
|
||||||
|
'包厢',
|
||||||
|
'吧台',
|
||||||
|
'安静区',
|
||||||
|
'家庭位',
|
||||||
|
'靠窗',
|
||||||
|
];
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import type { ComputedRef, Ref } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件职责:堂食管理复制动作。
|
||||||
|
* 1. 管理复制弹窗状态与目标门店勾选。
|
||||||
|
* 2. 提交复制请求并反馈结果。
|
||||||
|
*/
|
||||||
|
import type { StoreListItemDto } from '#/api/store';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { copyStoreDineInSettingsApi } from '#/api/store-dinein';
|
||||||
|
|
||||||
|
interface CreateCopyActionsOptions {
|
||||||
|
copyCandidates: ComputedRef<StoreListItemDto[]>;
|
||||||
|
copyTargetStoreIds: Ref<string[]>;
|
||||||
|
isCopyModalOpen: Ref<boolean>;
|
||||||
|
isCopySubmitting: Ref<boolean>;
|
||||||
|
selectedStoreId: Ref<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCopyActions(options: CreateCopyActionsOptions) {
|
||||||
|
/** 打开复制弹窗。 */
|
||||||
|
function openCopyModal() {
|
||||||
|
if (!options.selectedStoreId.value) return;
|
||||||
|
options.copyTargetStoreIds.value = [];
|
||||||
|
options.isCopyModalOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换单个目标门店。 */
|
||||||
|
function toggleCopyStore(storeId: string, checked: boolean) {
|
||||||
|
options.copyTargetStoreIds.value = checked
|
||||||
|
? [...new Set([storeId, ...options.copyTargetStoreIds.value])]
|
||||||
|
: options.copyTargetStoreIds.value.filter((id) => id !== storeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全选/取消全选。 */
|
||||||
|
function handleCopyCheckAll(checked: boolean) {
|
||||||
|
options.copyTargetStoreIds.value = checked
|
||||||
|
? options.copyCandidates.value.map((item) => item.id)
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交复制。 */
|
||||||
|
async function handleCopySubmit() {
|
||||||
|
if (!options.selectedStoreId.value) return;
|
||||||
|
if (options.copyTargetStoreIds.value.length === 0) {
|
||||||
|
message.error('请至少选择一个目标门店');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.isCopySubmitting.value = true;
|
||||||
|
try {
|
||||||
|
await copyStoreDineInSettingsApi({
|
||||||
|
sourceStoreId: options.selectedStoreId.value,
|
||||||
|
targetStoreIds: options.copyTargetStoreIds.value,
|
||||||
|
});
|
||||||
|
message.success(
|
||||||
|
`已复制到 ${options.copyTargetStoreIds.value.length} 家门店`,
|
||||||
|
);
|
||||||
|
options.isCopyModalOpen.value = false;
|
||||||
|
options.copyTargetStoreIds.value = [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
options.isCopySubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleCopyCheckAll,
|
||||||
|
handleCopySubmit,
|
||||||
|
openCopyModal,
|
||||||
|
toggleCopyStore,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件职责:堂食管理数据动作。
|
||||||
|
* 1. 加载门店列表与门店堂食设置。
|
||||||
|
* 2. 保存基础设置并维护快照。
|
||||||
|
*/
|
||||||
|
import type { StoreListItemDto } from '#/api/store';
|
||||||
|
import type {
|
||||||
|
DineInAreaDto,
|
||||||
|
DineInBasicSettingsDto,
|
||||||
|
DineInTableDto,
|
||||||
|
} from '#/api/store-dinein';
|
||||||
|
import type { DineInSettingsSnapshot } from '#/views/store/dine-in/types';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getStoreListApi } from '#/api/store';
|
||||||
|
import {
|
||||||
|
getStoreDineInSettingsApi,
|
||||||
|
saveStoreDineInBasicSettingsApi,
|
||||||
|
} from '#/api/store-dinein';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEFAULT_DINE_IN_AREAS,
|
||||||
|
DEFAULT_DINE_IN_BASIC_SETTINGS,
|
||||||
|
DEFAULT_DINE_IN_TABLES,
|
||||||
|
} from './constants';
|
||||||
|
import {
|
||||||
|
cloneAreas,
|
||||||
|
cloneBasicSettings,
|
||||||
|
cloneTables,
|
||||||
|
createSettingsSnapshot,
|
||||||
|
sortAreas,
|
||||||
|
sortTables,
|
||||||
|
} from './helpers';
|
||||||
|
|
||||||
|
interface CreateDataActionsOptions {
|
||||||
|
areas: Ref<DineInAreaDto[]>;
|
||||||
|
basicSettings: DineInBasicSettingsDto;
|
||||||
|
isPageLoading: Ref<boolean>;
|
||||||
|
isSavingBasic: Ref<boolean>;
|
||||||
|
isStoreLoading: Ref<boolean>;
|
||||||
|
selectedAreaId: Ref<string>;
|
||||||
|
selectedStoreId: Ref<string>;
|
||||||
|
snapshot: Ref<DineInSettingsSnapshot | null>;
|
||||||
|
stores: Ref<StoreListItemDto[]>;
|
||||||
|
tables: Ref<DineInTableDto[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDataActions(options: CreateDataActionsOptions) {
|
||||||
|
/** 同步基础设置,保持 reactive 引用不变。 */
|
||||||
|
function syncBasicSettings(next: DineInBasicSettingsDto) {
|
||||||
|
options.basicSettings.enabled = next.enabled;
|
||||||
|
options.basicSettings.defaultDiningMinutes = next.defaultDiningMinutes;
|
||||||
|
options.basicSettings.overtimeReminderMinutes =
|
||||||
|
next.overtimeReminderMinutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 应用默认配置(接口异常兜底)。 */
|
||||||
|
function applyDefaultSettings() {
|
||||||
|
options.areas.value = sortAreas(cloneAreas(DEFAULT_DINE_IN_AREAS));
|
||||||
|
options.tables.value = sortTables(cloneTables(DEFAULT_DINE_IN_TABLES));
|
||||||
|
syncBasicSettings(cloneBasicSettings(DEFAULT_DINE_IN_BASIC_SETTINGS));
|
||||||
|
options.selectedAreaId.value = options.areas.value[0]?.id ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 构建当前快照。 */
|
||||||
|
function buildCurrentSnapshot() {
|
||||||
|
return createSettingsSnapshot({
|
||||||
|
areas: options.areas.value,
|
||||||
|
tables: options.tables.value,
|
||||||
|
basicSettings: options.basicSettings,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据当前区域合法性回填选中值。 */
|
||||||
|
function fixSelectedArea() {
|
||||||
|
if (options.areas.value.length === 0) {
|
||||||
|
options.selectedAreaId.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hasSelected = options.areas.value.some(
|
||||||
|
(area) => area.id === options.selectedAreaId.value,
|
||||||
|
);
|
||||||
|
if (!hasSelected) {
|
||||||
|
options.selectedAreaId.value = options.areas.value[0]?.id ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载门店堂食设置。 */
|
||||||
|
async function loadStoreSettings(storeId: string) {
|
||||||
|
options.isPageLoading.value = true;
|
||||||
|
try {
|
||||||
|
const currentStoreId = storeId;
|
||||||
|
const result = await getStoreDineInSettingsApi(storeId);
|
||||||
|
if (options.selectedStoreId.value !== currentStoreId) return;
|
||||||
|
|
||||||
|
options.areas.value = sortAreas(
|
||||||
|
result.areas?.length > 0
|
||||||
|
? result.areas
|
||||||
|
: cloneAreas(DEFAULT_DINE_IN_AREAS),
|
||||||
|
);
|
||||||
|
options.tables.value = sortTables(
|
||||||
|
result.tables?.length > 0
|
||||||
|
? result.tables
|
||||||
|
: cloneTables(DEFAULT_DINE_IN_TABLES),
|
||||||
|
);
|
||||||
|
syncBasicSettings({
|
||||||
|
...DEFAULT_DINE_IN_BASIC_SETTINGS,
|
||||||
|
...result.basicSettings,
|
||||||
|
});
|
||||||
|
fixSelectedArea();
|
||||||
|
|
||||||
|
options.snapshot.value = buildCurrentSnapshot();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
applyDefaultSettings();
|
||||||
|
options.snapshot.value = buildCurrentSnapshot();
|
||||||
|
} finally {
|
||||||
|
options.isPageLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载门店列表。 */
|
||||||
|
async function loadStores() {
|
||||||
|
options.isStoreLoading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await getStoreListApi({
|
||||||
|
keyword: undefined,
|
||||||
|
businessStatus: undefined,
|
||||||
|
auditStatus: undefined,
|
||||||
|
serviceType: undefined,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 200,
|
||||||
|
});
|
||||||
|
options.stores.value = result.items ?? [];
|
||||||
|
|
||||||
|
if (options.stores.value.length === 0) {
|
||||||
|
options.selectedStoreId.value = '';
|
||||||
|
applyDefaultSettings();
|
||||||
|
options.snapshot.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSelectedStore = options.stores.value.some(
|
||||||
|
(item) => item.id === options.selectedStoreId.value,
|
||||||
|
);
|
||||||
|
if (!hasSelectedStore) {
|
||||||
|
const firstStore = options.stores.value[0];
|
||||||
|
if (firstStore) options.selectedStoreId.value = firstStore.id;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (options.selectedStoreId.value) {
|
||||||
|
await loadStoreSettings(options.selectedStoreId.value);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
options.stores.value = [];
|
||||||
|
options.selectedStoreId.value = '';
|
||||||
|
applyDefaultSettings();
|
||||||
|
options.snapshot.value = null;
|
||||||
|
} finally {
|
||||||
|
options.isStoreLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存基础设置。 */
|
||||||
|
async function saveBasicSettings() {
|
||||||
|
if (!options.selectedStoreId.value) return;
|
||||||
|
options.isSavingBasic.value = true;
|
||||||
|
try {
|
||||||
|
await saveStoreDineInBasicSettingsApi({
|
||||||
|
storeId: options.selectedStoreId.value,
|
||||||
|
basicSettings: cloneBasicSettings(options.basicSettings),
|
||||||
|
});
|
||||||
|
options.snapshot.value = buildCurrentSnapshot();
|
||||||
|
message.success('堂食设置已保存');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
options.isSavingBasic.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置基础设置到最近快照。 */
|
||||||
|
function resetBasicSettings() {
|
||||||
|
const source =
|
||||||
|
options.snapshot.value?.basicSettings ??
|
||||||
|
cloneBasicSettings(DEFAULT_DINE_IN_BASIC_SETTINGS);
|
||||||
|
syncBasicSettings(source);
|
||||||
|
message.success('已恢复到最近一次保存状态');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
buildCurrentSnapshot,
|
||||||
|
fixSelectedArea,
|
||||||
|
loadStoreSettings,
|
||||||
|
loadStores,
|
||||||
|
resetBasicSettings,
|
||||||
|
saveBasicSettings,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
/**
|
||||||
|
* 文件职责:堂食管理页面纯函数工具。
|
||||||
|
* 1. 负责克隆、排序、格式化、校验等纯逻辑。
|
||||||
|
* 2. 负责批量编号预览与冲突检测。
|
||||||
|
*/
|
||||||
|
import type {
|
||||||
|
DineInAreaDto,
|
||||||
|
DineInBasicSettingsDto,
|
||||||
|
DineInTableDto,
|
||||||
|
DineInTableStatus,
|
||||||
|
} from '#/api/store-dinein';
|
||||||
|
import type {
|
||||||
|
DineInAreaFormState,
|
||||||
|
DineInBatchFormState,
|
||||||
|
DineInSettingsSnapshot,
|
||||||
|
DineInTableFormState,
|
||||||
|
} from '#/views/store/dine-in/types';
|
||||||
|
|
||||||
|
/** 深拷贝堂食设置。 */
|
||||||
|
export function cloneBasicSettings(source: DineInBasicSettingsDto) {
|
||||||
|
return { ...source };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 深拷贝区域列表。 */
|
||||||
|
export function cloneAreas(source: DineInAreaDto[]) {
|
||||||
|
return source.map((item) => ({ ...item }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 深拷贝桌位列表。 */
|
||||||
|
export function cloneTables(source: DineInTableDto[]) {
|
||||||
|
return source.map((item) => ({ ...item, tags: [...item.tags] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 组装设置快照。 */
|
||||||
|
export function createSettingsSnapshot(payload: {
|
||||||
|
areas: DineInAreaDto[];
|
||||||
|
basicSettings: DineInBasicSettingsDto;
|
||||||
|
tables: DineInTableDto[];
|
||||||
|
}): DineInSettingsSnapshot {
|
||||||
|
return {
|
||||||
|
basicSettings: cloneBasicSettings(payload.basicSettings),
|
||||||
|
areas: cloneAreas(payload.areas),
|
||||||
|
tables: cloneTables(payload.tables),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按排序字段与名称稳定排序区域。 */
|
||||||
|
export function sortAreas(source: DineInAreaDto[]) {
|
||||||
|
return cloneAreas(source).toSorted((a, b) => {
|
||||||
|
const sortDiff = a.sort - b.sort;
|
||||||
|
if (sortDiff !== 0) return sortDiff;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按编号排序桌位。 */
|
||||||
|
export function sortTables(source: DineInTableDto[]) {
|
||||||
|
return cloneTables(source).toSorted((a, b) => a.code.localeCompare(b.code));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 生成唯一 ID。 */
|
||||||
|
export function createDineInId(prefix: 'area' | 'table') {
|
||||||
|
return `dinein-${prefix}-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 规范化桌位编号(大写 + 去空格)。 */
|
||||||
|
export function normalizeTableCode(code: string) {
|
||||||
|
return code.trim().toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 统计区域下桌位数量。 */
|
||||||
|
export function countAreaTables(areaId: string, tables: DineInTableDto[]) {
|
||||||
|
return tables.filter((item) => item.areaId === areaId).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 根据状态计算样式 class。 */
|
||||||
|
export function resolveStatusClassName(status: DineInTableStatus) {
|
||||||
|
return `status-${status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量生成编号预览。 */
|
||||||
|
export 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}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验区域表单。 */
|
||||||
|
export function validateAreaForm(payload: {
|
||||||
|
areaId?: string;
|
||||||
|
areas: DineInAreaDto[];
|
||||||
|
form: DineInAreaFormState;
|
||||||
|
}) {
|
||||||
|
if (!payload.form.name.trim()) return '请输入区域名称';
|
||||||
|
if (payload.form.sort <= 0) return '排序必须大于 0';
|
||||||
|
|
||||||
|
const duplicated = payload.areas.some((item) => {
|
||||||
|
if (payload.areaId && item.id === payload.areaId) return false;
|
||||||
|
return item.name.trim() === payload.form.name.trim();
|
||||||
|
});
|
||||||
|
if (duplicated) return '区域名称已存在,请更换';
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验桌位表单。 */
|
||||||
|
export function validateTableForm(payload: {
|
||||||
|
form: DineInTableFormState;
|
||||||
|
tableId?: string;
|
||||||
|
tables: DineInTableDto[];
|
||||||
|
}) {
|
||||||
|
const code = normalizeTableCode(payload.form.code);
|
||||||
|
if (!code) return '请输入桌位编号';
|
||||||
|
if (!payload.form.areaId) return '请选择所属区域';
|
||||||
|
if (payload.form.seats <= 0) return '座位数必须大于 0';
|
||||||
|
|
||||||
|
const duplicated = payload.tables.some((item) => {
|
||||||
|
if (payload.tableId && item.id === payload.tableId) return false;
|
||||||
|
return item.code === code;
|
||||||
|
});
|
||||||
|
if (duplicated) return '桌位编号已存在,请更换';
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验批量生成表单。 */
|
||||||
|
export function validateBatchForm(payload: {
|
||||||
|
existingCodes: string[];
|
||||||
|
form: DineInBatchFormState;
|
||||||
|
}) {
|
||||||
|
if (!payload.form.areaId) return '请选择所属区域';
|
||||||
|
if (!payload.form.codePrefix.trim()) return '请输入编号前缀';
|
||||||
|
if (payload.form.startNumber <= 0) return '起始编号必须大于 0';
|
||||||
|
if (payload.form.count <= 0 || payload.form.count > 50)
|
||||||
|
return '生成数量范围为 1-50';
|
||||||
|
if (payload.form.seats <= 0) return '座位数必须大于 0';
|
||||||
|
|
||||||
|
const generatedCodes = generateBatchCodes(payload.form);
|
||||||
|
const existingSet = new Set(
|
||||||
|
payload.existingCodes.map((item) => item.toUpperCase()),
|
||||||
|
);
|
||||||
|
const conflictCode = generatedCodes.find((code) => existingSet.has(code));
|
||||||
|
if (conflictCode) return `桌位编号 ${conflictCode} 已存在,请调整后重试`;
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
import type { ComputedRef, Ref } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件职责:堂食桌位动作。
|
||||||
|
* 1. 管理桌位抽屉与批量生成弹窗状态。
|
||||||
|
* 2. 处理桌位新增、编辑、删除与批量生成流程。
|
||||||
|
*/
|
||||||
|
import type {
|
||||||
|
DineInAreaDto,
|
||||||
|
DineInTableDto,
|
||||||
|
DineInTableStatus,
|
||||||
|
} from '#/api/store-dinein';
|
||||||
|
import type {
|
||||||
|
DineInBatchFormState,
|
||||||
|
DineInTableDrawerMode,
|
||||||
|
DineInTableFormState,
|
||||||
|
} from '#/views/store/dine-in/types';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
batchCreateDineInTablesApi,
|
||||||
|
deleteDineInTableApi,
|
||||||
|
saveDineInTableApi,
|
||||||
|
} from '#/api/store-dinein';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createDineInId,
|
||||||
|
generateBatchCodes,
|
||||||
|
normalizeTableCode,
|
||||||
|
sortTables,
|
||||||
|
validateBatchForm,
|
||||||
|
validateTableForm,
|
||||||
|
} from './helpers';
|
||||||
|
|
||||||
|
interface CreateTableActionsOptions {
|
||||||
|
areas: Ref<DineInAreaDto[]>;
|
||||||
|
batchForm: DineInBatchFormState;
|
||||||
|
batchPreviewCodes: ComputedRef<string[]>;
|
||||||
|
isBatchModalOpen: Ref<boolean>;
|
||||||
|
isSavingBatch: Ref<boolean>;
|
||||||
|
isSavingTable: Ref<boolean>;
|
||||||
|
isTableDrawerOpen: Ref<boolean>;
|
||||||
|
selectedStoreId: Ref<string>;
|
||||||
|
selectedTableAreaId: Ref<string>;
|
||||||
|
tableDrawerMode: Ref<DineInTableDrawerMode>;
|
||||||
|
tableForm: DineInTableFormState;
|
||||||
|
tables: Ref<DineInTableDto[]>;
|
||||||
|
updateSnapshot: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTableActions(options: CreateTableActionsOptions) {
|
||||||
|
/** 打开桌位抽屉并初始化表单。 */
|
||||||
|
function openTableDrawer(
|
||||||
|
mode: DineInTableDrawerMode,
|
||||||
|
table?: DineInTableDto,
|
||||||
|
) {
|
||||||
|
options.tableDrawerMode.value = mode;
|
||||||
|
if (mode === 'edit' && table) {
|
||||||
|
options.tableForm.id = table.id;
|
||||||
|
options.tableForm.code = table.code;
|
||||||
|
options.tableForm.areaId = table.areaId;
|
||||||
|
options.tableForm.seats = table.seats;
|
||||||
|
options.tableForm.tags = [...table.tags];
|
||||||
|
options.tableForm.sourceStatus = table.status;
|
||||||
|
options.tableForm.isDisabled = table.status === 'disabled';
|
||||||
|
options.isTableDrawerOpen.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.tableForm.id = '';
|
||||||
|
options.tableForm.code = '';
|
||||||
|
options.tableForm.areaId = options.selectedTableAreaId.value;
|
||||||
|
options.tableForm.seats = 4;
|
||||||
|
options.tableForm.tags = [];
|
||||||
|
options.tableForm.sourceStatus = 'free';
|
||||||
|
options.tableForm.isDisabled = false;
|
||||||
|
options.isTableDrawerOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 控制桌位抽屉显隐。 */
|
||||||
|
function setTableDrawerOpen(value: boolean) {
|
||||||
|
options.isTableDrawerOpen.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTableCode(value: string) {
|
||||||
|
options.tableForm.code = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTableAreaId(value: string) {
|
||||||
|
options.tableForm.areaId = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTableSeats(value: number) {
|
||||||
|
options.tableForm.seats = Math.max(1, Math.floor(Number(value || 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTableDisabled(value: boolean) {
|
||||||
|
options.tableForm.isDisabled = Boolean(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTableTags(tags: string[]) {
|
||||||
|
options.tableForm.tags = [
|
||||||
|
...new Set(tags.map((item) => item.trim()).filter(Boolean)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交桌位表单。 */
|
||||||
|
async function handleSubmitTable() {
|
||||||
|
if (!options.selectedStoreId.value) return;
|
||||||
|
const validateMessage = validateTableForm({
|
||||||
|
tableId: options.tableForm.id,
|
||||||
|
form: options.tableForm,
|
||||||
|
tables: options.tables.value,
|
||||||
|
});
|
||||||
|
if (validateMessage) {
|
||||||
|
message.error(validateMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.isSavingTable.value = true;
|
||||||
|
try {
|
||||||
|
const tableId = options.tableForm.id || createDineInId('table');
|
||||||
|
let nextStatus: DineInTableStatus = options.tableForm.sourceStatus;
|
||||||
|
if (options.tableForm.isDisabled) {
|
||||||
|
nextStatus = 'disabled';
|
||||||
|
} else if (options.tableForm.sourceStatus === 'disabled') {
|
||||||
|
nextStatus = 'free';
|
||||||
|
}
|
||||||
|
|
||||||
|
const tablePayload: DineInTableDto = {
|
||||||
|
id: tableId,
|
||||||
|
code: normalizeTableCode(options.tableForm.code),
|
||||||
|
areaId: options.tableForm.areaId,
|
||||||
|
seats: options.tableForm.seats,
|
||||||
|
status: nextStatus,
|
||||||
|
tags: [...options.tableForm.tags],
|
||||||
|
};
|
||||||
|
|
||||||
|
await saveDineInTableApi({
|
||||||
|
storeId: options.selectedStoreId.value,
|
||||||
|
table: tablePayload,
|
||||||
|
});
|
||||||
|
|
||||||
|
options.tables.value =
|
||||||
|
options.tableDrawerMode.value === 'edit' && options.tableForm.id
|
||||||
|
? sortTables(
|
||||||
|
options.tables.value.map((item) =>
|
||||||
|
item.id === options.tableForm.id ? tablePayload : item,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: sortTables([...options.tables.value, tablePayload]);
|
||||||
|
|
||||||
|
options.updateSnapshot();
|
||||||
|
options.isTableDrawerOpen.value = false;
|
||||||
|
message.success(
|
||||||
|
options.tableDrawerMode.value === 'edit' ? '桌位已保存' : '桌位已添加',
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
options.isSavingTable.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除桌位。 */
|
||||||
|
async function handleDeleteTable(tableId: string) {
|
||||||
|
if (!options.selectedStoreId.value) return;
|
||||||
|
options.isSavingTable.value = true;
|
||||||
|
try {
|
||||||
|
await deleteDineInTableApi({
|
||||||
|
storeId: options.selectedStoreId.value,
|
||||||
|
tableId,
|
||||||
|
});
|
||||||
|
options.tables.value = options.tables.value.filter(
|
||||||
|
(item) => item.id !== tableId,
|
||||||
|
);
|
||||||
|
options.updateSnapshot();
|
||||||
|
message.success('桌位已删除');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
options.isSavingTable.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打开批量生成弹窗并初始化参数。 */
|
||||||
|
function openBatchModal() {
|
||||||
|
options.batchForm.areaId = options.selectedTableAreaId.value;
|
||||||
|
options.batchForm.codePrefix = 'A';
|
||||||
|
options.batchForm.startNumber = 1;
|
||||||
|
options.batchForm.count = 4;
|
||||||
|
options.batchForm.seats = 4;
|
||||||
|
options.isBatchModalOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 控制批量弹窗显隐。 */
|
||||||
|
function setBatchModalOpen(value: boolean) {
|
||||||
|
options.isBatchModalOpen.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBatchAreaId(value: string) {
|
||||||
|
options.batchForm.areaId = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBatchCodePrefix(value: string) {
|
||||||
|
options.batchForm.codePrefix = value.trim().toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBatchStartNumber(value: number) {
|
||||||
|
options.batchForm.startNumber = Math.max(1, Math.floor(Number(value || 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBatchCount(value: number) {
|
||||||
|
options.batchForm.count = Math.max(
|
||||||
|
1,
|
||||||
|
Math.min(50, Math.floor(Number(value || 1))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBatchSeats(value: number) {
|
||||||
|
options.batchForm.seats = Math.max(1, Math.floor(Number(value || 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交批量生成。 */
|
||||||
|
async function handleSubmitBatch() {
|
||||||
|
if (!options.selectedStoreId.value) return;
|
||||||
|
|
||||||
|
const validateMessage = validateBatchForm({
|
||||||
|
form: options.batchForm,
|
||||||
|
existingCodes: options.tables.value.map((item) => item.code),
|
||||||
|
});
|
||||||
|
if (validateMessage) {
|
||||||
|
message.error(validateMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.isSavingBatch.value = true;
|
||||||
|
try {
|
||||||
|
await batchCreateDineInTablesApi({
|
||||||
|
storeId: options.selectedStoreId.value,
|
||||||
|
areaId: options.batchForm.areaId,
|
||||||
|
codePrefix: options.batchForm.codePrefix,
|
||||||
|
startNumber: options.batchForm.startNumber,
|
||||||
|
count: options.batchForm.count,
|
||||||
|
seats: options.batchForm.seats,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createdTables: DineInTableDto[] =
|
||||||
|
options.batchPreviewCodes.value.map((code) => ({
|
||||||
|
id: createDineInId('table'),
|
||||||
|
areaId: options.batchForm.areaId,
|
||||||
|
code,
|
||||||
|
seats: options.batchForm.seats,
|
||||||
|
status: 'free',
|
||||||
|
tags: [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
options.tables.value = sortTables([
|
||||||
|
...options.tables.value,
|
||||||
|
...createdTables,
|
||||||
|
]);
|
||||||
|
options.updateSnapshot();
|
||||||
|
options.isBatchModalOpen.value = false;
|
||||||
|
message.success(`已生成 ${createdTables.length} 张桌位`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
options.isSavingBatch.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
generateBatchCodes,
|
||||||
|
handleDeleteTable,
|
||||||
|
handleSubmitBatch,
|
||||||
|
handleSubmitTable,
|
||||||
|
openBatchModal,
|
||||||
|
openTableDrawer,
|
||||||
|
setBatchAreaId,
|
||||||
|
setBatchCodePrefix,
|
||||||
|
setBatchCount,
|
||||||
|
setBatchModalOpen,
|
||||||
|
setBatchSeats,
|
||||||
|
setBatchStartNumber,
|
||||||
|
setTableAreaId,
|
||||||
|
setTableCode,
|
||||||
|
setTableDisabled,
|
||||||
|
setTableDrawerOpen,
|
||||||
|
setTableSeats,
|
||||||
|
setTableTags,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
/**
|
||||||
|
* 文件职责:堂食管理页面主编排。
|
||||||
|
* 1. 维护页面级状态(门店、区域、桌位、设置、抽屉、弹窗)。
|
||||||
|
* 2. 组合数据加载、复制、区域/桌位动作。
|
||||||
|
* 3. 对外暴露视图层可直接消费的状态与方法。
|
||||||
|
*/
|
||||||
|
import type { StoreListItemDto } from '#/api/store';
|
||||||
|
import type {
|
||||||
|
DineInAreaDto,
|
||||||
|
DineInBasicSettingsDto,
|
||||||
|
DineInTableDto,
|
||||||
|
} from '#/api/store-dinein';
|
||||||
|
import type {
|
||||||
|
DineInAreaDrawerMode,
|
||||||
|
DineInAreaFormState,
|
||||||
|
DineInBatchFormState,
|
||||||
|
DineInSettingsSnapshot,
|
||||||
|
DineInTableDrawerMode,
|
||||||
|
DineInTableFormState,
|
||||||
|
} from '#/views/store/dine-in/types';
|
||||||
|
|
||||||
|
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { createAreaActions } from './dinein-page/area-actions';
|
||||||
|
import {
|
||||||
|
DEFAULT_DINE_IN_AREAS,
|
||||||
|
DEFAULT_DINE_IN_BASIC_SETTINGS,
|
||||||
|
DEFAULT_DINE_IN_TABLES,
|
||||||
|
DINE_IN_SEATS_OPTIONS,
|
||||||
|
DINE_IN_STATUS_MAP,
|
||||||
|
TABLE_TAG_SUGGESTIONS,
|
||||||
|
} from './dinein-page/constants';
|
||||||
|
import { createCopyActions } from './dinein-page/copy-actions';
|
||||||
|
import { createDataActions } from './dinein-page/data-actions';
|
||||||
|
import {
|
||||||
|
cloneAreas,
|
||||||
|
cloneBasicSettings,
|
||||||
|
cloneTables,
|
||||||
|
countAreaTables,
|
||||||
|
createSettingsSnapshot,
|
||||||
|
generateBatchCodes,
|
||||||
|
resolveStatusClassName,
|
||||||
|
sortAreas,
|
||||||
|
sortTables,
|
||||||
|
} from './dinein-page/helpers';
|
||||||
|
import { createTableActions } from './dinein-page/table-actions';
|
||||||
|
|
||||||
|
export function useStoreDineInPage() {
|
||||||
|
// 1. 页面 loading / submitting 状态。
|
||||||
|
const isStoreLoading = ref(false);
|
||||||
|
const isPageLoading = ref(false);
|
||||||
|
const isSavingBasic = ref(false);
|
||||||
|
const isSavingArea = ref(false);
|
||||||
|
const isSavingTable = ref(false);
|
||||||
|
const isSavingBatch = ref(false);
|
||||||
|
const isCopySubmitting = ref(false);
|
||||||
|
|
||||||
|
// 2. 页面核心业务数据。
|
||||||
|
const stores = ref<StoreListItemDto[]>([]);
|
||||||
|
const selectedStoreId = ref('');
|
||||||
|
const areas = ref<DineInAreaDto[]>(
|
||||||
|
sortAreas(cloneAreas(DEFAULT_DINE_IN_AREAS)),
|
||||||
|
);
|
||||||
|
const tables = ref<DineInTableDto[]>(
|
||||||
|
sortTables(cloneTables(DEFAULT_DINE_IN_TABLES)),
|
||||||
|
);
|
||||||
|
const basicSettings = reactive<DineInBasicSettingsDto>(
|
||||||
|
cloneBasicSettings(DEFAULT_DINE_IN_BASIC_SETTINGS),
|
||||||
|
);
|
||||||
|
const selectedAreaId = ref(areas.value[0]?.id ?? '');
|
||||||
|
const snapshot = ref<DineInSettingsSnapshot | null>(
|
||||||
|
createSettingsSnapshot({
|
||||||
|
areas: areas.value,
|
||||||
|
tables: tables.value,
|
||||||
|
basicSettings,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. 复制弹窗状态。
|
||||||
|
const isCopyModalOpen = ref(false);
|
||||||
|
const copyTargetStoreIds = ref<string[]>([]);
|
||||||
|
|
||||||
|
// 4. 区域抽屉状态。
|
||||||
|
const isAreaDrawerOpen = ref(false);
|
||||||
|
const areaDrawerMode = ref<DineInAreaDrawerMode>('create');
|
||||||
|
const areaForm = reactive<DineInAreaFormState>({
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
sort: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. 桌位抽屉与批量弹窗状态。
|
||||||
|
const isTableDrawerOpen = ref(false);
|
||||||
|
const tableDrawerMode = ref<DineInTableDrawerMode>('create');
|
||||||
|
const tableForm = reactive<DineInTableFormState>({
|
||||||
|
id: '',
|
||||||
|
code: '',
|
||||||
|
areaId: selectedAreaId.value,
|
||||||
|
seats: 4,
|
||||||
|
tags: [],
|
||||||
|
sourceStatus: 'free',
|
||||||
|
isDisabled: false,
|
||||||
|
});
|
||||||
|
const isBatchModalOpen = ref(false);
|
||||||
|
const batchForm = reactive<DineInBatchFormState>({
|
||||||
|
areaId: selectedAreaId.value,
|
||||||
|
codePrefix: 'A',
|
||||||
|
startNumber: 1,
|
||||||
|
count: 4,
|
||||||
|
seats: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. 页面衍生视图数据。
|
||||||
|
const storeOptions = computed(() =>
|
||||||
|
stores.value.map((store) => ({ label: store.name, value: store.id })),
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedStoreName = computed(
|
||||||
|
() =>
|
||||||
|
stores.value.find((store) => store.id === selectedStoreId.value)?.name ??
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedArea = computed(
|
||||||
|
() =>
|
||||||
|
areas.value.find((area) => area.id === selectedAreaId.value) ??
|
||||||
|
areas.value[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedAreaTableCount = computed(() => {
|
||||||
|
const area = selectedArea.value;
|
||||||
|
if (!area) return 0;
|
||||||
|
return countAreaTables(area.id, tables.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredTables = computed(() => {
|
||||||
|
const area = selectedArea.value;
|
||||||
|
if (!area) return [];
|
||||||
|
return tables.value.filter((table) => table.areaId === area.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
const areaOptions = computed(() =>
|
||||||
|
areas.value.map((area) => ({ label: area.name, value: area.id })),
|
||||||
|
);
|
||||||
|
|
||||||
|
const copyCandidates = computed(() =>
|
||||||
|
stores.value.filter((store) => store.id !== selectedStoreId.value),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isCopyAllChecked = computed(
|
||||||
|
() =>
|
||||||
|
copyCandidates.value.length > 0 &&
|
||||||
|
copyTargetStoreIds.value.length === copyCandidates.value.length,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isCopyIndeterminate = computed(
|
||||||
|
() =>
|
||||||
|
copyTargetStoreIds.value.length > 0 &&
|
||||||
|
copyTargetStoreIds.value.length < copyCandidates.value.length,
|
||||||
|
);
|
||||||
|
|
||||||
|
const batchPreviewCodes = computed(() => generateBatchCodes(batchForm));
|
||||||
|
|
||||||
|
const areaDrawerTitle = computed(() =>
|
||||||
|
areaDrawerMode.value === 'edit' ? '编辑区域' : '添加区域',
|
||||||
|
);
|
||||||
|
|
||||||
|
const areaSubmitText = computed(() =>
|
||||||
|
areaDrawerMode.value === 'edit' ? '保存修改' : '确认添加',
|
||||||
|
);
|
||||||
|
|
||||||
|
const tableDrawerTitle = computed(() =>
|
||||||
|
tableDrawerMode.value === 'edit'
|
||||||
|
? `编辑桌位 - ${tableForm.code || '--'}`
|
||||||
|
: '添加桌位',
|
||||||
|
);
|
||||||
|
|
||||||
|
const tableSubmitText = computed(() =>
|
||||||
|
tableDrawerMode.value === 'edit' ? '保存修改' : '确认添加',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 7. 数据域动作装配。
|
||||||
|
const {
|
||||||
|
buildCurrentSnapshot,
|
||||||
|
fixSelectedArea,
|
||||||
|
loadStoreSettings,
|
||||||
|
loadStores,
|
||||||
|
resetBasicSettings,
|
||||||
|
saveBasicSettings,
|
||||||
|
} = createDataActions({
|
||||||
|
areas,
|
||||||
|
basicSettings,
|
||||||
|
isPageLoading,
|
||||||
|
isSavingBasic,
|
||||||
|
isStoreLoading,
|
||||||
|
selectedAreaId,
|
||||||
|
selectedStoreId,
|
||||||
|
snapshot,
|
||||||
|
stores,
|
||||||
|
tables,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleCopyCheckAll,
|
||||||
|
handleCopySubmit,
|
||||||
|
openCopyModal,
|
||||||
|
toggleCopyStore,
|
||||||
|
} = createCopyActions({
|
||||||
|
copyCandidates,
|
||||||
|
copyTargetStoreIds,
|
||||||
|
isCopyModalOpen,
|
||||||
|
isCopySubmitting,
|
||||||
|
selectedStoreId,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 更新快照供重置使用。 */
|
||||||
|
function updateSnapshot() {
|
||||||
|
snapshot.value = buildCurrentSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleDeleteArea,
|
||||||
|
handleSubmitArea,
|
||||||
|
openAreaDrawer,
|
||||||
|
setAreaDescription,
|
||||||
|
setAreaDrawerOpen,
|
||||||
|
setAreaName,
|
||||||
|
setAreaSort,
|
||||||
|
} = createAreaActions({
|
||||||
|
areaDrawerMode,
|
||||||
|
areaForm,
|
||||||
|
areas,
|
||||||
|
fixSelectedArea,
|
||||||
|
isAreaDrawerOpen,
|
||||||
|
isSavingArea,
|
||||||
|
selectedAreaId,
|
||||||
|
selectedStoreId,
|
||||||
|
tables,
|
||||||
|
updateSnapshot,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleDeleteTable,
|
||||||
|
handleSubmitBatch,
|
||||||
|
handleSubmitTable,
|
||||||
|
openBatchModal,
|
||||||
|
openTableDrawer,
|
||||||
|
setBatchAreaId,
|
||||||
|
setBatchCodePrefix,
|
||||||
|
setBatchCount,
|
||||||
|
setBatchModalOpen,
|
||||||
|
setBatchSeats,
|
||||||
|
setBatchStartNumber,
|
||||||
|
setTableAreaId,
|
||||||
|
setTableCode,
|
||||||
|
setTableDisabled,
|
||||||
|
setTableDrawerOpen,
|
||||||
|
setTableSeats,
|
||||||
|
setTableTags,
|
||||||
|
} = createTableActions({
|
||||||
|
areas,
|
||||||
|
batchForm,
|
||||||
|
batchPreviewCodes,
|
||||||
|
isBatchModalOpen,
|
||||||
|
isSavingBatch,
|
||||||
|
isSavingTable,
|
||||||
|
isTableDrawerOpen,
|
||||||
|
selectedStoreId,
|
||||||
|
selectedTableAreaId: selectedAreaId,
|
||||||
|
tableDrawerMode,
|
||||||
|
tableForm,
|
||||||
|
tables,
|
||||||
|
updateSnapshot,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 8. 页面字段更新方法。
|
||||||
|
function setSelectedStoreId(value: string) {
|
||||||
|
selectedStoreId.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectedAreaId(value: string) {
|
||||||
|
selectedAreaId.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDineInEnabled(value: boolean) {
|
||||||
|
basicSettings.enabled = Boolean(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDefaultDiningMinutes(value: number) {
|
||||||
|
basicSettings.defaultDiningMinutes = Math.max(
|
||||||
|
1,
|
||||||
|
Math.floor(Number(value || 1)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOvertimeReminderMinutes(value: number) {
|
||||||
|
basicSettings.overtimeReminderMinutes = Math.max(
|
||||||
|
0,
|
||||||
|
Math.floor(Number(value || 0)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. 门店切换时自动刷新配置。
|
||||||
|
watch(selectedStoreId, async (storeId) => {
|
||||||
|
if (!storeId) {
|
||||||
|
areas.value = sortAreas(cloneAreas(DEFAULT_DINE_IN_AREAS));
|
||||||
|
tables.value = sortTables(cloneTables(DEFAULT_DINE_IN_TABLES));
|
||||||
|
basicSettings.enabled = DEFAULT_DINE_IN_BASIC_SETTINGS.enabled;
|
||||||
|
basicSettings.defaultDiningMinutes =
|
||||||
|
DEFAULT_DINE_IN_BASIC_SETTINGS.defaultDiningMinutes;
|
||||||
|
basicSettings.overtimeReminderMinutes =
|
||||||
|
DEFAULT_DINE_IN_BASIC_SETTINGS.overtimeReminderMinutes;
|
||||||
|
selectedAreaId.value = areas.value[0]?.id ?? '';
|
||||||
|
snapshot.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await loadStoreSettings(storeId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 10. 页面首屏初始化。
|
||||||
|
onMounted(loadStores);
|
||||||
|
|
||||||
|
return {
|
||||||
|
DINE_IN_SEATS_OPTIONS,
|
||||||
|
DINE_IN_STATUS_MAP,
|
||||||
|
TABLE_TAG_SUGGESTIONS,
|
||||||
|
areaDrawerTitle,
|
||||||
|
areaForm,
|
||||||
|
areaOptions,
|
||||||
|
areaSubmitText,
|
||||||
|
areas,
|
||||||
|
basicSettings,
|
||||||
|
batchForm,
|
||||||
|
batchPreviewCodes,
|
||||||
|
copyCandidates,
|
||||||
|
copyTargetStoreIds,
|
||||||
|
filteredTables,
|
||||||
|
handleCopyCheckAll,
|
||||||
|
handleCopySubmit,
|
||||||
|
handleDeleteArea,
|
||||||
|
handleDeleteTable,
|
||||||
|
handleSubmitArea,
|
||||||
|
handleSubmitBatch,
|
||||||
|
handleSubmitTable,
|
||||||
|
isAreaDrawerOpen,
|
||||||
|
isBatchModalOpen,
|
||||||
|
isCopyAllChecked,
|
||||||
|
isCopyIndeterminate,
|
||||||
|
isCopyModalOpen,
|
||||||
|
isCopySubmitting,
|
||||||
|
isPageLoading,
|
||||||
|
isSavingArea,
|
||||||
|
isSavingBasic,
|
||||||
|
isSavingBatch,
|
||||||
|
isSavingTable,
|
||||||
|
isStoreLoading,
|
||||||
|
isTableDrawerOpen,
|
||||||
|
openAreaDrawer,
|
||||||
|
openBatchModal,
|
||||||
|
openCopyModal,
|
||||||
|
openTableDrawer,
|
||||||
|
resetBasicSettings,
|
||||||
|
resolveStatusClassName,
|
||||||
|
saveBasicSettings,
|
||||||
|
selectedArea,
|
||||||
|
selectedAreaId,
|
||||||
|
selectedAreaTableCount,
|
||||||
|
selectedStoreId,
|
||||||
|
selectedStoreName,
|
||||||
|
setAreaDescription,
|
||||||
|
setAreaDrawerOpen,
|
||||||
|
setAreaName,
|
||||||
|
setAreaSort,
|
||||||
|
setBatchAreaId,
|
||||||
|
setBatchCodePrefix,
|
||||||
|
setBatchCount,
|
||||||
|
setBatchModalOpen,
|
||||||
|
setBatchSeats,
|
||||||
|
setBatchStartNumber,
|
||||||
|
setDefaultDiningMinutes,
|
||||||
|
setDineInEnabled,
|
||||||
|
setOvertimeReminderMinutes,
|
||||||
|
setSelectedAreaId,
|
||||||
|
setSelectedStoreId,
|
||||||
|
setTableAreaId,
|
||||||
|
setTableCode,
|
||||||
|
setTableDisabled,
|
||||||
|
setTableDrawerOpen,
|
||||||
|
setTableSeats,
|
||||||
|
setTableTags,
|
||||||
|
storeOptions,
|
||||||
|
tableDrawerTitle,
|
||||||
|
tableForm,
|
||||||
|
tableSubmitText,
|
||||||
|
tables,
|
||||||
|
toggleCopyStore,
|
||||||
|
};
|
||||||
|
}
|
||||||
246
apps/web-antd/src/views/store/dine-in/index.vue
Normal file
246
apps/web-antd/src/views/store/dine-in/index.vue
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DineInAreaDto } from '#/api/store-dinein';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件职责:堂食管理页面主视图。
|
||||||
|
* 1. 组合区域、桌位、堂食设置与抽屉/弹窗子组件。
|
||||||
|
* 2. 承接门店维度切换与复制弹窗。
|
||||||
|
*/
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Card, Empty, message, Spin } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import CopyToStoresModal from '../components/CopyToStoresModal.vue';
|
||||||
|
import StoreScopeToolbar from '../components/StoreScopeToolbar.vue';
|
||||||
|
import DineInAreaDrawer from './components/DineInAreaDrawer.vue';
|
||||||
|
import DineInAreaSection from './components/DineInAreaSection.vue';
|
||||||
|
import DineInBasicSettingsCard from './components/DineInBasicSettingsCard.vue';
|
||||||
|
import DineInBatchModal from './components/DineInBatchModal.vue';
|
||||||
|
import DineInTableDrawer from './components/DineInTableDrawer.vue';
|
||||||
|
import DineInTableGridSection from './components/DineInTableGridSection.vue';
|
||||||
|
import { useStoreDineInPage } from './composables/useStoreDineInPage';
|
||||||
|
|
||||||
|
const {
|
||||||
|
DINE_IN_SEATS_OPTIONS,
|
||||||
|
DINE_IN_STATUS_MAP,
|
||||||
|
TABLE_TAG_SUGGESTIONS,
|
||||||
|
areaDrawerTitle,
|
||||||
|
areaForm,
|
||||||
|
areaOptions,
|
||||||
|
areaSubmitText,
|
||||||
|
areas,
|
||||||
|
basicSettings,
|
||||||
|
batchForm,
|
||||||
|
batchPreviewCodes,
|
||||||
|
copyCandidates,
|
||||||
|
copyTargetStoreIds,
|
||||||
|
filteredTables,
|
||||||
|
handleCopyCheckAll,
|
||||||
|
handleCopySubmit,
|
||||||
|
handleDeleteArea,
|
||||||
|
handleDeleteTable,
|
||||||
|
handleSubmitArea,
|
||||||
|
handleSubmitBatch,
|
||||||
|
handleSubmitTable,
|
||||||
|
isAreaDrawerOpen,
|
||||||
|
isBatchModalOpen,
|
||||||
|
isCopyAllChecked,
|
||||||
|
isCopyIndeterminate,
|
||||||
|
isCopyModalOpen,
|
||||||
|
isCopySubmitting,
|
||||||
|
isPageLoading,
|
||||||
|
isSavingArea,
|
||||||
|
isSavingBasic,
|
||||||
|
isSavingBatch,
|
||||||
|
isSavingTable,
|
||||||
|
isStoreLoading,
|
||||||
|
isTableDrawerOpen,
|
||||||
|
openAreaDrawer,
|
||||||
|
openBatchModal,
|
||||||
|
openCopyModal,
|
||||||
|
openTableDrawer,
|
||||||
|
resetBasicSettings,
|
||||||
|
resolveStatusClassName,
|
||||||
|
saveBasicSettings,
|
||||||
|
selectedArea,
|
||||||
|
selectedAreaId,
|
||||||
|
selectedStoreId,
|
||||||
|
selectedStoreName,
|
||||||
|
setAreaDescription,
|
||||||
|
setAreaDrawerOpen,
|
||||||
|
setAreaName,
|
||||||
|
setAreaSort,
|
||||||
|
setBatchAreaId,
|
||||||
|
setBatchCodePrefix,
|
||||||
|
setBatchCount,
|
||||||
|
setBatchModalOpen,
|
||||||
|
setBatchSeats,
|
||||||
|
setBatchStartNumber,
|
||||||
|
setDefaultDiningMinutes,
|
||||||
|
setDineInEnabled,
|
||||||
|
setOvertimeReminderMinutes,
|
||||||
|
setSelectedAreaId,
|
||||||
|
setSelectedStoreId,
|
||||||
|
setTableAreaId,
|
||||||
|
setTableCode,
|
||||||
|
setTableDisabled,
|
||||||
|
setTableDrawerOpen,
|
||||||
|
setTableSeats,
|
||||||
|
setTableTags,
|
||||||
|
storeOptions,
|
||||||
|
tableDrawerTitle,
|
||||||
|
tableForm,
|
||||||
|
tableSubmitText,
|
||||||
|
tables,
|
||||||
|
toggleCopyStore,
|
||||||
|
} = useStoreDineInPage();
|
||||||
|
|
||||||
|
/** 统计指定区域桌位数。 */
|
||||||
|
function getAreaTableCount(areaId: string) {
|
||||||
|
return tables.value.filter((item) => item.areaId === areaId).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 桌位状态文案映射。 */
|
||||||
|
const tableStatusLabelMap = {
|
||||||
|
free: DINE_IN_STATUS_MAP.free.label,
|
||||||
|
dining: DINE_IN_STATUS_MAP.dining.label,
|
||||||
|
reserved: DINE_IN_STATUS_MAP.reserved.label,
|
||||||
|
disabled: DINE_IN_STATUS_MAP.disabled.label,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 当前区域删除处理。 */
|
||||||
|
function onDeleteSelectedArea(area: DineInAreaDto) {
|
||||||
|
handleDeleteArea(area);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 二维码按钮占位处理。 */
|
||||||
|
function onViewQrCode(tableCode: string) {
|
||||||
|
message.info(`桌位 ${tableCode} 二维码功能待接入`);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page title="堂食管理" content-class="space-y-4 page-store-dinein">
|
||||||
|
<StoreScopeToolbar
|
||||||
|
:selected-store-id="selectedStoreId"
|
||||||
|
:store-options="storeOptions"
|
||||||
|
:is-store-loading="isStoreLoading"
|
||||||
|
:copy-disabled="!selectedStoreId || copyCandidates.length === 0"
|
||||||
|
@update:selected-store-id="setSelectedStoreId"
|
||||||
|
@copy="openCopyModal"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template v-if="storeOptions.length === 0">
|
||||||
|
<Card :bordered="false">
|
||||||
|
<Empty description="暂无门店,请先创建门店" />
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<Spin :spinning="isPageLoading">
|
||||||
|
<DineInAreaSection
|
||||||
|
:areas="areas"
|
||||||
|
:selected-area-id="selectedAreaId"
|
||||||
|
:selected-area="selectedArea"
|
||||||
|
:is-saving="isSavingArea"
|
||||||
|
:get-area-table-count="getAreaTableCount"
|
||||||
|
@add="openAreaDrawer('create')"
|
||||||
|
@edit="(area) => openAreaDrawer('edit', area)"
|
||||||
|
@delete="onDeleteSelectedArea"
|
||||||
|
@select-area="setSelectedAreaId"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DineInTableGridSection
|
||||||
|
:tables="filteredTables"
|
||||||
|
:is-saving="isSavingTable"
|
||||||
|
:status-map="DINE_IN_STATUS_MAP"
|
||||||
|
:resolve-status-class-name="resolveStatusClassName"
|
||||||
|
@add="openTableDrawer('create')"
|
||||||
|
@batch="openBatchModal"
|
||||||
|
@edit="(table) => openTableDrawer('edit', table)"
|
||||||
|
@delete="handleDeleteTable"
|
||||||
|
@qrcode="(table) => onViewQrCode(table.code)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DineInBasicSettingsCard
|
||||||
|
:settings="basicSettings"
|
||||||
|
:is-saving="isSavingBasic"
|
||||||
|
:on-set-enabled="setDineInEnabled"
|
||||||
|
:on-set-default-dining-minutes="setDefaultDiningMinutes"
|
||||||
|
:on-set-overtime-reminder-minutes="setOvertimeReminderMinutes"
|
||||||
|
@save="saveBasicSettings"
|
||||||
|
@reset="resetBasicSettings"
|
||||||
|
/>
|
||||||
|
</Spin>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<DineInAreaDrawer
|
||||||
|
:open="isAreaDrawerOpen"
|
||||||
|
:title="areaDrawerTitle"
|
||||||
|
:submit-text="areaSubmitText"
|
||||||
|
:form="areaForm"
|
||||||
|
:is-saving="isSavingArea"
|
||||||
|
:on-set-name="setAreaName"
|
||||||
|
:on-set-description="setAreaDescription"
|
||||||
|
:on-set-sort="setAreaSort"
|
||||||
|
@update:open="setAreaDrawerOpen"
|
||||||
|
@submit="handleSubmitArea"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DineInTableDrawer
|
||||||
|
:open="isTableDrawerOpen"
|
||||||
|
:title="tableDrawerTitle"
|
||||||
|
:submit-text="tableSubmitText"
|
||||||
|
:form="tableForm"
|
||||||
|
:is-saving="isSavingTable"
|
||||||
|
:area-options="areaOptions"
|
||||||
|
:seats-options="DINE_IN_SEATS_OPTIONS"
|
||||||
|
:status-label-map="tableStatusLabelMap"
|
||||||
|
:tag-suggestions="TABLE_TAG_SUGGESTIONS"
|
||||||
|
:on-set-code="setTableCode"
|
||||||
|
:on-set-area-id="setTableAreaId"
|
||||||
|
:on-set-seats="setTableSeats"
|
||||||
|
:on-set-disabled="setTableDisabled"
|
||||||
|
:on-set-tags="setTableTags"
|
||||||
|
@update:open="setTableDrawerOpen"
|
||||||
|
@submit="handleSubmitTable"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DineInBatchModal
|
||||||
|
:open="isBatchModalOpen"
|
||||||
|
:form="batchForm"
|
||||||
|
:is-saving="isSavingBatch"
|
||||||
|
:area-options="areaOptions"
|
||||||
|
:seats-options="DINE_IN_SEATS_OPTIONS"
|
||||||
|
:preview-codes="batchPreviewCodes"
|
||||||
|
:on-set-area-id="setBatchAreaId"
|
||||||
|
:on-set-code-prefix="setBatchCodePrefix"
|
||||||
|
:on-set-start-number="setBatchStartNumber"
|
||||||
|
:on-set-count="setBatchCount"
|
||||||
|
:on-set-seats="setBatchSeats"
|
||||||
|
@update:open="setBatchModalOpen"
|
||||||
|
@submit="handleSubmitBatch"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CopyToStoresModal
|
||||||
|
v-model:open="isCopyModalOpen"
|
||||||
|
title="复制堂食设置到其他门店"
|
||||||
|
confirm-text="确认复制"
|
||||||
|
:copy-candidates="copyCandidates"
|
||||||
|
:target-store-ids="copyTargetStoreIds"
|
||||||
|
:is-all-checked="isCopyAllChecked"
|
||||||
|
:is-indeterminate="isCopyIndeterminate"
|
||||||
|
:is-submitting="isCopySubmitting"
|
||||||
|
:selected-store-name="selectedStoreName"
|
||||||
|
@check-all="handleCopyCheckAll"
|
||||||
|
@submit="handleCopySubmit"
|
||||||
|
@toggle-store="
|
||||||
|
({ storeId, checked }) => toggleCopyStore(storeId, checked)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
@import './styles/index.less';
|
||||||
|
</style>
|
||||||
54
apps/web-antd/src/views/store/dine-in/styles/area.less
Normal file
54
apps/web-antd/src/views/store/dine-in/styles/area.less
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/* 文件职责:堂食管理区域区块样式。 */
|
||||||
|
.page-store-dinein {
|
||||||
|
.dinein-area-pills {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-area-pill {
|
||||||
|
padding: 6px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #4b5563;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 20px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-area-pill:hover {
|
||||||
|
color: #1677ff;
|
||||||
|
border-color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-area-pill.active {
|
||||||
|
color: #fff;
|
||||||
|
background: #1677ff;
|
||||||
|
border-color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-area-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #f8f9fb;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-area-description {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-area-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
apps/web-antd/src/views/store/dine-in/styles/base.less
Normal file
40
apps/web-antd/src/views/store/dine-in/styles/base.less
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/* 文件职责:堂食管理页面基础骨架与通用样式。 */
|
||||||
|
.page-store-dinein {
|
||||||
|
max-width: 980px;
|
||||||
|
|
||||||
|
.dinein-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 3px rgb(15 23 42 / 8%);
|
||||||
|
|
||||||
|
.ant-card-head {
|
||||||
|
min-height: 52px;
|
||||||
|
padding: 0 18px;
|
||||||
|
background: #f8f9fb;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-head-title {
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-extra {
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 16px 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-number-input {
|
||||||
|
width: 92px;
|
||||||
|
}
|
||||||
|
}
|
||||||
123
apps/web-antd/src/views/store/dine-in/styles/drawer.less
Normal file
123
apps/web-antd/src/views/store/dine-in/styles/drawer.less
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/* 文件职责:堂食管理抽屉与批量弹窗样式。 */
|
||||||
|
.dinein-area-drawer-wrap,
|
||||||
|
.dinein-table-drawer-wrap {
|
||||||
|
.ant-drawer-body {
|
||||||
|
padding: 16px 20px 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-drawer-footer {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-form-block {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-form-label.required::before {
|
||||||
|
margin-right: 4px;
|
||||||
|
color: #ef4444;
|
||||||
|
content: '*';
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-input-with-unit {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-input {
|
||||||
|
width: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-form-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-switch-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-status-preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-batch-modal-wrap {
|
||||||
|
.ant-modal-content {
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-body {
|
||||||
|
padding-top: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-form-item.full {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-form-item label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-input-number {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-preview-wrap {
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
background: #f8f9fb;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-preview-title {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-preview-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
7
apps/web-antd/src/views/store/dine-in/styles/index.less
Normal file
7
apps/web-antd/src/views/store/dine-in/styles/index.less
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/* 文件职责:堂食管理页面样式聚合入口(仅负责分片导入)。 */
|
||||||
|
@import './base.less';
|
||||||
|
@import './area.less';
|
||||||
|
@import './table.less';
|
||||||
|
@import './settings.less';
|
||||||
|
@import './drawer.less';
|
||||||
|
@import './responsive.less';
|
||||||
44
apps/web-antd/src/views/store/dine-in/styles/responsive.less
Normal file
44
apps/web-antd/src/views/store/dine-in/styles/responsive.less
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/* 文件职责:堂食管理页面响应式规则。 */
|
||||||
|
.page-store-dinein {
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.dinein-table-grid {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.dinein-area-info {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-table-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-form-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-form-label {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.drawer-form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-form-item.full {
|
||||||
|
grid-column: span 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
apps/web-antd/src/views/store/dine-in/styles/settings.less
Normal file
50
apps/web-antd/src/views/store/dine-in/styles/settings.less
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/* 文件职责:堂食管理基础设置区块样式。 */
|
||||||
|
.page-store-dinein {
|
||||||
|
.dinein-form-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-form-row:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-form-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 120px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-form-control {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-form-unit {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-form-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
border-top: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
109
apps/web-antd/src/views/store/dine-in/styles/table.less
Normal file
109
apps/web-antd/src/views/store/dine-in/styles/table.less
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/* 文件职责:堂食管理桌位区块样式。 */
|
||||||
|
.page-store-dinein {
|
||||||
|
.dinein-table-header-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-table-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-table-card {
|
||||||
|
padding: 14px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 3px rgb(15 23 42 / 8%);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-table-card:hover {
|
||||||
|
box-shadow: 0 6px 20px rgb(15 23 42 / 10%);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-table-card.disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-table-code {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-table-seat {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-table-status {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 5px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-table-status .status-dot {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-table-status.status-free {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-table-status.status-free .status-dot {
|
||||||
|
background: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-table-status.status-dining {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-table-status.status-dining .status-dot {
|
||||||
|
background: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-table-status.status-reserved {
|
||||||
|
color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-table-status.status-reserved .status-dot {
|
||||||
|
background: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-table-status.status-disabled {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-table-status.status-disabled .status-dot {
|
||||||
|
background: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-table-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
min-height: 24px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dinein-table-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
57
apps/web-antd/src/views/store/dine-in/types.ts
Normal file
57
apps/web-antd/src/views/store/dine-in/types.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* 文件职责:堂食管理页面类型定义。
|
||||||
|
* 1. 声明区域、桌位、批量生成表单态。
|
||||||
|
* 2. 声明页面快照与选择项类型。
|
||||||
|
*/
|
||||||
|
import type {
|
||||||
|
DineInAreaDto,
|
||||||
|
DineInBasicSettingsDto,
|
||||||
|
DineInTableDto,
|
||||||
|
DineInTableStatus,
|
||||||
|
} from '#/api/store-dinein';
|
||||||
|
|
||||||
|
export type DineInAreaDrawerMode = 'create' | 'edit';
|
||||||
|
export type DineInTableDrawerMode = 'create' | 'edit';
|
||||||
|
|
||||||
|
export interface DineInAreaFormState {
|
||||||
|
description: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
sort: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DineInTableFormState {
|
||||||
|
areaId: string;
|
||||||
|
code: string;
|
||||||
|
id: string;
|
||||||
|
isDisabled: boolean;
|
||||||
|
seats: number;
|
||||||
|
sourceStatus: DineInTableStatus;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DineInBatchFormState {
|
||||||
|
areaId: string;
|
||||||
|
codePrefix: string;
|
||||||
|
count: number;
|
||||||
|
seats: number;
|
||||||
|
startNumber: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DineInSettingsSnapshot {
|
||||||
|
areas: DineInAreaDto[];
|
||||||
|
basicSettings: DineInBasicSettingsDto;
|
||||||
|
tables: DineInTableDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DineInSeatsOption {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DineInStatusOption {
|
||||||
|
className: string;
|
||||||
|
color: string;
|
||||||
|
label: string;
|
||||||
|
value: DineInTableStatus;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user