From 2aceb8b662dc6931630a1e5d961d5eecc93bd913 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Mon, 16 Feb 2026 15:29:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=A0=82=E9=A3=9F?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E9=A1=B5=E9=9D=A2=E5=B9=B6=E5=AF=B9=E9=BD=90?= =?UTF-8?q?=E5=90=8E=E7=AB=AFdine-in=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-antd/src/api/store-dinein/index.ts | 152 ++++++ apps/web-antd/src/mock/index.ts | 1 + apps/web-antd/src/mock/store-dinein.ts | 515 ++++++++++++++++++ .../src/router/routes/modules/store.ts | 9 + .../dine-in/components/DineInAreaDrawer.vue | 98 ++++ .../dine-in/components/DineInAreaSection.vue | 81 +++ .../components/DineInBasicSettingsCard.vue | 90 +++ .../dine-in/components/DineInBatchModal.vue | 136 +++++ .../dine-in/components/DineInTableDrawer.vue | 154 ++++++ .../components/DineInTableGridSection.vue | 94 ++++ .../composables/dinein-page/area-actions.ts | 170 ++++++ .../composables/dinein-page/constants.ts | 159 ++++++ .../composables/dinein-page/copy-actions.ts | 76 +++ .../composables/dinein-page/data-actions.ts | 203 +++++++ .../composables/dinein-page/helpers.ts | 156 ++++++ .../composables/dinein-page/table-actions.ts | 293 ++++++++++ .../dine-in/composables/useStoreDineInPage.ts | 399 ++++++++++++++ .../src/views/store/dine-in/index.vue | 246 +++++++++ .../src/views/store/dine-in/styles/area.less | 54 ++ .../src/views/store/dine-in/styles/base.less | 40 ++ .../views/store/dine-in/styles/drawer.less | 123 +++++ .../src/views/store/dine-in/styles/index.less | 7 + .../store/dine-in/styles/responsive.less | 44 ++ .../views/store/dine-in/styles/settings.less | 50 ++ .../src/views/store/dine-in/styles/table.less | 109 ++++ .../web-antd/src/views/store/dine-in/types.ts | 57 ++ 26 files changed, 3516 insertions(+) create mode 100644 apps/web-antd/src/api/store-dinein/index.ts create mode 100644 apps/web-antd/src/mock/store-dinein.ts create mode 100644 apps/web-antd/src/views/store/dine-in/components/DineInAreaDrawer.vue create mode 100644 apps/web-antd/src/views/store/dine-in/components/DineInAreaSection.vue create mode 100644 apps/web-antd/src/views/store/dine-in/components/DineInBasicSettingsCard.vue create mode 100644 apps/web-antd/src/views/store/dine-in/components/DineInBatchModal.vue create mode 100644 apps/web-antd/src/views/store/dine-in/components/DineInTableDrawer.vue create mode 100644 apps/web-antd/src/views/store/dine-in/components/DineInTableGridSection.vue create mode 100644 apps/web-antd/src/views/store/dine-in/composables/dinein-page/area-actions.ts create mode 100644 apps/web-antd/src/views/store/dine-in/composables/dinein-page/constants.ts create mode 100644 apps/web-antd/src/views/store/dine-in/composables/dinein-page/copy-actions.ts create mode 100644 apps/web-antd/src/views/store/dine-in/composables/dinein-page/data-actions.ts create mode 100644 apps/web-antd/src/views/store/dine-in/composables/dinein-page/helpers.ts create mode 100644 apps/web-antd/src/views/store/dine-in/composables/dinein-page/table-actions.ts create mode 100644 apps/web-antd/src/views/store/dine-in/composables/useStoreDineInPage.ts create mode 100644 apps/web-antd/src/views/store/dine-in/index.vue create mode 100644 apps/web-antd/src/views/store/dine-in/styles/area.less create mode 100644 apps/web-antd/src/views/store/dine-in/styles/base.less create mode 100644 apps/web-antd/src/views/store/dine-in/styles/drawer.less create mode 100644 apps/web-antd/src/views/store/dine-in/styles/index.less create mode 100644 apps/web-antd/src/views/store/dine-in/styles/responsive.less create mode 100644 apps/web-antd/src/views/store/dine-in/styles/settings.less create mode 100644 apps/web-antd/src/views/store/dine-in/styles/table.less create mode 100644 apps/web-antd/src/views/store/dine-in/types.ts diff --git a/apps/web-antd/src/api/store-dinein/index.ts b/apps/web-antd/src/api/store-dinein/index.ts new file mode 100644 index 0000000..240d843 --- /dev/null +++ b/apps/web-antd/src/api/store-dinein/index.ts @@ -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 & { id?: string }; + storeId: string; +} + +/** 删除堂食区域参数 */ +export interface DeleteDineInAreaParams { + areaId: string; + storeId: string; +} + +/** 保存堂食桌位参数 */ +export interface SaveDineInTableParams { + storeId: string; + table: Omit & { 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('/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('/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('/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( + '/store/dinein/table/batch-create', + data, + ); +} + +/** 复制堂食设置到其他门店 */ +export async function copyStoreDineInSettingsApi( + data: CopyStoreDineInSettingsParams, +) { + return requestClient.post('/store/dinein/copy', data); +} diff --git a/apps/web-antd/src/mock/index.ts b/apps/web-antd/src/mock/index.ts index 84e3a1e..c795695 100644 --- a/apps/web-antd/src/mock/index.ts +++ b/apps/web-antd/src/mock/index.ts @@ -1,5 +1,6 @@ // Mock 数据入口,仅在开发环境下使用 import './store'; +import './store-dinein'; import './store-hours'; import './store-pickup'; diff --git a/apps/web-antd/src/mock/store-dinein.ts b/apps/web-antd/src/mock/store-dinein.ts new file mode 100644 index 0000000..7716ba2 --- /dev/null +++ b/apps/web-antd/src/mock/store-dinein.ts @@ -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(); + +/** 解析 URL 查询参数。 */ +function parseUrlParams(url: string) { + const parsed = new URL(url, 'http://localhost'); + const params: Record = {}; + 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; + } 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 }, + }; +}); diff --git a/apps/web-antd/src/router/routes/modules/store.ts b/apps/web-antd/src/router/routes/modules/store.ts index d866238..b0bac92 100644 --- a/apps/web-antd/src/router/routes/modules/store.ts +++ b/apps/web-antd/src/router/routes/modules/store.ts @@ -46,6 +46,15 @@ const routes: RouteRecordRaw[] = [ title: '自提设置', }, }, + { + name: 'StoreDineIn', + path: '/store/dine-in', + component: () => import('#/views/store/dine-in/index.vue'), + meta: { + icon: 'lucide:utensils', + title: '堂食管理', + }, + }, ], }, ]; diff --git a/apps/web-antd/src/views/store/dine-in/components/DineInAreaDrawer.vue b/apps/web-antd/src/views/store/dine-in/components/DineInAreaDrawer.vue new file mode 100644 index 0000000..570ec7f --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/components/DineInAreaDrawer.vue @@ -0,0 +1,98 @@ + + + diff --git a/apps/web-antd/src/views/store/dine-in/components/DineInAreaSection.vue b/apps/web-antd/src/views/store/dine-in/components/DineInAreaSection.vue new file mode 100644 index 0000000..dd5ea9d --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/components/DineInAreaSection.vue @@ -0,0 +1,81 @@ + + + diff --git a/apps/web-antd/src/views/store/dine-in/components/DineInBasicSettingsCard.vue b/apps/web-antd/src/views/store/dine-in/components/DineInBasicSettingsCard.vue new file mode 100644 index 0000000..50e839e --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/components/DineInBasicSettingsCard.vue @@ -0,0 +1,90 @@ + + + diff --git a/apps/web-antd/src/views/store/dine-in/components/DineInBatchModal.vue b/apps/web-antd/src/views/store/dine-in/components/DineInBatchModal.vue new file mode 100644 index 0000000..0cd2420 --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/components/DineInBatchModal.vue @@ -0,0 +1,136 @@ + + + diff --git a/apps/web-antd/src/views/store/dine-in/components/DineInTableDrawer.vue b/apps/web-antd/src/views/store/dine-in/components/DineInTableDrawer.vue new file mode 100644 index 0000000..9b273d4 --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/components/DineInTableDrawer.vue @@ -0,0 +1,154 @@ + + + diff --git a/apps/web-antd/src/views/store/dine-in/components/DineInTableGridSection.vue b/apps/web-antd/src/views/store/dine-in/components/DineInTableGridSection.vue new file mode 100644 index 0000000..f9004df --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/components/DineInTableGridSection.vue @@ -0,0 +1,94 @@ + + + diff --git a/apps/web-antd/src/views/store/dine-in/composables/dinein-page/area-actions.ts b/apps/web-antd/src/views/store/dine-in/composables/dinein-page/area-actions.ts new file mode 100644 index 0000000..cb8efe8 --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/composables/dinein-page/area-actions.ts @@ -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; + areaForm: DineInAreaFormState; + areas: Ref; + fixSelectedArea: () => void; + isAreaDrawerOpen: Ref; + isSavingArea: Ref; + selectedAreaId: Ref; + selectedStoreId: Ref; + tables: Ref; + 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, + }; +} diff --git a/apps/web-antd/src/views/store/dine-in/composables/dinein-page/constants.ts b/apps/web-antd/src/views/store/dine-in/composables/dinein-page/constants.ts new file mode 100644 index 0000000..bdee15b --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/composables/dinein-page/constants.ts @@ -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 = + { + 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', + '包厢', + '吧台', + '安静区', + '家庭位', + '靠窗', +]; diff --git a/apps/web-antd/src/views/store/dine-in/composables/dinein-page/copy-actions.ts b/apps/web-antd/src/views/store/dine-in/composables/dinein-page/copy-actions.ts new file mode 100644 index 0000000..e2b4373 --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/composables/dinein-page/copy-actions.ts @@ -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; + copyTargetStoreIds: Ref; + isCopyModalOpen: Ref; + isCopySubmitting: Ref; + selectedStoreId: Ref; +} + +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, + }; +} diff --git a/apps/web-antd/src/views/store/dine-in/composables/dinein-page/data-actions.ts b/apps/web-antd/src/views/store/dine-in/composables/dinein-page/data-actions.ts new file mode 100644 index 0000000..d271e59 --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/composables/dinein-page/data-actions.ts @@ -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; + basicSettings: DineInBasicSettingsDto; + isPageLoading: Ref; + isSavingBasic: Ref; + isStoreLoading: Ref; + selectedAreaId: Ref; + selectedStoreId: Ref; + snapshot: Ref; + stores: Ref; + tables: Ref; +} + +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, + }; +} diff --git a/apps/web-antd/src/views/store/dine-in/composables/dinein-page/helpers.ts b/apps/web-antd/src/views/store/dine-in/composables/dinein-page/helpers.ts new file mode 100644 index 0000000..6e0d446 --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/composables/dinein-page/helpers.ts @@ -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 ''; +} diff --git a/apps/web-antd/src/views/store/dine-in/composables/dinein-page/table-actions.ts b/apps/web-antd/src/views/store/dine-in/composables/dinein-page/table-actions.ts new file mode 100644 index 0000000..1d7f84e --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/composables/dinein-page/table-actions.ts @@ -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; + batchForm: DineInBatchFormState; + batchPreviewCodes: ComputedRef; + isBatchModalOpen: Ref; + isSavingBatch: Ref; + isSavingTable: Ref; + isTableDrawerOpen: Ref; + selectedStoreId: Ref; + selectedTableAreaId: Ref; + tableDrawerMode: Ref; + tableForm: DineInTableFormState; + tables: Ref; + 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, + }; +} diff --git a/apps/web-antd/src/views/store/dine-in/composables/useStoreDineInPage.ts b/apps/web-antd/src/views/store/dine-in/composables/useStoreDineInPage.ts new file mode 100644 index 0000000..82be0cb --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/composables/useStoreDineInPage.ts @@ -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([]); + const selectedStoreId = ref(''); + const areas = ref( + sortAreas(cloneAreas(DEFAULT_DINE_IN_AREAS)), + ); + const tables = ref( + sortTables(cloneTables(DEFAULT_DINE_IN_TABLES)), + ); + const basicSettings = reactive( + cloneBasicSettings(DEFAULT_DINE_IN_BASIC_SETTINGS), + ); + const selectedAreaId = ref(areas.value[0]?.id ?? ''); + const snapshot = ref( + createSettingsSnapshot({ + areas: areas.value, + tables: tables.value, + basicSettings, + }), + ); + + // 3. 复制弹窗状态。 + const isCopyModalOpen = ref(false); + const copyTargetStoreIds = ref([]); + + // 4. 区域抽屉状态。 + const isAreaDrawerOpen = ref(false); + const areaDrawerMode = ref('create'); + const areaForm = reactive({ + id: '', + name: '', + description: '', + sort: 1, + }); + + // 5. 桌位抽屉与批量弹窗状态。 + const isTableDrawerOpen = ref(false); + const tableDrawerMode = ref('create'); + const tableForm = reactive({ + id: '', + code: '', + areaId: selectedAreaId.value, + seats: 4, + tags: [], + sourceStatus: 'free', + isDisabled: false, + }); + const isBatchModalOpen = ref(false); + const batchForm = reactive({ + 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, + }; +} diff --git a/apps/web-antd/src/views/store/dine-in/index.vue b/apps/web-antd/src/views/store/dine-in/index.vue new file mode 100644 index 0000000..5e7f987 --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/index.vue @@ -0,0 +1,246 @@ + + + + + diff --git a/apps/web-antd/src/views/store/dine-in/styles/area.less b/apps/web-antd/src/views/store/dine-in/styles/area.less new file mode 100644 index 0000000..f383ce5 --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/styles/area.less @@ -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; + } +} diff --git a/apps/web-antd/src/views/store/dine-in/styles/base.less b/apps/web-antd/src/views/store/dine-in/styles/base.less new file mode 100644 index 0000000..adbd590 --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/styles/base.less @@ -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; + } +} diff --git a/apps/web-antd/src/views/store/dine-in/styles/drawer.less b/apps/web-antd/src/views/store/dine-in/styles/drawer.less new file mode 100644 index 0000000..3fd7af8 --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/styles/drawer.less @@ -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; +} diff --git a/apps/web-antd/src/views/store/dine-in/styles/index.less b/apps/web-antd/src/views/store/dine-in/styles/index.less new file mode 100644 index 0000000..a5a5696 --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/styles/index.less @@ -0,0 +1,7 @@ +/* 文件职责:堂食管理页面样式聚合入口(仅负责分片导入)。 */ +@import './base.less'; +@import './area.less'; +@import './table.less'; +@import './settings.less'; +@import './drawer.less'; +@import './responsive.less'; diff --git a/apps/web-antd/src/views/store/dine-in/styles/responsive.less b/apps/web-antd/src/views/store/dine-in/styles/responsive.less new file mode 100644 index 0000000..89ac761 --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/styles/responsive.less @@ -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; + } +} diff --git a/apps/web-antd/src/views/store/dine-in/styles/settings.less b/apps/web-antd/src/views/store/dine-in/styles/settings.less new file mode 100644 index 0000000..a04a227 --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/styles/settings.less @@ -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; + } +} diff --git a/apps/web-antd/src/views/store/dine-in/styles/table.less b/apps/web-antd/src/views/store/dine-in/styles/table.less new file mode 100644 index 0000000..826f472 --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/styles/table.less @@ -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; + } +} diff --git a/apps/web-antd/src/views/store/dine-in/types.ts b/apps/web-antd/src/views/store/dine-in/types.ts new file mode 100644 index 0000000..6160dc3 --- /dev/null +++ b/apps/web-antd/src/views/store/dine-in/types.ts @@ -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; +}