diff --git a/apps/web-antd/src/main.ts b/apps/web-antd/src/main.ts index 07786c5..e3e00fa 100644 --- a/apps/web-antd/src/main.ts +++ b/apps/web-antd/src/main.ts @@ -3,8 +3,8 @@ import { unmountGlobalLoading } from '@vben/utils'; import { overridesPreferences } from './preferences'; -// 开发环境启用 Mock 数据 -if (import.meta.env.DEV) { +// 仅在显式开启时启用 Mock 数据 +if (import.meta.env.DEV && import.meta.env.VITE_NITRO_MOCK === 'true') { import('./mock'); } diff --git a/apps/web-antd/src/mock/index.ts b/apps/web-antd/src/mock/index.ts index de82b2d..0353ebe 100644 --- a/apps/web-antd/src/mock/index.ts +++ b/apps/web-antd/src/mock/index.ts @@ -1,9 +1,5 @@ // Mock 数据入口,仅在开发环境下使用 -import './store'; -import './store-dinein'; -import './store-fees'; -import './store-hours'; -import './store-pickup'; -import './store-staff'; +// 门店模块已切换真实 TenantApi,此处仅保留其他业务的 mock。 +import './product'; -console.warn('[Mock] Mock 数据已启用'); +console.warn('[Mock] 非门店模块 Mock 数据已启用'); diff --git a/apps/web-antd/src/mock/store-dinein.ts b/apps/web-antd/src/mock/store-dinein.ts deleted file mode 100644 index 7716ba2..0000000 --- a/apps/web-antd/src/mock/store-dinein.ts +++ /dev/null @@ -1,515 +0,0 @@ -import Mock from 'mockjs'; - -/** 文件职责:堂食管理页面 Mock 接口。 */ -interface MockRequestOptions { - body: null | string; - type: string; - url: string; -} - -type DineInTableStatus = 'dining' | 'disabled' | 'free' | 'reserved'; - -interface DineInBasicSettingsMock { - defaultDiningMinutes: number; - enabled: boolean; - overtimeReminderMinutes: number; -} - -interface DineInAreaMock { - description: string; - id: string; - name: string; - sort: number; -} - -interface DineInTableMock { - areaId: string; - code: string; - id: string; - seats: number; - status: DineInTableStatus; - tags: string[]; -} - -interface StoreDineInState { - areas: DineInAreaMock[]; - basicSettings: DineInBasicSettingsMock; - tables: DineInTableMock[]; -} - -const storeDineInMap = new Map(); - -/** 解析 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/mock/store-fees.ts b/apps/web-antd/src/mock/store-fees.ts deleted file mode 100644 index 468a720..0000000 --- a/apps/web-antd/src/mock/store-fees.ts +++ /dev/null @@ -1,346 +0,0 @@ -import Mock from 'mockjs'; - -/** 文件职责:费用设置页面 Mock 接口。 */ -interface MockRequestOptions { - body: null | string; - type: string; - url: string; -} - -type PackagingFeeMode = 'item' | 'order'; -type OrderPackagingFeeMode = 'fixed' | 'tiered'; - -interface PackagingFeeTierMock { - fee: number; - id: string; - maxAmount: null | number; - minAmount: number; - sort: number; -} - -interface AdditionalFeeItemMock { - amount: number; - enabled: boolean; -} - -interface StoreFeesState { - baseDeliveryFee: number; - fixedPackagingFee: number; - freeDeliveryThreshold: null | number; - minimumOrderAmount: number; - orderPackagingFeeMode: OrderPackagingFeeMode; - otherFees: { - cutlery: AdditionalFeeItemMock; - rush: AdditionalFeeItemMock; - }; - packagingFeeMode: PackagingFeeMode; - packagingFeeTiers: PackagingFeeTierMock[]; -} - -const storeFeesMap = new Map(); - -/** 解析 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); - } catch (error) { - console.error('[mock-store-fees] parseBody error:', error); - return {}; - } -} - -/** 保留两位小数并裁剪为非负数。 */ -function normalizeMoney(value: unknown, fallback = 0) { - const parsed = Number(value); - if (!Number.isFinite(parsed)) return fallback; - return Math.round(Math.max(0, parsed) * 100) / 100; -} - -/** 归一化包装费模式。 */ -function normalizePackagingFeeMode(value: unknown, fallback: PackagingFeeMode) { - return value === 'item' || value === 'order' ? value : fallback; -} - -/** 归一化按订单包装费模式。 */ -function normalizeOrderPackagingFeeMode( - value: unknown, - fallback: OrderPackagingFeeMode, -) { - return value === 'fixed' || value === 'tiered' ? value : fallback; -} - -/** 深拷贝阶梯列表。 */ -function cloneTiers(source: PackagingFeeTierMock[]) { - return source.map((item) => ({ ...item })); -} - -/** 深拷贝状态对象。 */ -function cloneStoreState(source: StoreFeesState): StoreFeesState { - return { - minimumOrderAmount: source.minimumOrderAmount, - baseDeliveryFee: source.baseDeliveryFee, - freeDeliveryThreshold: source.freeDeliveryThreshold, - packagingFeeMode: source.packagingFeeMode, - orderPackagingFeeMode: source.orderPackagingFeeMode, - fixedPackagingFee: source.fixedPackagingFee, - packagingFeeTiers: cloneTiers(source.packagingFeeTiers), - otherFees: { - cutlery: { ...source.otherFees.cutlery }, - rush: { ...source.otherFees.rush }, - }, - }; -} - -/** 排序并归一化阶梯列表。 */ -function normalizeTiers( - source: unknown, - fallback: PackagingFeeTierMock[], -): PackagingFeeTierMock[] { - if (!Array.isArray(source) || source.length === 0) { - return cloneTiers(fallback); - } - - const raw = source - .map((item, index) => { - const record = item as Partial; - const minAmount = normalizeMoney(record.minAmount, 0); - let maxAmount: null | number = null; - if ( - record.maxAmount !== null && - record.maxAmount !== undefined && - String(record.maxAmount) !== '' - ) { - maxAmount = normalizeMoney(record.maxAmount, minAmount); - } - return { - id: - typeof record.id === 'string' && record.id.trim() - ? record.id - : `fee-tier-${Date.now()}-${index}`, - minAmount, - maxAmount, - fee: normalizeMoney(record.fee, 0), - sort: Math.max(1, Number(record.sort) || index + 1), - }; - }) - .toSorted((a, b) => { - if (a.minAmount !== b.minAmount) return a.minAmount - b.minAmount; - if (a.maxAmount === null) return 1; - if (b.maxAmount === null) return -1; - return a.maxAmount - b.maxAmount; - }) - .slice(0, 10); - - let hasUnbounded = false; - return raw.map((item, index) => { - let maxAmount = item.maxAmount; - if (hasUnbounded) { - maxAmount = item.minAmount + 0.01; - } - if (maxAmount !== null && maxAmount <= item.minAmount) { - maxAmount = item.minAmount + 0.01; - } - if (maxAmount === null) hasUnbounded = true; - return { - ...item, - maxAmount: - index === raw.length - 1 - ? maxAmount - : (maxAmount ?? item.minAmount + 1), - sort: index + 1, - }; - }); -} - -/** 归一化其他费用。 */ -function normalizeOtherFees( - source: unknown, - fallback: StoreFeesState['otherFees'], -) { - const record = (source || {}) as Partial; - return { - cutlery: { - enabled: Boolean(record.cutlery?.enabled), - amount: normalizeMoney(record.cutlery?.amount, fallback.cutlery.amount), - }, - rush: { - enabled: Boolean(record.rush?.enabled), - amount: normalizeMoney(record.rush?.amount, fallback.rush.amount), - }, - }; -} - -/** 归一化提交数据。 */ -function normalizeStoreState(source: unknown, fallback: StoreFeesState) { - const record = (source || {}) as Partial; - const packagingFeeMode = normalizePackagingFeeMode( - record.packagingFeeMode, - fallback.packagingFeeMode, - ); - - const orderPackagingFeeMode = - packagingFeeMode === 'order' - ? normalizeOrderPackagingFeeMode( - record.orderPackagingFeeMode, - fallback.orderPackagingFeeMode, - ) - : 'fixed'; - - return { - minimumOrderAmount: normalizeMoney( - record.minimumOrderAmount, - fallback.minimumOrderAmount, - ), - baseDeliveryFee: normalizeMoney( - record.baseDeliveryFee, - fallback.baseDeliveryFee, - ), - freeDeliveryThreshold: - record.freeDeliveryThreshold === null || - record.freeDeliveryThreshold === undefined || - String(record.freeDeliveryThreshold) === '' - ? null - : normalizeMoney( - record.freeDeliveryThreshold, - fallback.freeDeliveryThreshold ?? 0, - ), - packagingFeeMode, - orderPackagingFeeMode, - fixedPackagingFee: normalizeMoney( - record.fixedPackagingFee, - fallback.fixedPackagingFee, - ), - packagingFeeTiers: normalizeTiers( - record.packagingFeeTiers, - fallback.packagingFeeTiers, - ), - otherFees: normalizeOtherFees(record.otherFees, fallback.otherFees), - } satisfies StoreFeesState; -} - -/** 创建默认状态。 */ -function createDefaultState(): StoreFeesState { - return { - minimumOrderAmount: 15, - baseDeliveryFee: 3, - freeDeliveryThreshold: 30, - packagingFeeMode: 'order', - orderPackagingFeeMode: 'tiered', - fixedPackagingFee: 2, - packagingFeeTiers: [ - { - id: `fee-tier-${Date.now()}-1`, - minAmount: 0, - maxAmount: 30, - fee: 2, - sort: 1, - }, - { - id: `fee-tier-${Date.now()}-2`, - minAmount: 30, - maxAmount: 60, - fee: 3, - sort: 2, - }, - { - id: `fee-tier-${Date.now()}-3`, - minAmount: 60, - maxAmount: null, - fee: 5, - sort: 3, - }, - ], - otherFees: { - cutlery: { - enabled: false, - amount: 1, - }, - rush: { - enabled: false, - amount: 3, - }, - }, - }; -} - -/** 确保门店状态存在。 */ -function ensureStoreState(storeId = '') { - const key = storeId || 'default'; - let state = storeFeesMap.get(key); - if (!state) { - state = createDefaultState(); - storeFeesMap.set(key, state); - } - return state; -} - -Mock.mock(/\/store\/fees(?:\?|$)/, 'get', (options: MockRequestOptions) => { - const params = parseUrlParams(options.url); - const storeId = String(params.storeId || ''); - const state = ensureStoreState(storeId); - - return { - code: 200, - data: { - storeId, - ...cloneStoreState(state), - }, - }; -}); - -Mock.mock(/\/store\/fees\/save/, 'post', (options: MockRequestOptions) => { - const body = parseBody(options); - const storeId = String((body as { storeId?: unknown }).storeId || ''); - const fallback = ensureStoreState(storeId); - const next = normalizeStoreState(body, fallback); - storeFeesMap.set(storeId || 'default', next); - - return { - code: 200, - data: { - storeId, - ...cloneStoreState(next), - }, - }; -}); - -Mock.mock(/\/store\/fees\/copy/, 'post', (options: MockRequestOptions) => { - const body = parseBody(options) as { - sourceStoreId?: string; - targetStoreIds?: string[]; - }; - const sourceStoreId = String(body.sourceStoreId || ''); - const targetStoreIds = Array.isArray(body.targetStoreIds) - ? body.targetStoreIds.map(String).filter(Boolean) - : []; - - if (!sourceStoreId || targetStoreIds.length === 0) { - return { - code: 400, - message: '参数错误', - }; - } - - const source = ensureStoreState(sourceStoreId); - targetStoreIds.forEach((storeId) => { - storeFeesMap.set(storeId, cloneStoreState(source)); - }); - - return { - code: 200, - data: { - copiedCount: targetStoreIds.length, - }, - }; -}); diff --git a/apps/web-antd/src/mock/store-hours.ts b/apps/web-antd/src/mock/store-hours.ts deleted file mode 100644 index 6f147c2..0000000 --- a/apps/web-antd/src/mock/store-hours.ts +++ /dev/null @@ -1,417 +0,0 @@ -import Mock from 'mockjs'; - -const Random = Mock.Random; - -/** mockjs 请求回调参数 */ -interface MockRequestOptions { - url: string; - type: string; - body: null | string; -} - -interface TimeSlotMock { - id: string; - type: number; - startTime: string; - endTime: string; - capacity?: number; - remark?: string; -} - -interface DayHoursMock { - dayOfWeek: number; - isOpen: boolean; - slots: TimeSlotMock[]; -} - -interface HolidayMock { - id: string; - startDate: string; - endDate: string; - type: number; - startTime?: string; - endTime?: string; - reason: string; - remark?: string; -} - -interface StoreHoursState { - holidays: HolidayMock[]; - weeklyHours: DayHoursMock[]; -} - -function parseUrlParams(url: string) { - const parsed = new URL(url, 'http://localhost'); - const params: Record = {}; - parsed.searchParams.forEach((value, key) => { - params[key] = value; - }); - return params; -} - -function parseBody(options: MockRequestOptions) { - if (!options.body) return {}; - try { - return JSON.parse(options.body); - } catch (error) { - console.error('[mock-store-hours] parseBody error:', error); - return {}; - } -} - -function normalizeDate(date?: string) { - if (!date) return ''; - return String(date).slice(0, 10); -} - -function normalizeTime(time?: string) { - if (!time) return ''; - const matched = /(\d{2}:\d{2})/.exec(time); - return matched?.[1] ?? ''; -} - -function sortSlots(slots: TimeSlotMock[]) { - return [...slots].toSorted((a, b) => { - const startA = a.startTime; - const startB = b.startTime; - if (startA !== startB) return startA.localeCompare(startB); - return a.type - b.type; - }); -} - -function sortHolidays(holidays: HolidayMock[]) { - return [...holidays].toSorted((a, b) => { - const dateCompare = a.startDate.localeCompare(b.startDate); - if (dateCompare !== 0) return dateCompare; - return a.id.localeCompare(b.id); - }); -} - -function cloneWeeklyHours(weeklyHours: DayHoursMock[]) { - return weeklyHours.map((day) => ({ - ...day, - slots: day.slots.map((slot) => ({ ...slot })), - })); -} - -function cloneHolidays(holidays: HolidayMock[]) { - return holidays.map((holiday) => ({ ...holiday })); -} - -function createDefaultWeeklyHours(): DayHoursMock[] { - const weekdays = [ - { - dayOfWeek: 0, - bizEnd: '22:00', - delEnd: '21:30', - delCap: 50, - pickEnd: '21:00', - }, - { - dayOfWeek: 1, - bizEnd: '22:00', - delEnd: '21:30', - delCap: 50, - pickEnd: '21:00', - }, - { - dayOfWeek: 2, - bizEnd: '22:00', - delEnd: '21:30', - delCap: 50, - pickEnd: '21:00', - }, - { - dayOfWeek: 3, - bizEnd: '22:00', - delEnd: '21:30', - delCap: 50, - pickEnd: '21:00', - }, - { - dayOfWeek: 4, - bizEnd: '23:00', - delEnd: '22:30', - delCap: 80, - pickEnd: '22:00', - }, - { - dayOfWeek: 5, - bizEnd: '23:00', - delEnd: '22:30', - delCap: 80, - pickEnd: '22:00', - }, - ]; - - const result = weekdays.map((day) => ({ - dayOfWeek: day.dayOfWeek, - isOpen: true, - slots: [ - { id: Random.guid(), type: 1, startTime: '09:00', endTime: day.bizEnd }, - { - id: Random.guid(), - type: 2, - startTime: '10:00', - endTime: day.delEnd, - capacity: day.delCap, - }, - { id: Random.guid(), type: 3, startTime: '09:00', endTime: day.pickEnd }, - ], - })); - - result.push({ - dayOfWeek: 6, - isOpen: true, - slots: [ - { id: Random.guid(), type: 1, startTime: '10:00', endTime: '22:00' }, - { - id: Random.guid(), - type: 2, - startTime: '10:30', - endTime: '21:30', - capacity: 60, - }, - ], - }); - - return result; -} - -function createDefaultHolidays(): HolidayMock[] { - return [ - { - id: Random.guid(), - startDate: '2026-02-17', - endDate: '2026-02-19', - type: 1, - reason: '春节假期', - }, - { - id: Random.guid(), - startDate: '2026-04-05', - endDate: '2026-04-05', - type: 1, - reason: '清明节', - }, - { - id: Random.guid(), - startDate: '2026-02-14', - endDate: '2026-02-14', - type: 2, - startTime: '09:00', - endTime: '23:30', - reason: '情人节延长营业', - }, - { - id: Random.guid(), - startDate: '2026-05-01', - endDate: '2026-05-01', - type: 2, - startTime: '10:00', - endTime: '20:00', - reason: '劳动节缩短营业', - }, - ]; -} - -function normalizeWeeklyHoursInput(list: any): DayHoursMock[] { - const dayMap = new Map(); - - if (Array.isArray(list)) { - for (const item of list) { - const dayOfWeek = Number(item?.dayOfWeek); - if (!Number.isInteger(dayOfWeek) || dayOfWeek < 0 || dayOfWeek > 6) - continue; - - const slots: TimeSlotMock[] = Array.isArray(item?.slots) - ? item.slots.map((slot: any) => ({ - id: String(slot?.id || Random.guid()), - type: Number(slot?.type) || 1, - startTime: normalizeTime(slot?.startTime) || '09:00', - endTime: normalizeTime(slot?.endTime) || '22:00', - capacity: - Number(slot?.type) === 2 && slot?.capacity !== undefined - ? Number(slot.capacity) - : undefined, - remark: slot?.remark || undefined, - })) - : []; - - dayMap.set(dayOfWeek, { - dayOfWeek, - isOpen: Boolean(item?.isOpen), - slots: sortSlots(slots), - }); - } - } - - return Array.from({ length: 7 }).map((_, dayOfWeek) => { - return ( - dayMap.get(dayOfWeek) ?? { - dayOfWeek, - isOpen: false, - slots: [], - } - ); - }); -} - -function normalizeHolidayInput(holiday: any): HolidayMock { - const type = Number(holiday?.type) === 2 ? 2 : 1; - - return { - id: String(holiday?.id || Random.guid()), - startDate: - normalizeDate(holiday?.startDate) || normalizeDate(holiday?.date), - endDate: - normalizeDate(holiday?.endDate) || - normalizeDate(holiday?.startDate) || - normalizeDate(holiday?.date), - type, - startTime: - type === 2 ? normalizeTime(holiday?.startTime) || undefined : undefined, - endTime: - type === 2 ? normalizeTime(holiday?.endTime) || undefined : undefined, - reason: holiday?.reason || '', - remark: holiday?.remark || undefined, - }; -} - -const storeHoursMap = new Map(); - -function ensureStoreState(storeId = '') { - const key = storeId || 'default'; - let state = storeHoursMap.get(key); - if (!state) { - state = { - weeklyHours: createDefaultWeeklyHours(), - holidays: createDefaultHolidays(), - }; - storeHoursMap.set(key, state); - } - return state; -} - -// 获取门店营业时间 -Mock.mock(/\/store\/hours(?:\?|$)/, 'get', (options: MockRequestOptions) => { - const params = parseUrlParams(options.url); - const storeId = params.storeId || ''; - const state = ensureStoreState(storeId); - - return { - code: 200, - data: { - storeId, - weeklyHours: cloneWeeklyHours(state.weeklyHours), - holidays: cloneHolidays(state.holidays), - }, - }; -}); - -// 保存每周营业时间 -Mock.mock(/\/store\/hours\/weekly/, 'post', (options: MockRequestOptions) => { - const body = parseBody(options); - const storeId = String(body.storeId || ''); - const state = ensureStoreState(storeId); - state.weeklyHours = normalizeWeeklyHoursInput(body.weeklyHours); - return { code: 200, data: null }; -}); - -// 删除特殊日期 -Mock.mock( - /\/store\/hours\/holiday\/delete/, - 'post', - (options: MockRequestOptions) => { - const body = parseBody(options); - const holidayId = String(body.id || ''); - if (!holidayId) return { code: 200, data: null }; - - for (const [, state] of storeHoursMap) { - const index = state.holidays.findIndex( - (holiday) => holiday.id === holidayId, - ); - if (index !== -1) { - state.holidays.splice(index, 1); - break; - } - } - return { code: 200, data: null }; - }, -); - -// 新增 / 编辑特殊日期 -Mock.mock( - /\/store\/hours\/holiday(?!\/delete)/, - 'post', - (options: MockRequestOptions) => { - const body = parseBody(options); - const storeId = String(body.storeId || ''); - const state = ensureStoreState(storeId); - const incomingHoliday = normalizeHolidayInput(body.holiday); - - const existingIndex = state.holidays.findIndex( - (item) => item.id === incomingHoliday.id, - ); - if (existingIndex === -1) { - state.holidays.push(incomingHoliday); - } else { - state.holidays[existingIndex] = incomingHoliday; - } - - state.holidays = sortHolidays(state.holidays); - - return { - code: 200, - data: { ...incomingHoliday }, - }; - }, -); - -// 复制营业时间 -Mock.mock(/\/store\/hours\/copy/, 'post', (options: MockRequestOptions) => { - const body = parseBody(options); - const sourceStoreId = String(body.sourceStoreId || ''); - const targetStoreIds: string[] = Array.isArray(body.targetStoreIds) - ? body.targetStoreIds.map(String).filter(Boolean) - : []; - - if (!sourceStoreId || targetStoreIds.length === 0) { - return { - code: 200, - data: { copiedCount: 0 }, - }; - } - - const includeWeeklyHours = body.includeWeeklyHours !== false; - const includeHolidays = body.includeHolidays !== false; - const sourceState = ensureStoreState(sourceStoreId); - - const uniqueTargets = [...new Set(targetStoreIds)].filter( - (id) => id !== sourceStoreId, - ); - - for (const targetId of uniqueTargets) { - const targetState = ensureStoreState(targetId); - if (includeWeeklyHours) { - targetState.weeklyHours = cloneWeeklyHours(sourceState.weeklyHours); - } - if (includeHolidays) { - targetState.holidays = cloneHolidays(sourceState.holidays).map( - (holiday) => ({ - ...holiday, - id: Random.guid(), - }), - ); - } - } - - return { - code: 200, - data: { - copiedCount: uniqueTargets.length, - includeHolidays, - includeWeeklyHours, - }, - }; -}); diff --git a/apps/web-antd/src/mock/store-pickup.ts b/apps/web-antd/src/mock/store-pickup.ts deleted file mode 100644 index a056243..0000000 --- a/apps/web-antd/src/mock/store-pickup.ts +++ /dev/null @@ -1,582 +0,0 @@ -import Mock from 'mockjs'; - -const Random = Mock.Random; - -/** 文件职责:自提设置页面 Mock 接口。 */ -interface MockRequestOptions { - body: null | string; - type: string; - url: string; -} - -type PickupMode = 'big' | 'fine'; -type PickupWeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6; -type PickupPreviewStatus = 'almost' | 'available' | 'expired' | 'full'; - -interface PickupBasicSettingsMock { - allowSameDayPickup: boolean; - bookingDays: number; - maxItemsPerOrder: null | number; -} - -interface PickupSlotMock { - capacity: number; - cutoffMinutes: number; - dayOfWeeks: PickupWeekDay[]; - enabled: boolean; - endTime: string; - id: string; - name: string; - reservedCount: number; - startTime: string; -} - -interface PickupFineRuleMock { - dayEndTime: string; - dayOfWeeks: PickupWeekDay[]; - dayStartTime: string; - intervalMinutes: number; - minAdvanceHours: number; - slotCapacity: number; -} - -interface PickupPreviewSlotMock { - remainingCount: number; - status: PickupPreviewStatus; - time: string; -} - -interface PickupPreviewDayMock { - date: string; - label: string; - slots: PickupPreviewSlotMock[]; - subLabel: string; -} - -interface StorePickupState { - basicSettings: PickupBasicSettingsMock; - bigSlots: PickupSlotMock[]; - fineRule: PickupFineRuleMock; - mode: PickupMode; - previewDays: PickupPreviewDayMock[]; -} - -const ALL_WEEK_DAYS: PickupWeekDay[] = [0, 1, 2, 3, 4, 5, 6]; -const WEEKDAY_ONLY: PickupWeekDay[] = [0, 1, 2, 3, 4]; -const WEEKEND_ONLY: PickupWeekDay[] = [5, 6]; - -const WEEKDAY_LABEL_MAP: Record = { - 0: '周一', - 1: '周二', - 2: '周三', - 3: '周四', - 4: '周五', - 5: '周六', - 6: '周日', -}; - -const storePickupMap = 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); - } catch (error) { - console.error('[mock-store-pickup] parseBody error:', error); - return {}; - } -} - -/** 确保门店状态存在。 */ -function ensureStoreState(storeId = '') { - const key = storeId || 'default'; - let state = storePickupMap.get(key); - if (!state) { - state = createDefaultState(); - storePickupMap.set(key, state); - } - return state; -} - -function createDefaultState(): StorePickupState { - const fineRule: PickupFineRuleMock = { - intervalMinutes: 30, - slotCapacity: 5, - dayStartTime: '09:00', - dayEndTime: '20:30', - minAdvanceHours: 2, - dayOfWeeks: [...ALL_WEEK_DAYS], - }; - - return { - mode: 'big', - basicSettings: { - allowSameDayPickup: true, - bookingDays: 3, - maxItemsPerOrder: 20, - }, - bigSlots: sortSlots([ - { - id: Random.guid(), - name: '上午时段', - startTime: '09:00', - endTime: '11:30', - cutoffMinutes: 30, - capacity: 20, - reservedCount: 5, - dayOfWeeks: [...WEEKDAY_ONLY], - enabled: true, - }, - { - id: Random.guid(), - name: '午间时段', - startTime: '11:30', - endTime: '14:00', - cutoffMinutes: 20, - capacity: 30, - reservedCount: 12, - dayOfWeeks: [...ALL_WEEK_DAYS], - enabled: true, - }, - { - id: Random.guid(), - name: '下午时段', - startTime: '14:00', - endTime: '17:00', - cutoffMinutes: 30, - capacity: 15, - reservedCount: 3, - dayOfWeeks: [...WEEKDAY_ONLY], - enabled: true, - }, - { - id: Random.guid(), - name: '晚间时段', - startTime: '17:00', - endTime: '20:30', - cutoffMinutes: 30, - capacity: 25, - reservedCount: 8, - dayOfWeeks: [...ALL_WEEK_DAYS], - enabled: true, - }, - { - id: Random.guid(), - name: '周末特惠', - startTime: '10:00', - endTime: '15:00', - cutoffMinutes: 45, - capacity: 40, - reservedCount: 18, - dayOfWeeks: [...WEEKEND_ONLY], - enabled: false, - }, - ]), - fineRule, - previewDays: generatePreviewDays(fineRule), - }; -} - -/** 深拷贝基础设置。 */ -function cloneBasicSettings(source: PickupBasicSettingsMock) { - return { ...source }; -} - -/** 深拷贝大时段列表。 */ -function cloneBigSlots(source: PickupSlotMock[]) { - return source.map((item) => ({ - ...item, - dayOfWeeks: [...item.dayOfWeeks], - })); -} - -/** 深拷贝精细规则。 */ -function cloneFineRule(source: PickupFineRuleMock) { - return { - ...source, - dayOfWeeks: [...source.dayOfWeeks], - }; -} - -/** 深拷贝预览日列表。 */ -function clonePreviewDays(source: PickupPreviewDayMock[]) { - return source.map((day) => ({ - ...day, - slots: day.slots.map((slot) => ({ ...slot })), - })); -} - -/** 深拷贝门店配置。 */ -function cloneStoreState(source: StorePickupState): StorePickupState { - return { - mode: source.mode, - basicSettings: cloneBasicSettings(source.basicSettings), - bigSlots: cloneBigSlots(source.bigSlots), - fineRule: cloneFineRule(source.fineRule), - previewDays: clonePreviewDays(source.previewDays), - }; -} - -/** 归一化基础设置提交数据。 */ -function normalizeBasicSettings(source: any): PickupBasicSettingsMock { - return { - allowSameDayPickup: Boolean(source?.allowSameDayPickup), - bookingDays: clampInt(source?.bookingDays, 1, 30, 3), - maxItemsPerOrder: - source?.maxItemsPerOrder === null || - source?.maxItemsPerOrder === undefined - ? null - : clampInt(source?.maxItemsPerOrder, 0, 999, 20), - }; -} - -/** 归一化精细规则提交数据。 */ -function normalizeFineRule(source: any): PickupFineRuleMock { - return { - intervalMinutes: clampInt(source?.intervalMinutes, 5, 180, 30), - slotCapacity: clampInt(source?.slotCapacity, 1, 999, 5), - dayStartTime: normalizeTime(source?.dayStartTime, '09:00'), - dayEndTime: normalizeTime(source?.dayEndTime, '20:30'), - minAdvanceHours: clampInt(source?.minAdvanceHours, 0, 72, 2), - dayOfWeeks: normalizeDayOfWeeks(source?.dayOfWeeks, [...ALL_WEEK_DAYS]), - }; -} - -/** 归一化大时段提交数据。 */ -function normalizeSlots(source: any, previous: PickupSlotMock[]) { - const reservedMap = new Map( - previous.map((item) => [item.id, item.reservedCount]), - ); - if (!Array.isArray(source) || source.length === 0) return []; - - return sortSlots( - source.map((item, index) => { - const id = String(item?.id || Random.guid()); - const capacity = clampInt(item?.capacity, 0, 9999, 0); - const existingReserved = reservedMap.get(id) ?? 0; - const incomingReserved = clampInt( - item?.reservedCount, - 0, - capacity, - existingReserved, - ); - - return { - id, - name: String(item?.name || `时段${index + 1}`).trim(), - startTime: normalizeTime(item?.startTime, '09:00'), - endTime: normalizeTime(item?.endTime, '17:00'), - cutoffMinutes: clampInt(item?.cutoffMinutes, 0, 720, 30), - capacity, - reservedCount: Math.min(capacity, incomingReserved), - dayOfWeeks: normalizeDayOfWeeks(item?.dayOfWeeks, [...WEEKDAY_ONLY]), - enabled: Boolean(item?.enabled), - }; - }), - ); -} - -/** 归一化模式字段。 */ -function normalizeMode(mode: unknown, fallback: PickupMode): PickupMode { - return mode === 'fine' || mode === 'big' ? mode : fallback; -} - -/** 排序大时段。 */ -function sortSlots(source: PickupSlotMock[]) { - return source.toSorted((a, b) => { - const diff = - parseTimeToMinutes(a.startTime) - parseTimeToMinutes(b.startTime); - if (diff !== 0) return diff; - return a.name.localeCompare(b.name); - }); -} - -/** 归一化 HH:mm 时间。 */ -function normalizeTime(time: unknown, fallback: string) { - const value = typeof time === 'string' ? time : ''; - const matched = /(\d{2}):(\d{2})/.exec(value); - if (!matched) return fallback; - - const hour = Number(matched[1]); - const minute = Number(matched[2]); - if (hour < 0 || hour > 23 || minute < 0 || minute > 59) return fallback; - - return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`; -} - -/** 归一化星期数组。 */ -function normalizeDayOfWeeks(source: unknown, fallback: PickupWeekDay[]) { - if (!Array.isArray(source)) return fallback; - const values = source - .map(Number) - .filter((item) => Number.isInteger(item) && item >= 0 && item <= 6) - .map((item) => item as PickupWeekDay); - const unique = [...new Set(values)].toSorted((a, b) => a - b); - return unique.length > 0 ? unique : fallback; -} - -/** 数值裁剪为整数区间。 */ -function clampInt(value: unknown, min: number, max: number, fallback: number) { - const numberValue = Number(value); - if (!Number.isFinite(numberValue)) return fallback; - const normalized = Math.floor(numberValue); - return Math.max(min, Math.min(max, normalized)); -} - -/** HH:mm 转分钟。 */ -function parseTimeToMinutes(time: string) { - const matched = /^(\d{2}):(\d{2})$/.exec(time); - if (!matched) return Number.NaN; - return Number(matched[1]) * 60 + Number(matched[2]); -} - -/** 生成三天预览数据。 */ -function generatePreviewDays( - fineRule: PickupFineRuleMock, - baseDate = new Date(), -) { - const startMinutes = parseTimeToMinutes(fineRule.dayStartTime); - const endMinutes = parseTimeToMinutes(fineRule.dayEndTime); - if (!Number.isFinite(startMinutes) || !Number.isFinite(endMinutes)) return []; - if (endMinutes <= startMinutes || fineRule.intervalMinutes <= 0) return []; - - return Array.from({ length: 3 }).map((_, index) => { - const date = addDays(baseDate, index); - const dateKey = toDateOnly(date); - const dayOfWeek = toPickupWeekDay(date); - const slots = fineRule.dayOfWeeks.includes(dayOfWeek) - ? generateDaySlots({ - date, - dateKey, - fineRule, - }) - : []; - - return { - date: dateKey, - label: `${date.getMonth() + 1}/${date.getDate()}`, - subLabel: resolvePreviewSubLabel(index, dayOfWeek), - slots, - }; - }); -} - -/** 生成某天时段预览。 */ -function generateDaySlots(payload: { - date: Date; - dateKey: string; - fineRule: PickupFineRuleMock; -}) { - const startMinutes = parseTimeToMinutes(payload.fineRule.dayStartTime); - const endMinutes = parseTimeToMinutes(payload.fineRule.dayEndTime); - const interval = payload.fineRule.intervalMinutes; - const total = Math.floor((endMinutes - startMinutes) / interval); - - return Array.from({ length: total + 1 }).map((_, index) => { - const minutes = startMinutes + index * interval; - const time = `${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String( - minutes % 60, - ).padStart(2, '0')}`; - const booked = calcMockBookedCount( - `${payload.dateKey}|${time}`, - payload.fineRule.slotCapacity, - ); - const remainingCount = Math.max(0, payload.fineRule.slotCapacity - booked); - - return { - time, - remainingCount, - status: resolvePreviewStatus({ - date: payload.date, - fineRule: payload.fineRule, - remainingCount, - time, - }), - }; - }); -} - -/** 计算时段预览状态。 */ -function resolvePreviewStatus(payload: { - date: Date; - fineRule: PickupFineRuleMock; - remainingCount: number; - time: string; -}): PickupPreviewStatus { - const now = new Date(); - const today = toDateOnly(now); - const dateKey = toDateOnly(payload.date); - const slotMinutes = parseTimeToMinutes(payload.time); - const nowMinutes = now.getHours() * 60 + now.getMinutes(); - const minAdvanceMinutes = payload.fineRule.minAdvanceHours * 60; - - if (dateKey < today) return 'expired'; - if (dateKey === today && slotMinutes - nowMinutes <= minAdvanceMinutes) { - return 'expired'; - } - if (payload.remainingCount <= 0) return 'full'; - if (payload.remainingCount <= 1) return 'almost'; - return 'available'; -} - -/** 计算稳定的伪随机已预约量。 */ -function calcMockBookedCount(seed: string, capacity: number) { - if (capacity <= 0) return 0; - let hash = 0; - for (const char of seed) { - hash = (hash * 31 + (char.codePointAt(0) ?? 0)) >>> 0; - } - if (hash % 7 === 0) return capacity; - if (hash % 5 === 0) return Math.max(0, capacity - 1); - return hash % (capacity + 1); -} - -function toDateOnly(date: Date) { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; -} - -function addDays(baseDate: Date, days: number) { - const next = new Date(baseDate); - next.setDate(baseDate.getDate() + days); - return next; -} - -function toPickupWeekDay(date: Date): PickupWeekDay { - const mapping: PickupWeekDay[] = [6, 0, 1, 2, 3, 4, 5]; - return mapping[date.getDay()] ?? 0; -} - -function resolvePreviewSubLabel(offset: number, dayOfWeek: PickupWeekDay) { - const dayText = WEEKDAY_LABEL_MAP[dayOfWeek]; - if (offset === 0) return `${dayText} 今天`; - if (offset === 1) return `${dayText} 明天`; - if (offset === 2) return `${dayText} 后天`; - return dayText; -} - -// 获取门店自提设置 -Mock.mock(/\/store\/pickup(?:\?|$)/, 'get', (options: MockRequestOptions) => { - const params = parseUrlParams(options.url); - const storeId = String(params.storeId || ''); - const state = ensureStoreState(storeId); - state.previewDays = generatePreviewDays(state.fineRule); - - return { - code: 200, - data: { - storeId, - mode: state.mode, - basicSettings: cloneBasicSettings(state.basicSettings), - bigSlots: cloneBigSlots(state.bigSlots), - fineRule: cloneFineRule(state.fineRule), - previewDays: clonePreviewDays(state.previewDays), - }, - }; -}); - -// 保存自提基础设置 -Mock.mock( - /\/store\/pickup\/basic\/save/, - 'post', - (options: MockRequestOptions) => { - const body = parseBody(options); - const storeId = String(body.storeId || ''); - if (!storeId) return { code: 200, data: null }; - - const state = ensureStoreState(storeId); - state.basicSettings = normalizeBasicSettings(body.basicSettings); - state.mode = normalizeMode(body.mode, state.mode); - - return { - code: 200, - data: null, - }; - }, -); - -// 保存自提大时段 -Mock.mock( - /\/store\/pickup\/slots\/save/, - 'post', - (options: MockRequestOptions) => { - const body = parseBody(options); - const storeId = String(body.storeId || ''); - if (!storeId) return { code: 200, data: null }; - - const state = ensureStoreState(storeId); - state.bigSlots = normalizeSlots(body.slots, state.bigSlots); - state.mode = normalizeMode(body.mode, state.mode); - - return { - code: 200, - data: null, - }; - }, -); - -// 保存自提精细规则 -Mock.mock( - /\/store\/pickup\/fine-rule\/save/, - 'post', - (options: MockRequestOptions) => { - const body = parseBody(options); - const storeId = String(body.storeId || ''); - if (!storeId) return { code: 200, data: null }; - - const state = ensureStoreState(storeId); - state.fineRule = normalizeFineRule(body.fineRule); - state.mode = normalizeMode(body.mode, state.mode); - state.previewDays = generatePreviewDays(state.fineRule); - - return { - code: 200, - data: null, - }; - }, -); - -// 复制门店自提设置 -Mock.mock(/\/store\/pickup\/copy/, 'post', (options: MockRequestOptions) => { - const body = parseBody(options); - const sourceStoreId = String(body.sourceStoreId || ''); - const targetStoreIds: string[] = Array.isArray(body.targetStoreIds) - ? body.targetStoreIds.map(String).filter(Boolean) - : []; - - if (!sourceStoreId || targetStoreIds.length === 0) { - return { - code: 200, - data: { copiedCount: 0 }, - }; - } - - const sourceState = ensureStoreState(sourceStoreId); - const uniqueTargets = [...new Set(targetStoreIds)].filter( - (storeId) => storeId !== sourceStoreId, - ); - - for (const targetStoreId of uniqueTargets) { - storePickupMap.set(targetStoreId, cloneStoreState(sourceState)); - } - - return { - code: 200, - data: { - copiedCount: uniqueTargets.length, - }, - }; -}); diff --git a/apps/web-antd/src/mock/store-staff.ts b/apps/web-antd/src/mock/store-staff.ts deleted file mode 100644 index b1fef3a..0000000 --- a/apps/web-antd/src/mock/store-staff.ts +++ /dev/null @@ -1,919 +0,0 @@ -import Mock from 'mockjs'; - -/** 文件职责:员工排班页面 Mock 接口。 */ -interface MockRequestOptions { - body: null | string; - type: string; - url: string; -} - -type StaffRoleType = 'cashier' | 'chef' | 'courier' | 'manager'; -type StaffStatus = 'active' | 'leave' | 'resigned'; -type ShiftType = 'evening' | 'full' | 'morning' | 'off'; - -interface StoreStaffMock { - avatarColor: string; - email: string; - hiredAt: string; - id: string; - name: string; - permissions: string[]; - phone: string; - roleType: StaffRoleType; - status: StaffStatus; -} - -interface ShiftTemplateItemMock { - endTime: string; - startTime: string; -} - -interface StoreShiftTemplatesMock { - evening: ShiftTemplateItemMock; - full: ShiftTemplateItemMock; - morning: ShiftTemplateItemMock; -} - -interface StaffDayShiftMock { - dayOfWeek: number; - endTime: string; - shiftType: ShiftType; - startTime: string; -} - -interface StaffScheduleMock { - shifts: StaffDayShiftMock[]; - staffId: string; -} - -interface StoreStaffState { - schedules: StaffScheduleMock[]; - staffs: StoreStaffMock[]; - templates: StoreShiftTemplatesMock; - weekStartDate: string; -} - -const ROLE_VALUES = new Set([ - 'cashier', - 'chef', - 'courier', - 'manager', -]); -const STATUS_VALUES = new Set(['active', 'leave', 'resigned']); -const SHIFT_VALUES = new Set(['evening', 'full', 'morning', 'off']); - -const AVATAR_COLORS = [ - '#f56a00', - '#7265e6', - '#52c41a', - '#fa8c16', - '#1890ff', - '#bfbfbf', - '#13c2c2', - '#eb2f96', -]; - -const DEFAULT_TEMPLATES: StoreShiftTemplatesMock = { - morning: { - startTime: '09:00', - endTime: '14:00', - }, - evening: { - startTime: '14:00', - endTime: '21:00', - }, - full: { - startTime: '09:00', - endTime: '21:00', - }, -}; - -const DEFAULT_STAFFS: StoreStaffMock[] = [ - { - id: 'staff-001', - name: '张伟', - phone: '13800008001', - email: 'zhangwei@example.com', - roleType: 'manager', - status: 'active', - permissions: ['全部权限'], - hiredAt: '2024-01-15', - avatarColor: '#f56a00', - }, - { - id: 'staff-002', - name: '李娜', - phone: '13800008002', - email: 'lina@example.com', - roleType: 'cashier', - status: 'active', - permissions: ['收银', '退款'], - hiredAt: '2024-03-20', - avatarColor: '#7265e6', - }, - { - id: 'staff-003', - name: '王磊', - phone: '13800008003', - email: '', - roleType: 'courier', - status: 'active', - permissions: ['配送管理'], - hiredAt: '2024-06-01', - avatarColor: '#52c41a', - }, - { - id: 'staff-004', - name: '赵敏', - phone: '13800008004', - email: '', - roleType: 'chef', - status: 'active', - permissions: ['订单查看'], - hiredAt: '2024-08-10', - avatarColor: '#fa8c16', - }, - { - id: 'staff-005', - name: '刘洋', - phone: '13800008005', - email: 'liuyang@example.com', - roleType: 'courier', - status: 'leave', - permissions: ['配送管理'], - hiredAt: '2025-01-05', - avatarColor: '#1890ff', - }, - { - id: 'staff-006', - name: '陈静', - phone: '13800008006', - email: '', - roleType: 'cashier', - status: 'resigned', - permissions: [], - hiredAt: '2024-11-20', - avatarColor: '#bfbfbf', - }, -]; - -const storeStaffMap = new Map(); - -/** 解析 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-staff] parseBody error:', error); - return {}; - } -} - -/** 获取当前周一日期。 */ -function getCurrentWeekStartDate(baseDate = new Date()) { - const date = new Date(baseDate); - const weekDay = date.getDay(); - const diff = weekDay === 0 ? -6 : 1 - weekDay; - date.setDate(date.getDate() + diff); - return toDateOnly(date); -} - -/** 日期转 yyyy-MM-dd。 */ -function toDateOnly(date: Date) { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; -} - -/** 归一化 HH:mm 时间。 */ -function normalizeTime(value: unknown, fallback: string) { - const input = typeof value === 'string' ? value : ''; - const matched = /^(\d{2}):(\d{2})$/.exec(input); - if (!matched) return fallback; - const hour = Number(matched[1]); - const minute = Number(matched[2]); - if (hour < 0 || hour > 23 || minute < 0 || minute > 59) { - return fallback; - } - return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`; -} - -/** 归一化角色。 */ -function normalizeRoleType(value: unknown, fallback: StaffRoleType) { - return ROLE_VALUES.has(value as StaffRoleType) - ? (value as StaffRoleType) - : fallback; -} - -/** 归一化状态。 */ -function normalizeStatus(value: unknown, fallback: StaffStatus) { - return STATUS_VALUES.has(value as StaffStatus) - ? (value as StaffStatus) - : fallback; -} - -/** 归一化班次类型。 */ -function normalizeShiftType(value: unknown, fallback: ShiftType) { - return SHIFT_VALUES.has(value as ShiftType) ? (value as ShiftType) : fallback; -} - -/** 深拷贝员工列表。 */ -function cloneStaffs(source: StoreStaffMock[]) { - return source.map((item) => ({ - ...item, - permissions: [...item.permissions], - })); -} - -/** 深拷贝模板。 */ -function cloneTemplates( - source: StoreShiftTemplatesMock, -): StoreShiftTemplatesMock { - return { - morning: { ...source.morning }, - evening: { ...source.evening }, - full: { ...source.full }, - }; -} - -/** 深拷贝排班。 */ -function cloneSchedules(source: StaffScheduleMock[]) { - return source.map((item) => ({ - staffId: item.staffId, - shifts: item.shifts.map((shift) => ({ ...shift })), - })); -} - -/** 按入职时间稳定排序员工。 */ -function sortStaffs(source: StoreStaffMock[]) { - return cloneStaffs(source).toSorted((a, b) => { - const dateDiff = a.hiredAt.localeCompare(b.hiredAt); - if (dateDiff !== 0) return dateDiff; - return a.name.localeCompare(b.name); - }); -} - -/** 通过班次类型生成单日排班。 */ -function createDayShift( - dayOfWeek: number, - shiftType: ShiftType, - templates: StoreShiftTemplatesMock, -): StaffDayShiftMock { - if (shiftType === 'off') { - return { - dayOfWeek, - shiftType, - startTime: '', - endTime: '', - }; - } - - const template = templates[shiftType]; - return { - dayOfWeek, - shiftType, - startTime: template.startTime, - endTime: template.endTime, - }; -} - -/** 生成默认 7 天排班。 */ -function createDefaultWeekByRole( - roleType: StaffRoleType, - templates: StoreShiftTemplatesMock, -): StaffDayShiftMock[] { - const rolePatternMap: Record = { - manager: ['full', 'full', 'full', 'full', 'full', 'morning', 'off'], - cashier: [ - 'morning', - 'morning', - 'off', - 'morning', - 'evening', - 'full', - 'full', - ], - courier: [ - 'morning', - 'evening', - 'morning', - 'evening', - 'morning', - 'evening', - 'off', - ], - chef: ['full', 'full', 'evening', 'off', 'full', 'full', 'morning'], - }; - - const pattern = rolePatternMap[roleType] ?? rolePatternMap.cashier; - return Array.from({ length: 7 }).map((_, dayOfWeek) => - createDayShift(dayOfWeek, pattern[dayOfWeek] ?? 'off', templates), - ); -} - -/** 创建默认门店状态。 */ -function createDefaultState(): StoreStaffState { - const templates = cloneTemplates(DEFAULT_TEMPLATES); - const staffs = sortStaffs(cloneStaffs(DEFAULT_STAFFS)); - const schedules = staffs.map((staff) => ({ - staffId: staff.id, - shifts: - staff.status === 'resigned' - ? Array.from({ length: 7 }).map((_, dayOfWeek) => - createDayShift(dayOfWeek, 'off', templates), - ) - : createDefaultWeekByRole(staff.roleType, templates), - })); - - return { - staffs, - templates, - schedules, - weekStartDate: getCurrentWeekStartDate(), - }; -} - -/** 确保门店状态存在。 */ -function ensureStoreState(storeId = '') { - const key = storeId || 'default'; - let state = storeStaffMap.get(key); - if (!state) { - state = createDefaultState(); - storeStaffMap.set(key, state); - } - return state; -} - -/** 构建员工排班索引。 */ -function createScheduleMap(schedules: StaffScheduleMock[]) { - const scheduleMap = new Map(); - for (const schedule of schedules) { - scheduleMap.set( - schedule.staffId, - schedule.shifts.map((shift) => ({ ...shift })), - ); - } - return scheduleMap; -} - -/** 同步模板后刷新已有排班的时间段。 */ -function syncScheduleTimesWithTemplates( - schedules: StaffScheduleMock[], - templates: StoreShiftTemplatesMock, -) { - for (const schedule of schedules) { - schedule.shifts = schedule.shifts - .map((shift) => { - const normalizedType = normalizeShiftType(shift.shiftType, 'off'); - if (normalizedType === 'off') { - return { - dayOfWeek: shift.dayOfWeek, - shiftType: 'off' as const, - startTime: '', - endTime: '', - }; - } - return { - dayOfWeek: shift.dayOfWeek, - shiftType: normalizedType, - startTime: templates[normalizedType].startTime, - endTime: templates[normalizedType].endTime, - }; - }) - .toSorted((a, b) => a.dayOfWeek - b.dayOfWeek); - } -} - -/** 归一化模板。 */ -function normalizeTemplates( - input: unknown, - fallback: StoreShiftTemplatesMock, -): StoreShiftTemplatesMock { - const record = typeof input === 'object' && input ? input : {}; - const morning = - typeof (record as { morning?: unknown }).morning === 'object' && - (record as { morning?: unknown }).morning - ? ((record as { morning: Record }).morning as Record< - string, - unknown - >) - : {}; - const evening = - typeof (record as { evening?: unknown }).evening === 'object' && - (record as { evening?: unknown }).evening - ? ((record as { evening: Record }).evening as Record< - string, - unknown - >) - : {}; - const full = - typeof (record as { full?: unknown }).full === 'object' && - (record as { full?: unknown }).full - ? ((record as { full: Record }).full as Record< - string, - unknown - >) - : {}; - - return { - morning: { - startTime: normalizeTime(morning.startTime, fallback.morning.startTime), - endTime: normalizeTime(morning.endTime, fallback.morning.endTime), - }, - evening: { - startTime: normalizeTime(evening.startTime, fallback.evening.startTime), - endTime: normalizeTime(evening.endTime, fallback.evening.endTime), - }, - full: { - startTime: normalizeTime(full.startTime, fallback.full.startTime), - endTime: normalizeTime(full.endTime, fallback.full.endTime), - }, - }; -} - -/** 归一化单员工 7 天排班。 */ -function normalizeShifts( - input: unknown, - templates: StoreShiftTemplatesMock, - fallback: StaffDayShiftMock[], -): StaffDayShiftMock[] { - const byDay = new Map(); - - if (Array.isArray(input)) { - for (const rawShift of input) { - const shiftRecord = - typeof rawShift === 'object' && rawShift - ? (rawShift as Record) - : {}; - const dayOfWeek = Number(shiftRecord.dayOfWeek); - if (!Number.isInteger(dayOfWeek) || dayOfWeek < 0 || dayOfWeek > 6) { - continue; - } - - const shiftType = normalizeShiftType(shiftRecord.shiftType, 'off'); - if (shiftType === 'off') { - byDay.set(dayOfWeek, { - dayOfWeek, - shiftType, - startTime: '', - endTime: '', - }); - continue; - } - - byDay.set(dayOfWeek, { - dayOfWeek, - shiftType, - startTime: normalizeTime( - shiftRecord.startTime, - templates[shiftType].startTime, - ), - endTime: normalizeTime( - shiftRecord.endTime, - templates[shiftType].endTime, - ), - }); - } - } - - return Array.from({ length: 7 }).map((_, dayOfWeek) => { - const normalized = byDay.get(dayOfWeek); - if (normalized) { - return normalized; - } - - const fallbackShift = fallback.find((item) => item.dayOfWeek === dayOfWeek); - if (fallbackShift) { - return { ...fallbackShift }; - } - - return createDayShift(dayOfWeek, 'off', templates); - }); -} - -/** 规范手机号格式。 */ -function normalizePhone(value: unknown) { - return String(value || '') - .replaceAll(/\D/g, '') - .slice(0, 11); -} - -/** 规范权限列表。 */ -function normalizePermissions(value: unknown, roleType: StaffRoleType) { - if (!Array.isArray(value)) { - return roleType === 'manager' ? ['全部权限'] : []; - } - const unique = [ - ...new Set(value.map((item) => String(item || '').trim()).filter(Boolean)), - ]; - if (roleType === 'manager' && unique.length === 0) { - return ['全部权限']; - } - return unique; -} - -/** 生成员工 ID。 */ -function createStaffId() { - return `staff-${Date.now()}-${Math.floor(Math.random() * 1000)}`; -} - -/** 生成头像颜色。 */ -function resolveAvatarColor(seed: string) { - let hash = 0; - for (const char of seed) { - hash = (hash * 31 + (char.codePointAt(0) ?? 0)) >>> 0; - } - return AVATAR_COLORS[hash % AVATAR_COLORS.length] ?? '#1677ff'; -} - -/** 获取员工列表。 */ -Mock.mock(/\/store\/staff(?:\?|$)/, 'get', (options: MockRequestOptions) => { - const params = parseUrlParams(options.url); - const storeId = String(params.storeId || ''); - const state = ensureStoreState(storeId); - - const keyword = String(params.keyword || '') - .trim() - .toLowerCase(); - const roleType = params.roleType - ? normalizeRoleType(params.roleType, 'cashier') - : ''; - const status = params.status ? normalizeStatus(params.status, 'active') : ''; - - const page = Math.max(1, Number(params.page) || 1); - const pageSize = Math.max(1, Math.min(200, Number(params.pageSize) || 10)); - - const filtered = state.staffs.filter((staff) => { - if (keyword) { - const hitKeyword = - staff.name.toLowerCase().includes(keyword) || - staff.phone.includes(keyword) || - staff.email.toLowerCase().includes(keyword); - if (!hitKeyword) return false; - } - - if (roleType && staff.roleType !== roleType) { - return false; - } - - if (status && staff.status !== status) { - return false; - } - - return true; - }); - - const start = (page - 1) * pageSize; - const items = filtered.slice(start, start + pageSize); - - return { - code: 200, - data: { - items: cloneStaffs(items), - total: filtered.length, - page, - pageSize, - }, - }; -}); - -/** 新增 / 编辑员工。 */ -Mock.mock(/\/store\/staff\/save/, 'post', (options: MockRequestOptions) => { - const body = parseBody(options); - const storeId = String(body.storeId || ''); - if (!storeId) { - return { code: 200, data: null }; - } - - const state = ensureStoreState(storeId); - const id = String(body.id || '').trim(); - const existingIndex = state.staffs.findIndex((item) => item.id === id); - const roleType = normalizeRoleType(body.roleType, 'cashier'); - const status = normalizeStatus(body.status, 'active'); - - const nextStaff: StoreStaffMock = { - id: id || createStaffId(), - name: String(body.name || '').trim(), - phone: normalizePhone(body.phone), - email: String(body.email || '').trim(), - roleType, - status, - permissions: normalizePermissions(body.permissions, roleType), - hiredAt: - existingIndex === -1 - ? toDateOnly(new Date()) - : state.staffs[existingIndex]?.hiredAt || toDateOnly(new Date()), - avatarColor: - existingIndex === -1 - ? resolveAvatarColor(String(body.name || Date.now())) - : state.staffs[existingIndex]?.avatarColor || '#1677ff', - }; - - if (!nextStaff.name) { - return { - code: 400, - data: null, - message: '员工姓名不能为空', - }; - } - - if (!nextStaff.phone || nextStaff.phone.length < 6) { - return { - code: 400, - data: null, - message: '手机号格式不正确', - }; - } - - if (existingIndex === -1) { - state.staffs.push(nextStaff); - state.staffs = sortStaffs(state.staffs); - state.schedules.push({ - staffId: nextStaff.id, - shifts: - status === 'resigned' - ? Array.from({ length: 7 }).map((_, dayOfWeek) => - createDayShift(dayOfWeek, 'off', state.templates), - ) - : createDefaultWeekByRole(roleType, state.templates), - }); - } else { - state.staffs[existingIndex] = nextStaff; - state.staffs = sortStaffs(state.staffs); - - const schedule = state.schedules.find( - (item) => item.staffId === nextStaff.id, - ); - if (schedule && nextStaff.status === 'resigned') { - schedule.shifts = Array.from({ length: 7 }).map((_, dayOfWeek) => - createDayShift(dayOfWeek, 'off', state.templates), - ); - } - if (!schedule) { - state.schedules.push({ - staffId: nextStaff.id, - shifts: createDefaultWeekByRole(nextStaff.roleType, state.templates), - }); - } - } - - return { - code: 200, - data: { - ...nextStaff, - permissions: [...nextStaff.permissions], - }, - }; -}); - -/** 删除员工。 */ -Mock.mock(/\/store\/staff\/delete/, 'post', (options: MockRequestOptions) => { - const body = parseBody(options); - const storeId = String(body.storeId || ''); - const staffId = String(body.staffId || ''); - if (!storeId || !staffId) { - return { code: 200, data: null }; - } - - const state = ensureStoreState(storeId); - state.staffs = state.staffs.filter((item) => item.id !== staffId); - state.schedules = state.schedules.filter((item) => item.staffId !== staffId); - - return { - code: 200, - data: null, - }; -}); - -/** 获取门店排班配置。 */ -Mock.mock( - /\/store\/staff\/schedule(?:\?|$)/, - 'get', - (options: MockRequestOptions) => { - const params = parseUrlParams(options.url); - const storeId = String(params.storeId || ''); - const state = ensureStoreState(storeId); - - return { - code: 200, - data: { - storeId, - templates: cloneTemplates(state.templates), - schedules: cloneSchedules(state.schedules), - weekStartDate: String(params.weekStartDate || state.weekStartDate), - }, - }; - }, -); - -/** 保存班次模板。 */ -Mock.mock( - /\/store\/staff\/template\/save/, - 'post', - (options: MockRequestOptions) => { - const body = parseBody(options); - const storeId = String(body.storeId || ''); - if (!storeId) { - return { code: 200, data: cloneTemplates(DEFAULT_TEMPLATES) }; - } - - const state = ensureStoreState(storeId); - state.templates = normalizeTemplates(body.templates, state.templates); - syncScheduleTimesWithTemplates(state.schedules, state.templates); - - return { - code: 200, - data: cloneTemplates(state.templates), - }; - }, -); - -/** 保存员工个人排班。 */ -Mock.mock( - /\/store\/staff\/schedule\/personal\/save/, - 'post', - (options: MockRequestOptions) => { - const body = parseBody(options); - const storeId = String(body.storeId || ''); - const staffId = String(body.staffId || ''); - if (!storeId || !staffId) { - return { code: 200, data: null }; - } - - const state = ensureStoreState(storeId); - const staff = state.staffs.find((item) => item.id === staffId); - if (!staff) { - return { - code: 400, - data: null, - message: '员工不存在', - }; - } - - const schedule = state.schedules.find((item) => item.staffId === staffId); - const fallbackShifts = - schedule?.shifts ?? - createDefaultWeekByRole(staff.roleType, state.templates); - - const nextShifts = - staff.status === 'resigned' - ? Array.from({ length: 7 }).map((_, dayOfWeek) => - createDayShift(dayOfWeek, 'off', state.templates), - ) - : normalizeShifts(body.shifts, state.templates, fallbackShifts); - - if (schedule) { - schedule.shifts = nextShifts; - } else { - state.schedules.push({ - staffId, - shifts: nextShifts, - }); - } - - return { - code: 200, - data: { - staffId, - shifts: nextShifts.map((item) => ({ ...item })), - }, - }; - }, -); - -/** 保存周排班。 */ -Mock.mock( - /\/store\/staff\/schedule\/weekly\/save/, - 'post', - (options: MockRequestOptions) => { - const body = parseBody(options); - const storeId = String(body.storeId || ''); - if (!storeId) { - return { code: 200, data: null }; - } - - const state = ensureStoreState(storeId); - const incomingList = Array.isArray(body.schedules) ? body.schedules : []; - const scheduleMap = createScheduleMap(state.schedules); - - for (const incoming of incomingList) { - const record = - typeof incoming === 'object' && incoming - ? (incoming as Record) - : {}; - const staffId = String(record.staffId || ''); - if (!staffId) continue; - - const staff = state.staffs.find((item) => item.id === staffId); - if (!staff || staff.status === 'resigned') continue; - - const fallbackShifts = - scheduleMap.get(staffId) ?? - createDefaultWeekByRole(staff.roleType, state.templates); - - const nextShifts = normalizeShifts( - record.shifts, - state.templates, - fallbackShifts, - ); - scheduleMap.set(staffId, nextShifts); - } - - state.schedules = state.staffs.map((staff) => { - const baseShifts = - scheduleMap.get(staff.id) ?? - createDefaultWeekByRole(staff.roleType, state.templates); - return { - staffId: staff.id, - shifts: - staff.status === 'resigned' - ? Array.from({ length: 7 }).map((_, dayOfWeek) => - createDayShift(dayOfWeek, 'off', state.templates), - ) - : baseShifts.map((shift) => ({ ...shift })), - }; - }); - - return { - code: 200, - data: { - storeId, - templates: cloneTemplates(state.templates), - schedules: cloneSchedules(state.schedules), - weekStartDate: state.weekStartDate, - }, - }; - }, -); - -/** 复制门店模板与排班。 */ -Mock.mock(/\/store\/staff\/copy/, 'post', (options: MockRequestOptions) => { - const body = parseBody(options); - const sourceStoreId = String(body.sourceStoreId || ''); - const copyScope = String(body.copyScope || ''); - const targetStoreIds = Array.isArray(body.targetStoreIds) - ? body.targetStoreIds.map(String).filter(Boolean) - : []; - - if ( - !sourceStoreId || - copyScope !== 'template_and_schedule' || - targetStoreIds.length === 0 - ) { - return { - code: 200, - data: { copiedCount: 0 }, - }; - } - - const sourceState = ensureStoreState(sourceStoreId); - const sourceScheduleMap = createScheduleMap(sourceState.schedules); - const uniqueTargets = [...new Set(targetStoreIds)].filter( - (id) => id !== sourceStoreId, - ); - - for (const targetStoreId of uniqueTargets) { - const targetState = ensureStoreState(targetStoreId); - targetState.templates = cloneTemplates(sourceState.templates); - - targetState.schedules = targetState.staffs.map((staff) => { - const targetShiftFallback = - sourceScheduleMap.get(staff.id) ?? - createDefaultWeekByRole(staff.roleType, targetState.templates); - - return { - staffId: staff.id, - shifts: - staff.status === 'resigned' - ? Array.from({ length: 7 }).map((_, dayOfWeek) => - createDayShift(dayOfWeek, 'off', targetState.templates), - ) - : normalizeShifts( - targetShiftFallback, - targetState.templates, - targetShiftFallback, - ), - }; - }); - } - - return { - code: 200, - data: { - copiedCount: uniqueTargets.length, - copyScope: 'template_and_schedule', - }, - }; -}); diff --git a/apps/web-antd/src/mock/store.ts b/apps/web-antd/src/mock/store.ts deleted file mode 100644 index 9bb25ca..0000000 --- a/apps/web-antd/src/mock/store.ts +++ /dev/null @@ -1,257 +0,0 @@ -import Mock from 'mockjs'; - -const Random = Mock.Random; - -/** mockjs 请求回调参数 */ -interface MockRequestOptions { - url: string; - type: string; - body: null | string; -} - -/** 门店筛选参数 */ -interface StoreFilterParams { - keyword?: string; - businessStatus?: string; - auditStatus?: string; - serviceType?: string; - page?: string; - pageSize?: string; -} - -// 预定义门店数据,保证每次请求返回一致的数据 -const storePool = generateStores(23); - -function generateStores(count: number) { - const districts = [ - '朝阳区建国路88号', - '海淀区中关村大街66号', - '朝阳区望京西路50号', - '通州区新华大街120号', - '丰台区丰台路18号', - '西城区西单北大街100号', - '东城区王府井大街200号', - '大兴区黄村镇兴华路30号', - '昌平区回龙观东大街15号', - '顺义区府前街8号', - '石景山区石景山路22号', - '房山区良乡拱辰大街55号', - '密云区鼓楼东大街10号', - '怀柔区青春路6号', - '平谷区府前街12号', - '门头沟区新桥大街3号', - '延庆区妫水北街9号', - '亦庄经济开发区荣华南路1号', - '望京SOHO T1-2层', - '三里屯太古里南区B1', - '国贸商城3层', - '五道口华联商厦1层', - '中关村食宝街B1层', - ]; - - const managerNames = [ - '张伟', - '李娜', - '王磊', - '赵敏', - '刘洋', - '陈静', - '杨帆', - '周杰', - '吴芳', - '孙涛', - '马丽', - '朱军', - '胡明', - '郭强', - '何欢', - '林峰', - '徐婷', - '高远', - '罗斌', - '梁宇', - '宋佳', - '唐亮', - '韩雪', - ]; - - const storeNames = [ - '老三家外卖(朝阳店)', - '老三家外卖(海淀店)', - '老三家外卖(望京店)', - '老三家外卖(通州店)', - '老三家外卖(丰台店)', - '老三家外卖(西单店)', - '老三家外卖(王府井店)', - '老三家外卖(大兴店)', - '老三家外卖(回龙观店)', - '老三家外卖(顺义店)', - '老三家外卖(石景山店)', - '老三家外卖(良乡店)', - '老三家外卖(密云店)', - '老三家外卖(怀柔店)', - '老三家外卖(平谷店)', - '老三家外卖(门头沟店)', - '老三家外卖(延庆店)', - '老三家外卖(亦庄店)', - '老三家外卖(望京SOHO店)', - '老三家外卖(三里屯店)', - '老三家外卖(国贸店)', - '老三家外卖(五道口店)', - '老三家外卖(中关村店)', - ]; - - const avatarColors = [ - '#3b82f6', - '#f59e0b', - '#8b5cf6', - '#ef4444', - '#22c55e', - '#06b6d4', - '#ec4899', - '#f97316', - '#14b8a6', - '#6366f1', - ]; - - const stores = []; - for (let i = 0; i < count; i++) { - // 1. 按索引分配营业状态,模拟真实分布 - let businessStatus = 0; - if (i >= 21) { - businessStatus = Random.pick([0, 1, 2]); - } else if (i >= 18) { - businessStatus = 2; - } else if (i >= 14) { - businessStatus = 1; - } - - // 2. 按索引分配审核状态 - let auditStatus = 2; - if (i < 20) { - auditStatus = 1; - } else if (i < 22) { - auditStatus = 0; - } - - // 3. 循环分配服务方式组合 - const serviceTypeCombos = [[1], [1, 2], [1, 2, 3], [1, 3], [2, 3]]; - - stores.push({ - id: Random.guid(), - name: storeNames[i] || `老三家外卖(分店${i + 1})`, - code: `ST2025${String(i + 1).padStart(4, '0')}`, - contactPhone: `138****${String(8001 + i).slice(-4)}`, - managerName: managerNames[i] || Random.cname(), - address: `北京市${districts[i] || `朝阳区某路${i + 1}号`}`, - coverImage: '', - businessStatus, - auditStatus, - serviceTypes: serviceTypeCombos[i % serviceTypeCombos.length], - createdAt: Random.datetime('yyyy-MM-dd'), - _avatarColor: avatarColors[i % avatarColors.length], - }); - } - return stores; -} - -function filterStores(params: StoreFilterParams) { - let list = [...storePool]; - - // 1. 关键词模糊匹配(名称/编码/电话) - if (params.keyword) { - const kw = params.keyword.toLowerCase(); - list = list.filter( - (s) => - s.name.toLowerCase().includes(kw) || - s.code.toLowerCase().includes(kw) || - s.contactPhone.includes(kw), - ); - } - - // 2. 营业状态筛选 - if (params.businessStatus) { - const status = Number(params.businessStatus); - list = list.filter((s) => s.businessStatus === status); - } - - // 3. 审核状态筛选 - if (params.auditStatus !== undefined && params.auditStatus !== '') { - const status = Number(params.auditStatus); - list = list.filter((s) => s.auditStatus === status); - } - - // 4. 服务方式筛选 - if (params.serviceType) { - const type = Number(params.serviceType); - list = list.filter((s) => (s.serviceTypes ?? []).includes(type)); - } - - return list; -} - -/** 从 URL 中解析查询参数 */ -function parseUrlParams(url: string): StoreFilterParams { - const parsed = new URL(url, 'http://localhost'); - const params: Record = {}; - parsed.searchParams.forEach((value, key) => { - params[key] = value; - }); - return params; -} - -const enableStoreCrudMock = import.meta.env.VITE_STORE_CRUD_MOCK === 'true'; - -if (enableStoreCrudMock) { - // 门店列表 - Mock.mock(/\/store\/list/, 'get', (options: MockRequestOptions) => { - const params = parseUrlParams(options.url); - - const page = Number(params.page) || 1; - const pageSize = Number(params.pageSize) || 10; - const filtered = filterStores(params); - const start = (page - 1) * pageSize; - const items = filtered.slice(start, start + pageSize); - - return { - code: 200, - data: { - items, - total: filtered.length, - page, - pageSize, - }, - }; - }); - - // 门店统计 - Mock.mock(/\/store\/stats/, 'get', () => { - return { - code: 200, - data: { - total: storePool.length, - operating: storePool.filter((s) => s.businessStatus === 0).length, - resting: storePool.filter((s) => s.businessStatus === 1).length, - pendingAudit: storePool.filter((s) => s.auditStatus === 1).length, - }, - }; - }); - - // 创建门店 - Mock.mock(/\/store\/create/, 'post', () => { - return { code: 200, data: null }; - }); - - // 更新门店 - Mock.mock(/\/store\/update/, 'post', () => { - return { code: 200, data: null }; - }); - - // 删除门店 - Mock.mock(/\/store\/delete/, 'post', () => { - return { code: 200, data: null }; - }); -} - -// 设置 mock 响应延迟 -Mock.setup({ timeout: '200-400' }); diff --git a/apps/web-antd/src/views/store/staff/components/ShiftTemplateCard.vue b/apps/web-antd/src/views/store/staff/components/ShiftTemplateCard.vue index 531760e..0cd3bff 100644 --- a/apps/web-antd/src/views/store/staff/components/ShiftTemplateCard.vue +++ b/apps/web-antd/src/views/store/staff/components/ShiftTemplateCard.vue @@ -40,12 +40,14 @@ const templateRows: Array<{ /** 将 HH:mm 字符串转换为时间组件值。 */ function toPickerValue(time: string) { - if (!time) return null; + if (!time) return undefined; return dayjs(`2000-01-01 ${time}`); } /** 将时间组件值转换为 HH:mm 字符串。 */ -function toTimeText(value: Dayjs | null) { +function toTimeText(value: Dayjs | null | string | undefined) { + if (!value) return ''; + if (typeof value === 'string') return value; return value ? value.format('HH:mm') : ''; } @@ -53,7 +55,7 @@ function toTimeText(value: Dayjs | null) { function handleTemplateTimeChange(payload: { field: 'endTime' | 'startTime'; shiftType: Exclude; - value: Dayjs | null; + value: Dayjs | null | string; }) { props.onSetTemplateTime({ shiftType: payload.shiftType, diff --git a/apps/web-antd/src/views/store/staff/components/StaffScheduleDrawer.vue b/apps/web-antd/src/views/store/staff/components/StaffScheduleDrawer.vue index 2b6ad45..f6999da 100644 --- a/apps/web-antd/src/views/store/staff/components/StaffScheduleDrawer.vue +++ b/apps/web-antd/src/views/store/staff/components/StaffScheduleDrawer.vue @@ -42,13 +42,15 @@ function getDayShift(dayOfWeek: number) { /** 转换时间组件值。 */ function toPickerValue(time: string) { - if (!time) return null; + if (!time) return undefined; return dayjs(`2000-01-01 ${time}`); } /** 时间组件值转字符串。 */ -function toTimeText(value: Dayjs | null) { - return value ? value.format('HH:mm') : ''; +function toTimeText(value: Dayjs | null | string | undefined) { + if (!value) return ''; + if (typeof value === 'string') return value; + return value.format('HH:mm'); } diff --git a/apps/web-antd/src/views/store/staff/composables/staff-page/helpers.ts b/apps/web-antd/src/views/store/staff/composables/staff-page/helpers.ts index 54ae06f..e53b892 100644 --- a/apps/web-antd/src/views/store/staff/composables/staff-page/helpers.ts +++ b/apps/web-antd/src/views/store/staff/composables/staff-page/helpers.ts @@ -70,22 +70,13 @@ export function cloneStaffForm( /** 按模板创建空白一周(默认休息)。 */ export function createEmptyWeekShifts( - templates: StoreShiftTemplatesDto, + _templates: StoreShiftTemplatesDto, ): StaffDayShiftDto[] { - return DAY_OPTIONS.map((day) => { - const fallbackShift = templates.morning; - return { - dayOfWeek: day.dayOfWeek, - shiftType: 'off', - startTime: '', - endTime: '', - ...fallbackShift, - }; - }).map((item) => ({ - dayOfWeek: item.dayOfWeek, - shiftType: item.shiftType, - startTime: item.shiftType === 'off' ? '' : item.startTime, - endTime: item.shiftType === 'off' ? '' : item.endTime, + return DAY_OPTIONS.map((day) => ({ + dayOfWeek: day.dayOfWeek, + shiftType: 'off' as const, + startTime: '', + endTime: '', })); }