diff --git a/apps/web-antd/src/api/store-delivery/index.ts b/apps/web-antd/src/api/store-delivery/index.ts index e28c4f4..4de1900 100644 --- a/apps/web-antd/src/api/store-delivery/index.ts +++ b/apps/web-antd/src/api/store-delivery/index.ts @@ -45,8 +45,9 @@ export interface DeliveryGeneralSettingsDto { /** 门店配送设置聚合 */ export interface StoreDeliverySettingsDto { - generalSettings: DeliveryGeneralSettingsDto; - mode: DeliveryMode; + generalSettings: DeliveryGeneralSettingsDto | null; + isConfigured: boolean; + mode: DeliveryMode | null; /** 半径配送中心点纬度 */ radiusCenterLatitude: null | number; /** 半径配送中心点经度 */ @@ -57,7 +58,17 @@ export interface StoreDeliverySettingsDto { } /** 保存配送设置参数 */ -export type SaveStoreDeliverySettingsParams = StoreDeliverySettingsDto; +export interface SaveStoreDeliverySettingsParams { + generalSettings: DeliveryGeneralSettingsDto; + mode: DeliveryMode; + /** 半径配送中心点纬度 */ + radiusCenterLatitude: null | number; + /** 半径配送中心点经度 */ + radiusCenterLongitude: null | number; + polygonZones: PolygonZoneDto[]; + radiusTiers: RadiusTierDto[]; + storeId: string; +} /** 复制配送设置参数 */ export interface CopyStoreDeliverySettingsParams { diff --git a/apps/web-antd/src/api/store-dinein/index.ts b/apps/web-antd/src/api/store-dinein/index.ts index 240d843..82bd0c5 100644 --- a/apps/web-antd/src/api/store-dinein/index.ts +++ b/apps/web-antd/src/api/store-dinein/index.ts @@ -44,7 +44,8 @@ export interface DineInTableDto { /** 门店堂食设置聚合 */ export interface StoreDineInSettingsDto { areas: DineInAreaDto[]; - basicSettings: DineInBasicSettingsDto; + basicSettings: DineInBasicSettingsDto | null; + isConfigured: boolean; storeId: string; tables: DineInTableDto[]; } diff --git a/apps/web-antd/src/api/store-fees/index.ts b/apps/web-antd/src/api/store-fees/index.ts index b6f9df1..897c772 100644 --- a/apps/web-antd/src/api/store-fees/index.ts +++ b/apps/web-antd/src/api/store-fees/index.ts @@ -60,12 +60,33 @@ export interface StoreFeesSettingsDto { packagingFeeMode: PackagingFeeMode; /** 包装费阶梯 */ packagingFeeTiers: PackagingFeeTierDto[]; + /** 是否已配置 */ + isConfigured: boolean; /** 门店 ID */ storeId: string; } /** 保存费用设置参数 */ -export type SaveStoreFeesSettingsParams = StoreFeesSettingsDto; +export interface SaveStoreFeesSettingsParams { + /** 基础配送费 */ + baseDeliveryFee: number; + /** 固定包装费 */ + fixedPackagingFee: number; + /** 免配送费门槛,空值表示关闭 */ + freeDeliveryThreshold: null | number; + /** 起送金额 */ + minimumOrderAmount: number; + /** 其他费用 */ + otherFees: StoreOtherFeesDto; + /** 按订单包装费模式 */ + orderPackagingFeeMode: OrderPackagingFeeMode; + /** 包装费模式 */ + packagingFeeMode: PackagingFeeMode; + /** 包装费阶梯 */ + packagingFeeTiers: PackagingFeeTierDto[]; + /** 门店 ID */ + storeId: string; +} /** 复制费用设置参数 */ export interface CopyStoreFeesSettingsParams { diff --git a/apps/web-antd/src/api/store-pickup/index.ts b/apps/web-antd/src/api/store-pickup/index.ts index f5566b4..f6e333b 100644 --- a/apps/web-antd/src/api/store-pickup/index.ts +++ b/apps/web-antd/src/api/store-pickup/index.ts @@ -71,10 +71,11 @@ export interface PickupPreviewDayDto { /** 门店自提设置聚合 */ export interface StorePickupSettingsDto { - basicSettings: PickupBasicSettingsDto; + basicSettings: null | PickupBasicSettingsDto; bigSlots: PickupSlotDto[]; - fineRule: PickupFineRuleDto; - mode: PickupMode; + fineRule: null | PickupFineRuleDto; + isConfigured: boolean; + mode: null | PickupMode; previewDays: PickupPreviewDayDto[]; storeId: string; } diff --git a/apps/web-antd/src/views/store/delivery/components/DeliveryPolygonMapModal.vue b/apps/web-antd/src/views/store/delivery/components/DeliveryPolygonMapModal.vue index fea5d9e..b9e1689 100644 --- a/apps/web-antd/src/views/store/delivery/components/DeliveryPolygonMapModal.vue +++ b/apps/web-antd/src/views/store/delivery/components/DeliveryPolygonMapModal.vue @@ -43,7 +43,7 @@ const emit = defineEmits<{ (event: 'update:open', value: boolean): void; }>(); -const DEFAULT_CENTER: LngLatTuple = [116.397_428, 39.909_23]; +const DEFAULT_CENTER: LngLatTuple = [104.195_397, 35.861_66]; const MAP_MODAL_Z_INDEX = 10_000; const MAP_DEBUG_PREFIX = '[TenantUI-DeliveryMap]'; diff --git a/apps/web-antd/src/views/store/delivery/composables/delivery-page/constants.ts b/apps/web-antd/src/views/store/delivery/composables/delivery-page/constants.ts index d3d6bff..da71979 100644 --- a/apps/web-antd/src/views/store/delivery/composables/delivery-page/constants.ts +++ b/apps/web-antd/src/views/store/delivery/composables/delivery-page/constants.ts @@ -28,106 +28,13 @@ export const TIER_COLOR_PALETTE = [ export const DEFAULT_DELIVERY_MODE: DeliveryMode = 'radius'; -export const DEFAULT_RADIUS_TIERS: RadiusTierDto[] = [ - { - id: 'tier-1', - minDistance: 0, - maxDistance: 1, - deliveryFee: 3, - etaMinutes: 20, - minOrderAmount: 15, - color: '#52c41a', - }, - { - id: 'tier-2', - minDistance: 1, - maxDistance: 3, - deliveryFee: 5, - etaMinutes: 35, - minOrderAmount: 20, - color: '#faad14', - }, - { - id: 'tier-3', - minDistance: 3, - maxDistance: 5, - deliveryFee: 8, - etaMinutes: 50, - minOrderAmount: 25, - color: '#ff4d4f', - }, -]; +export const DEFAULT_RADIUS_TIERS: RadiusTierDto[] = []; -function createPolygonGeoJson(coordinates: Array<[number, number]>) { - return JSON.stringify({ - type: 'FeatureCollection', - features: [ - { - type: 'Feature', - geometry: { - type: 'Polygon', - coordinates: [coordinates], - }, - properties: {}, - }, - ], - }); -} - -export const DEFAULT_POLYGON_ZONES: PolygonZoneDto[] = [ - { - id: 'zone-core', - name: '核心区域', - color: '#52c41a', - deliveryFee: 3, - minOrderAmount: 15, - etaMinutes: 20, - priority: 1, - polygonGeoJson: createPolygonGeoJson([ - [116.389, 39.907], - [116.397, 39.907], - [116.397, 39.913], - [116.389, 39.913], - [116.389, 39.907], - ]), - }, - { - id: 'zone-cbd', - name: '朝阳CBD', - color: '#1677ff', - deliveryFee: 5, - minOrderAmount: 20, - etaMinutes: 35, - priority: 2, - polygonGeoJson: createPolygonGeoJson([ - [116.456, 39.914], - [116.468, 39.914], - [116.468, 39.923], - [116.456, 39.923], - [116.456, 39.914], - ]), - }, - { - id: 'zone-slt', - name: '三里屯片区', - color: '#faad14', - deliveryFee: 6, - minOrderAmount: 25, - etaMinutes: 40, - priority: 3, - polygonGeoJson: createPolygonGeoJson([ - [116.445, 39.928], - [116.455, 39.928], - [116.455, 39.936], - [116.445, 39.936], - [116.445, 39.928], - ]), - }, -]; +export const DEFAULT_POLYGON_ZONES: PolygonZoneDto[] = []; export const DEFAULT_GENERAL_SETTINGS: DeliveryGeneralSettingsDto = { - freeDeliveryThreshold: 30, - maxDeliveryDistance: 5, - hourlyCapacityLimit: 50, - etaAdjustmentMinutes: 10, + freeDeliveryThreshold: null, + maxDeliveryDistance: 0, + hourlyCapacityLimit: 1, + etaAdjustmentMinutes: 0, }; diff --git a/apps/web-antd/src/views/store/delivery/composables/delivery-page/data-actions.ts b/apps/web-antd/src/views/store/delivery/composables/delivery-page/data-actions.ts index ab5e38d..6bfa70e 100644 --- a/apps/web-antd/src/views/store/delivery/composables/delivery-page/data-actions.ts +++ b/apps/web-antd/src/views/store/delivery/composables/delivery-page/data-actions.ts @@ -38,11 +38,14 @@ import { } from './helpers'; interface CreateDataActionsOptions { + clearSettings: () => void; editingMode: Ref; generalSettings: DeliveryGeneralSettingsDto; + isConfigured: Ref; isSaving: Ref; isSettingsLoading: Ref; isStoreLoading: Ref; + loadedStoreId: Ref; mode: Ref; radiusCenterLatitude: Ref; radiusCenterLongitude: Ref; @@ -85,17 +88,6 @@ export function createDataActions(options: CreateDataActionsOptions) { }); } - /** 回填默认配置,作为接口异常时的兜底展示。 */ - function applyDefaultSettings() { - options.mode.value = DEFAULT_DELIVERY_MODE; - options.editingMode.value = DEFAULT_DELIVERY_MODE; - options.radiusCenterLatitude.value = null; - options.radiusCenterLongitude.value = null; - options.radiusTiers.value = sortRadiusTiers(DEFAULT_RADIUS_TIERS); - options.polygonZones.value = sortPolygonZones(DEFAULT_POLYGON_ZONES); - syncGeneralSettings(cloneGeneralSettings(DEFAULT_GENERAL_SETTINGS)); - } - /** 加载指定门店的配送设置。 */ async function loadStoreSettings(storeId: string) { options.isSettingsLoading.value = true; @@ -104,29 +96,41 @@ export function createDataActions(options: CreateDataActionsOptions) { const result = await getStoreDeliverySettingsApi(storeId); if (options.selectedStoreId.value !== currentStoreId) return; + if (!result.isConfigured) { + options.clearSettings(); + options.isConfigured.value = false; + options.loadedStoreId.value = currentStoreId; + options.snapshot.value = buildCurrentSnapshot(); + return; + } + options.mode.value = result.mode ?? DEFAULT_DELIVERY_MODE; options.editingMode.value = options.mode.value; options.radiusCenterLatitude.value = result.radiusCenterLatitude ?? null; options.radiusCenterLongitude.value = result.radiusCenterLongitude ?? null; options.radiusTiers.value = sortRadiusTiers( - result.radiusTiers?.length ? result.radiusTiers : DEFAULT_RADIUS_TIERS, + result.radiusTiers ?? DEFAULT_RADIUS_TIERS, ); options.polygonZones.value = sortPolygonZones( - result.polygonZones?.length - ? result.polygonZones - : clonePolygonZones(DEFAULT_POLYGON_ZONES), + result.polygonZones ?? clonePolygonZones(DEFAULT_POLYGON_ZONES), ); - syncGeneralSettings({ - ...DEFAULT_GENERAL_SETTINGS, - ...result.generalSettings, - }); + syncGeneralSettings( + result.generalSettings + ? cloneGeneralSettings(result.generalSettings) + : cloneGeneralSettings(DEFAULT_GENERAL_SETTINGS), + ); + options.isConfigured.value = true; + options.loadedStoreId.value = currentStoreId; options.snapshot.value = buildCurrentSnapshot(); } catch (error) { console.error(error); - applyDefaultSettings(); - options.snapshot.value = buildCurrentSnapshot(); + options.clearSettings(); + options.isConfigured.value = false; + options.loadedStoreId.value = ''; + options.snapshot.value = null; + message.error('加载配送设置失败,请稍后重试'); } finally { options.isSettingsLoading.value = false; } @@ -148,8 +152,10 @@ export function createDataActions(options: CreateDataActionsOptions) { if (options.stores.value.length === 0) { options.selectedStoreId.value = ''; + options.loadedStoreId.value = ''; + options.isConfigured.value = false; options.snapshot.value = null; - applyDefaultSettings(); + options.clearSettings(); return; } @@ -172,8 +178,11 @@ export function createDataActions(options: CreateDataActionsOptions) { console.error(error); options.stores.value = []; options.selectedStoreId.value = ''; + options.loadedStoreId.value = ''; + options.isConfigured.value = false; options.snapshot.value = null; - applyDefaultSettings(); + options.clearSettings(); + message.error('加载门店失败,请稍后重试'); } finally { options.isStoreLoading.value = false; } @@ -209,7 +218,7 @@ export function createDataActions(options: CreateDataActionsOptions) { /** 重置到最近一次加载/保存后的快照。 */ function resetFromSnapshot() { if (!options.snapshot.value) { - applyDefaultSettings(); + options.clearSettings(); return; } applySnapshot(options.snapshot.value); diff --git a/apps/web-antd/src/views/store/delivery/composables/delivery-page/tier-actions.ts b/apps/web-antd/src/views/store/delivery/composables/delivery-page/tier-actions.ts index f4f18f3..4ec39ac 100644 --- a/apps/web-antd/src/views/store/delivery/composables/delivery-page/tier-actions.ts +++ b/apps/web-antd/src/views/store/delivery/composables/delivery-page/tier-actions.ts @@ -95,7 +95,7 @@ export function createTierActions(options: CreateTierActionsOptions) { // 1. 校验区间与字段合法性。 if (options.tierForm.maxDistance <= options.tierForm.minDistance) { message.error('结束距离必须大于起始距离'); - return; + return false; } if ( @@ -103,7 +103,7 @@ export function createTierActions(options: CreateTierActionsOptions) { options.tierForm.minOrderAmount < 0 ) { message.error('金额字段不能小于 0'); - return; + return false; } // 2. 校验与现有梯度区间冲突。 @@ -116,7 +116,7 @@ export function createTierActions(options: CreateTierActionsOptions) { }); if (hasOverlap) { message.error('距离区间与已有梯度重叠,请调整后重试'); - return; + return false; } // 3. 组装记录并写回列表。 @@ -143,18 +143,20 @@ export function createTierActions(options: CreateTierActionsOptions) { message.success( options.tierDrawerMode.value === 'edit' ? '梯度已更新' : '梯度已添加', ); + return true; } /** 删除指定梯度。 */ function handleDeleteTier(tierId: string) { if (options.radiusTiers.value.length <= 1) { message.warning('至少保留一个梯度'); - return; + return false; } options.radiusTiers.value = options.radiusTiers.value.filter( (item) => item.id !== tierId, ); message.success('梯度已删除'); + return true; } return { diff --git a/apps/web-antd/src/views/store/delivery/composables/delivery-page/zone-actions.ts b/apps/web-antd/src/views/store/delivery/composables/delivery-page/zone-actions.ts index ee2fafd..53357c1 100644 --- a/apps/web-antd/src/views/store/delivery/composables/delivery-page/zone-actions.ts +++ b/apps/web-antd/src/views/store/delivery/composables/delivery-page/zone-actions.ts @@ -101,12 +101,12 @@ export function createZoneActions(options: CreateZoneActionsOptions) { const normalizedName = options.zoneForm.name.trim(); if (!normalizedName) { message.error('请输入区域名称'); - return; + return false; } if (countPolygonsInGeoJson(options.zoneForm.polygonGeoJson) <= 0) { message.error('请先绘制配送区域'); - return; + return false; } // 2. 优先级冲突校验。 @@ -116,7 +116,7 @@ export function createZoneActions(options: CreateZoneActionsOptions) { }); if (hasPriorityConflict) { message.error('优先级已存在,请调整后重试'); - return; + return false; } // 3. 写回列表。 @@ -144,6 +144,7 @@ export function createZoneActions(options: CreateZoneActionsOptions) { message.success( options.zoneDrawerMode.value === 'edit' ? '区域已更新' : '区域已添加', ); + return true; } /** 删除指定区域。 */ @@ -152,6 +153,7 @@ export function createZoneActions(options: CreateZoneActionsOptions) { (item) => item.id !== zoneId, ); message.success('区域已删除'); + return true; } return { diff --git a/apps/web-antd/src/views/store/delivery/composables/useStoreDeliveryPage.ts b/apps/web-antd/src/views/store/delivery/composables/useStoreDeliveryPage.ts index a5baa0c..cb61a62 100644 --- a/apps/web-antd/src/views/store/delivery/composables/useStoreDeliveryPage.ts +++ b/apps/web-antd/src/views/store/delivery/composables/useStoreDeliveryPage.ts @@ -67,6 +67,8 @@ export function useStoreDeliveryPage() { const isSettingsLoading = ref(false); const isSaving = ref(false); const isCopySubmitting = ref(false); + const isConfigured = ref(false); + const loadedStoreId = ref(''); // 2. 页面主业务数据。 const stores = ref([]); @@ -180,6 +182,19 @@ export function useStoreDeliveryPage() { const isRadiusMode = computed(() => editingMode.value === 'radius'); const isPageLoading = computed(() => isSettingsLoading.value); + const hasSelectedStore = computed(() => Boolean(selectedStoreId.value)); + const hasLoadedStoreSettings = computed( + () => + loadedStoreId.value === selectedStoreId.value && snapshot.value !== null, + ); + const canOperate = computed( + () => + hasSelectedStore.value && + loadedStoreId.value === selectedStoreId.value && + !isStoreLoading.value && + !isSettingsLoading.value && + !isSaving.value, + ); const tierDrawerTitle = computed(() => tierDrawerMode.value === 'edit' ? '编辑梯度' : '添加梯度', @@ -188,6 +203,19 @@ export function useStoreDeliveryPage() { zoneDrawerMode.value === 'edit' ? '编辑区域' : '新增区域', ); + function clearSettings() { + deliveryMode.value = DEFAULT_DELIVERY_MODE; + editingMode.value = DEFAULT_DELIVERY_MODE; + radiusCenterLatitude.value = null; + radiusCenterLongitude.value = null; + radiusTiers.value = cloneRadiusTiers(DEFAULT_RADIUS_TIERS); + polygonZones.value = clonePolygonZones(DEFAULT_POLYGON_ZONES); + Object.assign( + generalSettings, + cloneGeneralSettings(DEFAULT_GENERAL_SETTINGS), + ); + } + // 5. 数据域动作装配。 const { loadStoreSettings, @@ -195,11 +223,14 @@ export function useStoreDeliveryPage() { resetFromSnapshot, saveCurrentSettings, } = createDataActions({ + clearSettings, editingMode, generalSettings, + isConfigured, isSaving, isSettingsLoading, isStoreLoading, + loadedStoreId, mode: deliveryMode, radiusCenterLatitude, radiusCenterLongitude, @@ -224,8 +255,8 @@ export function useStoreDeliveryPage() { }); const { - handleDeleteTier, - handleTierSubmit, + handleDeleteTier: handleDeleteTierLocal, + handleTierSubmit: handleTierSubmitLocal, openTierDrawer, setTierColor, setTierDeliveryFee, @@ -245,8 +276,8 @@ export function useStoreDeliveryPage() { }); const { - handleDeleteZone, - handleZoneSubmit, + handleDeleteZone: handleDeleteZoneLocal, + handleZoneSubmit: handleZoneSubmitLocal, openZoneDrawer, setZoneColor, setZoneDeliveryFee, @@ -294,6 +325,7 @@ export function useStoreDeliveryPage() { // 切换“当前生效模式”,二次确认后保存,防止误操作。 function setDeliveryMode(value: DeliveryMode) { + if (!canOperate.value) return; if (value === deliveryMode.value) return; Modal.confirm({ title: '确认切换当前启用的配送模式?', @@ -362,22 +394,47 @@ export function useStoreDeliveryPage() { } } + async function handleTierSubmit() { + if (!canOperate.value) return; + const changed = handleTierSubmitLocal(); + if (!changed) return; + await saveCurrentSettings(); + } + + async function handleDeleteTier(tierId: string) { + if (!canOperate.value) return; + const changed = handleDeleteTierLocal(tierId); + if (!changed) return; + await saveCurrentSettings(); + } + + async function handleZoneSubmit() { + if (!canOperate.value) return; + const changed = handleZoneSubmitLocal(); + if (!changed) return; + await saveCurrentSettings(); + } + + async function handleDeleteZone(zoneId: string) { + if (!canOperate.value) return; + const changed = handleDeleteZoneLocal(zoneId); + if (!changed) return; + await saveCurrentSettings(); + } + // 7. 门店切换时自动刷新配置。 watch(selectedStoreId, async (storeId) => { if (!storeId) { - deliveryMode.value = DEFAULT_DELIVERY_MODE; - editingMode.value = DEFAULT_DELIVERY_MODE; - radiusCenterLatitude.value = null; - radiusCenterLongitude.value = null; - radiusTiers.value = cloneRadiusTiers(DEFAULT_RADIUS_TIERS); - polygonZones.value = clonePolygonZones(DEFAULT_POLYGON_ZONES); - Object.assign( - generalSettings, - cloneGeneralSettings(DEFAULT_GENERAL_SETTINGS), - ); + loadedStoreId.value = ''; + isConfigured.value = false; + clearSettings(); snapshot.value = null; return; } + loadedStoreId.value = ''; + isConfigured.value = false; + snapshot.value = null; + clearSettings(); await loadStoreSettings(storeId); }); @@ -392,6 +449,7 @@ export function useStoreDeliveryPage() { return { DELIVERY_MODE_OPTIONS, + canOperate, copyCandidates, copyTargetStoreIds, deliveryMode, @@ -409,6 +467,9 @@ export function useStoreDeliveryPage() { isCopyIndeterminate, isCopyModalOpen, isCopySubmitting, + isConfigured, + hasLoadedStoreSettings, + hasSelectedStore, isPageLoading, isRadiusMode, isSaving, @@ -422,6 +483,7 @@ export function useStoreDeliveryPage() { openCopyModal, openTierDrawer, openZoneDrawer, + loadedStoreId, polygonZones, radiusCenterLatitude, radiusCenterLongitude, diff --git a/apps/web-antd/src/views/store/delivery/index.vue b/apps/web-antd/src/views/store/delivery/index.vue index ce62b48..1f40512 100644 --- a/apps/web-antd/src/views/store/delivery/index.vue +++ b/apps/web-antd/src/views/store/delivery/index.vue @@ -20,6 +20,7 @@ import { useStoreDeliveryPage } from './composables/useStoreDeliveryPage'; const { DELIVERY_MODE_OPTIONS, + canOperate, copyCandidates, copyTargetStoreIds, deliveryMode, @@ -37,6 +38,8 @@ const { isCopyIndeterminate, isCopyModalOpen, isCopySubmitting, + isConfigured, + hasLoadedStoreSettings, isPageLoading, isRadiusMode, isSaving, @@ -98,7 +101,9 @@ const { :selected-store-id="selectedStoreId" :store-options="storeOptions" :is-store-loading="isStoreLoading" - :copy-disabled="!selectedStoreId || copyCandidates.length === 0" + :copy-disabled=" + !canOperate || !isConfigured || copyCandidates.length === 0 + " @update:selected-store-id="setSelectedStoreId" @copy="openCopyModal" /> @@ -111,6 +116,12 @@ const { - + 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 index cb8efe8..ee84d21 100644 --- 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 @@ -15,12 +15,7 @@ import { message } from 'ant-design-vue'; import { deleteDineInAreaApi, saveDineInAreaApi } from '#/api/store-dinein'; -import { - countAreaTables, - createDineInId, - sortAreas, - validateAreaForm, -} from './helpers'; +import { countAreaTables, sortAreas, validateAreaForm } from './helpers'; interface CreateAreaActionsOptions { areaDrawerMode: Ref; @@ -91,30 +86,27 @@ export function createAreaActions(options: CreateAreaActionsOptions) { 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({ + const savedArea = await saveDineInAreaApi({ storeId: options.selectedStoreId.value, - area: areaPayload, + area: { + id: options.areaForm.id || undefined, + name: options.areaForm.name.trim(), + description: options.areaForm.description.trim(), + sort: Math.max(1, Math.floor(options.areaForm.sort)), + }, }); options.areas.value = options.areaDrawerMode.value === 'edit' && options.areaForm.id ? sortAreas( options.areas.value.map((item) => - item.id === options.areaForm.id ? areaPayload : item, + item.id === options.areaForm.id ? savedArea : item, ), ) - : sortAreas([...options.areas.value, areaPayload]); + : sortAreas([...options.areas.value, savedArea]); if (!options.selectedAreaId.value) { - options.selectedAreaId.value = areaPayload.id; + options.selectedAreaId.value = savedArea.id; } options.fixSelectedArea(); options.updateSnapshot(); 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 index bdee15b..6318e9b 100644 --- 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 @@ -1,13 +1,10 @@ /** * 文件职责:堂食管理页面静态常量。 - * 1. 维护默认区域、桌位、基础设置。 - * 2. 提供状态、座位数等选项映射。 + * 1. 维护状态、座位数、标签建议等固定枚举。 + * 2. 不包含任何业务数据兜底。 */ import type { - DineInAreaDto, - DineInBasicSettingsDto, DineInEditableStatus, - DineInTableDto, DineInTableStatus, } from '#/api/store-dinein'; import type { @@ -55,100 +52,6 @@ export const DINE_IN_EDITABLE_STATUS_OPTIONS: Array<{ { 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/data-actions.ts b/apps/web-antd/src/views/store/dine-in/composables/dinein-page/data-actions.ts index d271e59..fce8f4b 100644 --- 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 @@ -22,14 +22,7 @@ import { } 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, @@ -38,9 +31,12 @@ import { interface CreateDataActionsOptions { areas: Ref; basicSettings: DineInBasicSettingsDto; + clearSettings: () => void; + isConfigured: Ref; isPageLoading: Ref; isSavingBasic: Ref; isStoreLoading: Ref; + loadedStoreId: Ref; selectedAreaId: Ref; selectedStoreId: Ref; snapshot: Ref; @@ -57,14 +53,6 @@ export function createDataActions(options: CreateDataActionsOptions) { 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({ @@ -96,27 +84,31 @@ export function createDataActions(options: CreateDataActionsOptions) { 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, - }); + if (!result.isConfigured) { + options.clearSettings(); + options.isConfigured.value = false; + options.loadedStoreId.value = currentStoreId; + options.snapshot.value = buildCurrentSnapshot(); + return; + } + + options.areas.value = sortAreas(result.areas ?? []); + options.tables.value = sortTables(result.tables ?? []); + if (result.basicSettings) { + syncBasicSettings(cloneBasicSettings(result.basicSettings)); + } fixSelectedArea(); + options.isConfigured.value = true; + options.loadedStoreId.value = currentStoreId; options.snapshot.value = buildCurrentSnapshot(); } catch (error) { console.error(error); - applyDefaultSettings(); - options.snapshot.value = buildCurrentSnapshot(); + options.isConfigured.value = false; + options.loadedStoreId.value = ''; + options.snapshot.value = null; + options.clearSettings(); + message.error('加载堂食设置失败,请稍后重试'); } finally { options.isPageLoading.value = false; } @@ -138,7 +130,8 @@ export function createDataActions(options: CreateDataActionsOptions) { if (options.stores.value.length === 0) { options.selectedStoreId.value = ''; - applyDefaultSettings(); + options.isConfigured.value = false; + options.clearSettings(); options.snapshot.value = null; return; } @@ -156,9 +149,11 @@ export function createDataActions(options: CreateDataActionsOptions) { } } catch (error) { console.error(error); + message.error('加载门店失败,请稍后重试'); options.stores.value = []; options.selectedStoreId.value = ''; - applyDefaultSettings(); + options.isConfigured.value = false; + options.clearSettings(); options.snapshot.value = null; } finally { options.isStoreLoading.value = false; @@ -174,6 +169,7 @@ export function createDataActions(options: CreateDataActionsOptions) { storeId: options.selectedStoreId.value, basicSettings: cloneBasicSettings(options.basicSettings), }); + options.isConfigured.value = true; options.snapshot.value = buildCurrentSnapshot(); message.success('堂食设置已保存'); } catch (error) { @@ -185,10 +181,11 @@ export function createDataActions(options: CreateDataActionsOptions) { /** 重置基础设置到最近快照。 */ function resetBasicSettings() { - const source = - options.snapshot.value?.basicSettings ?? - cloneBasicSettings(DEFAULT_DINE_IN_BASIC_SETTINGS); - syncBasicSettings(source); + if (!options.snapshot.value) { + message.warning('暂无可恢复的已保存配置'); + return; + } + syncBasicSettings(options.snapshot.value.basicSettings); message.success('已恢复到最近一次保存状态'); } 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 index 6e0d446..139a542 100644 --- 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 @@ -58,11 +58,6 @@ 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(); 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 index 1d7f84e..bcac72a 100644 --- 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 @@ -1,4 +1,4 @@ -import type { ComputedRef, Ref } from 'vue'; +import type { Ref } from 'vue'; /** * 文件职责:堂食桌位动作。 @@ -25,7 +25,6 @@ import { } from '#/api/store-dinein'; import { - createDineInId, generateBatchCodes, normalizeTableCode, sortTables, @@ -36,7 +35,6 @@ import { interface CreateTableActionsOptions { areas: Ref; batchForm: DineInBatchFormState; - batchPreviewCodes: ComputedRef; isBatchModalOpen: Ref; isSavingBatch: Ref; isSavingTable: Ref; @@ -120,7 +118,6 @@ export function createTableActions(options: CreateTableActionsOptions) { options.isSavingTable.value = true; try { - const tableId = options.tableForm.id || createDineInId('table'); let nextStatus: DineInTableStatus = options.tableForm.sourceStatus; if (options.tableForm.isDisabled) { nextStatus = 'disabled'; @@ -128,28 +125,26 @@ export function createTableActions(options: CreateTableActionsOptions) { 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({ + const savedTable = await saveDineInTableApi({ storeId: options.selectedStoreId.value, - table: tablePayload, + table: { + id: options.tableForm.id || undefined, + code: normalizeTableCode(options.tableForm.code), + areaId: options.tableForm.areaId, + seats: options.tableForm.seats, + status: nextStatus, + tags: [...options.tableForm.tags], + }, }); options.tables.value = options.tableDrawerMode.value === 'edit' && options.tableForm.id ? sortTables( options.tables.value.map((item) => - item.id === options.tableForm.id ? tablePayload : item, + item.id === options.tableForm.id ? savedTable : item, ), ) - : sortTables([...options.tables.value, tablePayload]); + : sortTables([...options.tables.value, savedTable]); options.updateSnapshot(); options.isTableDrawerOpen.value = false; @@ -237,7 +232,7 @@ export function createTableActions(options: CreateTableActionsOptions) { options.isSavingBatch.value = true; try { - await batchCreateDineInTablesApi({ + const result = await batchCreateDineInTablesApi({ storeId: options.selectedStoreId.value, areaId: options.batchForm.areaId, codePrefix: options.batchForm.codePrefix, @@ -246,15 +241,7 @@ export function createTableActions(options: CreateTableActionsOptions) { 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: [], - })); + const createdTables: DineInTableDto[] = result.createdTables ?? []; options.tables.value = sortTables([ ...options.tables.value, 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 index 12836c5..d867715 100644 --- a/apps/web-antd/src/views/store/dine-in/composables/useStoreDineInPage.ts +++ b/apps/web-antd/src/views/store/dine-in/composables/useStoreDineInPage.ts @@ -23,9 +23,6 @@ import { computed, onActivated, 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, @@ -33,18 +30,19 @@ import { 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'; +const EMPTY_BASIC_SETTINGS: DineInBasicSettingsDto = { + enabled: false, + defaultDiningMinutes: 0, + overtimeReminderMinutes: 0, +}; + export function useStoreDineInPage() { // 1. 页面 loading / submitting 状态。 const isStoreLoading = ref(false); @@ -54,27 +52,19 @@ export function useStoreDineInPage() { const isSavingTable = ref(false); const isSavingBatch = ref(false); const isCopySubmitting = ref(false); + const isConfigured = 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 loadedStoreId = ref(''); + const areas = ref([]); + const tables = ref([]); 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, - }), + cloneBasicSettings(EMPTY_BASIC_SETTINGS), ); + const selectedAreaId = ref(''); + const snapshot = ref(null); // 3. 复制弹窗状态。 const isCopyModalOpen = ref(false); @@ -121,6 +111,20 @@ export function useStoreDineInPage() { stores.value.find((store) => store.id === selectedStoreId.value)?.name ?? '', ); + const hasSelectedStore = computed(() => Boolean(selectedStoreId.value)); + const hasLoadedStoreSettings = computed(() => snapshot.value !== null); + const canOperate = computed( + () => + hasSelectedStore.value && + loadedStoreId.value === selectedStoreId.value && + hasLoadedStoreSettings.value && + !isStoreLoading.value && + !isPageLoading.value && + !isSavingArea.value && + !isSavingTable.value && + !isSavingBatch.value && + !isSavingBasic.value, + ); const selectedArea = computed( () => @@ -180,6 +184,18 @@ export function useStoreDineInPage() { tableDrawerMode.value === 'edit' ? '保存修改' : '确认添加', ); + function clearSettings() { + loadedStoreId.value = ''; + areas.value = []; + tables.value = []; + selectedAreaId.value = ''; + basicSettings.enabled = EMPTY_BASIC_SETTINGS.enabled; + basicSettings.defaultDiningMinutes = + EMPTY_BASIC_SETTINGS.defaultDiningMinutes; + basicSettings.overtimeReminderMinutes = + EMPTY_BASIC_SETTINGS.overtimeReminderMinutes; + } + // 7. 数据域动作装配。 const { buildCurrentSnapshot, @@ -191,9 +207,12 @@ export function useStoreDineInPage() { } = createDataActions({ areas, basicSettings, + clearSettings, + isConfigured, isPageLoading, isSavingBasic, isStoreLoading, + loadedStoreId, selectedAreaId, selectedStoreId, snapshot, @@ -261,7 +280,6 @@ export function useStoreDineInPage() { } = createTableActions({ areas, batchForm, - batchPreviewCodes, isBatchModalOpen, isSavingBatch, isSavingTable, @@ -304,17 +322,12 @@ export function useStoreDineInPage() { // 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 ?? ''; + clearSettings(); + isConfigured.value = false; snapshot.value = null; return; } + isConfigured.value = false; await loadStoreSettings(storeId); }); @@ -335,6 +348,7 @@ export function useStoreDineInPage() { basicSettings, batchForm, batchPreviewCodes, + canOperate, copyCandidates, copyTargetStoreIds, filteredTables, @@ -351,6 +365,7 @@ export function useStoreDineInPage() { isCopyIndeterminate, isCopyModalOpen, isCopySubmitting, + isConfigured, isPageLoading, isSavingArea, isSavingBasic, @@ -368,6 +383,9 @@ export function useStoreDineInPage() { selectedArea, selectedAreaId, selectedAreaTableCount, + hasSelectedStore, + hasLoadedStoreSettings, + loadedStoreId, selectedStoreId, selectedStoreName, setAreaDescription, diff --git a/apps/web-antd/src/views/store/dine-in/index.vue b/apps/web-antd/src/views/store/dine-in/index.vue index 5e7f987..444266e 100644 --- a/apps/web-antd/src/views/store/dine-in/index.vue +++ b/apps/web-antd/src/views/store/dine-in/index.vue @@ -32,6 +32,7 @@ const { basicSettings, batchForm, batchPreviewCodes, + canOperate, copyCandidates, copyTargetStoreIds, filteredTables, @@ -42,12 +43,14 @@ const { handleSubmitArea, handleSubmitBatch, handleSubmitTable, + hasLoadedStoreSettings, isAreaDrawerOpen, isBatchModalOpen, isCopyAllChecked, isCopyIndeterminate, isCopyModalOpen, isCopySubmitting, + isConfigured, isPageLoading, isSavingArea, isSavingBasic, @@ -125,7 +128,9 @@ function onViewQrCode(tableCode: string) { :selected-store-id="selectedStoreId" :store-options="storeOptions" :is-store-loading="isStoreLoading" - :copy-disabled="!selectedStoreId || copyCandidates.length === 0" + :copy-disabled=" + !canOperate || !isConfigured || copyCandidates.length === 0 + " @update:selected-store-id="setSelectedStoreId" @copy="openCopyModal" /> @@ -138,7 +143,14 @@ function onViewQrCode(tableCode: string) { -
+
+ + + + +
+ +
+
当前生效规则
+
+ 订单将按固定/阶梯包装费计算;商品包装费配置暂不生效。 +
+
+ 订单包装费将汇总商品维度配置;本页固定/阶梯包装费暂不生效。 +
@@ -149,8 +186,18 @@ function toNumber(value: null | number | string, fallback = 0) {
- - +
diff --git a/apps/web-antd/src/views/store/fees/components/FeesTierDrawer.vue b/apps/web-antd/src/views/store/fees/components/FeesTierDrawer.vue index 13ae48e..bf3e677 100644 --- a/apps/web-antd/src/views/store/fees/components/FeesTierDrawer.vue +++ b/apps/web-antd/src/views/store/fees/components/FeesTierDrawer.vue @@ -9,6 +9,7 @@ import type { PackagingFeeTierFormState } from '#/views/store/fees/types'; import { Button, Drawer, InputNumber } from 'ant-design-vue'; interface Props { + canOperate: boolean; form: PackagingFeeTierFormState; isSaving: boolean; onSetFee: (value: number) => void; @@ -49,6 +50,7 @@ function toNumber(value: null | number | string, fallback = 0) { :precision="2" :step="1" :controls="false" + :disabled="!props.canOperate" class="drawer-input" placeholder="起始金额" @update:value=" @@ -63,6 +65,7 @@ function toNumber(value: null | number | string, fallback = 0) { :precision="2" :step="1" :controls="false" + :disabled="!props.canOperate" class="drawer-input" placeholder="结束金额(留空表示无上限)" @update:value=" @@ -90,6 +93,7 @@ function toNumber(value: null | number | string, fallback = 0) { :precision="2" :step="0.5" :controls="false" + :disabled="!props.canOperate" class="drawer-input" placeholder="如:2.00" @update:value=" @@ -102,12 +106,16 @@ function toNumber(value: null | number | string, fallback = 0) {