diff --git a/apps/web-antd/src/api/store-fees/index.ts b/apps/web-antd/src/api/store-fees/index.ts index 897c772..b33efee 100644 --- a/apps/web-antd/src/api/store-fees/index.ts +++ b/apps/web-antd/src/api/store-fees/index.ts @@ -88,6 +88,14 @@ export interface SaveStoreFeesSettingsParams { storeId: string; } +/** 保存包装费收取方式参数 */ +export interface SaveStoreFeesModeParams { + /** 包装费模式 */ + packagingFeeMode: PackagingFeeMode; + /** 门店 ID */ + storeId: string; +} + /** 复制费用设置参数 */ export interface CopyStoreFeesSettingsParams { sourceStoreId: string; @@ -108,6 +116,11 @@ export async function saveStoreFeesSettingsApi( return requestClient.post('/store/fees/save', data); } +/** 保存包装费收取方式 */ +export async function saveStoreFeesModeApi(data: SaveStoreFeesModeParams) { + return requestClient.post('/store/fees/mode/save', data); +} + /** 复制费用设置到其他门店 */ export async function copyStoreFeesSettingsApi( data: CopyStoreFeesSettingsParams, diff --git a/apps/web-antd/src/views/store/fees/components/FeesPackagingCard.vue b/apps/web-antd/src/views/store/fees/components/FeesPackagingCard.vue index f0d70cf..0449690 100644 --- a/apps/web-antd/src/views/store/fees/components/FeesPackagingCard.vue +++ b/apps/web-antd/src/views/store/fees/components/FeesPackagingCard.vue @@ -14,6 +14,7 @@ interface Props { formatCurrency: (value: number) => string; formatTierRange: (tier: PackagingFeeTierDto) => string; isSaving: boolean; + isSwitchingMode: boolean; onSetFixedPackagingFee: (value: number) => void; onSetPackagingMode: (value: 'item' | 'order') => void; onSetTieredEnabled: (value: boolean) => void; @@ -36,6 +37,20 @@ function toNumber(value: null | number | string, fallback = 0) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : fallback; } + +function getModeLabel(mode: 'item' | 'order') { + return mode === 'order' ? '按订单收取' : '按商品收取'; +} + +function getModeHint(mode: 'item' | 'order') { + return mode === 'order' ? '按订单金额统一计算包装费' : '按商品维度汇总包装费'; +} + +function getModeEffect(mode: 'item' | 'order') { + return mode === 'order' + ? '固定包装费/阶梯包装费生效,商品包装费暂不生效。' + : '商品包装费生效,本页固定包装费/阶梯包装费暂不生效。'; +} -
- - - - - -
- -
-
当前生效规则
-
- 订单将按固定/阶梯包装费计算;商品包装费配置暂不生效。 +
+
+
包装费收取方式
+
+ {{ + props.isSwitchingMode + ? '切换中...' + : `当前:${getModeLabel(props.packagingMode)}` + }} +
-
- 订单包装费将汇总商品维度配置;本页固定/阶梯包装费暂不生效。 + +
+ + + +
+ +
+ 切换后效果:{{ getModeEffect(props.packagingMode) }}
diff --git a/apps/web-antd/src/views/store/fees/composables/useStoreFeesPage.ts b/apps/web-antd/src/views/store/fees/composables/useStoreFeesPage.ts index 18b76fe..6c9c40a 100644 --- a/apps/web-antd/src/views/store/fees/composables/useStoreFeesPage.ts +++ b/apps/web-antd/src/views/store/fees/composables/useStoreFeesPage.ts @@ -17,6 +17,8 @@ import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue'; import { message, Modal } from 'ant-design-vue'; +import { saveStoreFeesModeApi } from '#/api/store-fees'; + import { PACKAGING_MODE_OPTIONS } from './fees-page/constants'; import { createCopyActions } from './fees-page/copy-actions'; import { createDataActions } from './fees-page/data-actions'; @@ -24,6 +26,7 @@ import { cloneFeesForm, cloneOtherFees, cloneTiers, + createSettingsSnapshot, formatCurrency, formatTierRange, normalizeMoney, @@ -49,8 +52,6 @@ const EMPTY_FEES_SETTINGS: StoreFeesFormState = { }, }, }; -const PACKAGING_MODE_SWITCH_CONFIRM_KEY = - 'store-fees-packaging-mode-switch-confirmed'; export function useStoreFeesPage() { // 1. 页面 loading / submitting 状态。 @@ -59,6 +60,7 @@ export function useStoreFeesPage() { const isSavingDelivery = ref(false); const isSavingPackaging = ref(false); const isSavingOther = ref(false); + const isSwitchingPackagingMode = ref(false); const isCopySubmitting = ref(false); const isConfigured = ref(false); @@ -104,7 +106,8 @@ export function useStoreFeesPage() { !isPageLoading.value && !isSavingDelivery.value && !isSavingPackaging.value && - !isSavingOther.value, + !isSavingOther.value && + !isSwitchingPackagingMode.value, ); const copyCandidates = computed(() => @@ -129,29 +132,11 @@ export function useStoreFeesPage() { const isOrderMode = computed(() => form.packagingFeeMode === 'order'); - function hasConfirmedPackagingModeSwitch() { - try { - return ( - window.localStorage.getItem(PACKAGING_MODE_SWITCH_CONFIRM_KEY) === '1' - ); - } catch { - return false; - } - } - - function markPackagingModeSwitchConfirmed() { - try { - window.localStorage.setItem(PACKAGING_MODE_SWITCH_CONFIRM_KEY, '1'); - } catch { - // 忽略本地存储异常,不影响当前切换。 - } - } - function getPackagingModeConfirmContent(value: PackagingFeeMode) { if (value === 'order') { - return '切换后订单将按本页固定/阶梯包装费计算,商品包装费配置将暂不生效。'; + return '切换后将按订单收取包装费,固定/阶梯包装费立即生效;商品维度包装费将暂不生效。'; } - return '切换后订单包装费将汇总商品维度配置,本页固定/阶梯包装费将暂不生效。'; + return '切换后将按商品收取包装费,商品配置的包装费立即生效;本页固定/阶梯包装费将暂不生效。'; } function clearSettings() { @@ -237,21 +222,52 @@ export function useStoreFeesPage() { function setPackagingMode(value: PackagingFeeMode) { if (!canOperate.value) return; if (value === form.packagingFeeMode) return; + if (!selectedStoreId.value) return; - const applyMode = () => setPackagingFeeMode(value); - if (hasConfirmedPackagingModeSwitch()) { - applyMode(); - return; - } + const currentStoreId = selectedStoreId.value; + const previousMode = form.packagingFeeMode; + const previousOrderMode = form.orderPackagingFeeMode; Modal.confirm({ title: '确认切换包装费收取方式?', content: getPackagingModeConfirmContent(value), okText: '确认切换', cancelText: '取消', - onOk() { - applyMode(); - markPackagingModeSwitchConfirmed(); + async onOk() { + if (isSwitchingPackagingMode.value) return; + isSwitchingPackagingMode.value = true; + setPackagingFeeMode(value); + + try { + await saveStoreFeesModeApi({ + storeId: currentStoreId, + packagingFeeMode: value, + }); + + if (selectedStoreId.value === currentStoreId) { + isConfigured.value = true; + loadedStoreId.value = currentStoreId; + if (snapshot.value) { + snapshot.value = { + ...snapshot.value, + packagingFeeMode: form.packagingFeeMode, + orderPackagingFeeMode: form.orderPackagingFeeMode, + }; + } else { + snapshot.value = createSettingsSnapshot(form); + } + message.success('包装费收取方式已切换'); + } + } catch (error) { + console.error(error); + if (selectedStoreId.value === currentStoreId) { + form.packagingFeeMode = previousMode; + form.orderPackagingFeeMode = previousOrderMode; + } + message.error('包装费收取方式切换失败,请稍后重试'); + } finally { + isSwitchingPackagingMode.value = false; + } }, }); } @@ -369,6 +385,7 @@ export function useStoreFeesPage() { /** 切换门店时同步拉取配置。 */ watch(selectedStoreId, async (storeId) => { + isSwitchingPackagingMode.value = false; if (!storeId) { clearSettings(); isConfigured.value = false; @@ -407,6 +424,7 @@ export function useStoreFeesPage() { isSavingDelivery, isSavingOther, isSavingPackaging, + isSwitchingPackagingMode, isStoreLoading, isTierDrawerOpen, loadedStoreId, diff --git a/apps/web-antd/src/views/store/fees/index.vue b/apps/web-antd/src/views/store/fees/index.vue index 5f7edca..6560ef9 100644 --- a/apps/web-antd/src/views/store/fees/index.vue +++ b/apps/web-antd/src/views/store/fees/index.vue @@ -38,6 +38,7 @@ const { isSavingDelivery, isSavingOther, isSavingPackaging, + isSwitchingPackagingMode, isStoreLoading, isTierDrawerOpen, onDeleteTier, @@ -126,6 +127,7 @@ function onEditTier(tier: PackagingFeeTierDto) { :fixed-packaging-fee="form.fixedPackagingFee" :tiers="form.packagingFeeTiers" :is-saving="isSavingPackaging" + :is-switching-mode="isSwitchingPackagingMode" :format-currency="formatCurrency" :format-tier-range="formatTierRange" :on-set-packaging-mode="setPackagingMode" diff --git a/apps/web-antd/src/views/store/fees/styles/packaging.less b/apps/web-antd/src/views/store/fees/styles/packaging.less index 8990333..e4806b6 100644 --- a/apps/web-antd/src/views/store/fees/styles/packaging.less +++ b/apps/web-antd/src/views/store/fees/styles/packaging.less @@ -1,58 +1,110 @@ /* 文件职责:包装费卡片样式。 */ .page-store-fees { - .packaging-mode-toggle-row { - display: inline-flex; + .packaging-mode-panel { + display: flex; + flex-direction: column; + gap: 12px; + padding: 14px; + margin-bottom: 16px; + background: #fff; + border: 1px solid #e5e9f0; + border-radius: 12px; + } + + .packaging-mode-header { + display: flex; gap: 10px; align-items: center; - margin-bottom: 12px; + justify-content: space-between; } - .mode-toggle-label { - padding: 0; - font-size: 13px; - color: #4b5563; - cursor: pointer; - background: transparent; - border: none; - transition: color 0.2s ease; - } - - .mode-toggle-label.active { - font-weight: 600; - color: #1677ff; - } - - .mode-toggle-label:disabled { - cursor: not-allowed; - opacity: 0.7; - } - - .packaging-mode-guide { - padding: 10px 12px; - margin-bottom: 16px; - border: 1px solid #dbeafe; - border-radius: 8px; - } - - .packaging-mode-guide-order { - background: #f5f9ff; - } - - .packaging-mode-guide-item { - background: #f6ffed; - border-color: #ccebd2; - } - - .packaging-mode-guide .guide-title { - margin-bottom: 4px; - font-size: 12px; + .packaging-mode-title { + font-size: 14px; font-weight: 600; color: #1f2937; } - .packaging-mode-guide .guide-desc { + .packaging-mode-badge { + padding: 3px 10px; font-size: 12px; - color: #4b5563; + line-height: 1.5; + color: #155eef; + background: #eff4ff; + border: 1px solid #c7d7fe; + border-radius: 999px; + } + + .packaging-mode-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + } + + .packaging-mode-card { + display: flex; + flex-direction: column; + gap: 6px; + min-height: 92px; + padding: 12px; + text-align: left; + cursor: pointer; + background: #fff; + border: 1px solid #d8dde6; + border-radius: 10px; + transition: all 0.18s ease; + } + + .packaging-mode-card:disabled { + cursor: not-allowed; + opacity: 0.6; + } + + .packaging-mode-card:hover:not(:disabled) { + border-color: #86a6ff; + box-shadow: 0 4px 12px rgb(17 24 39 / 8%); + } + + .packaging-mode-card.active { + background: linear-gradient(160deg, #ecf3ff 0%, #f8fbff 100%); + border-color: #5b8bff; + box-shadow: 0 6px 18px rgb(32 94 255 / 15%); + } + + .packaging-mode-card-title { + display: inline-flex; + gap: 8px; + align-items: center; + font-size: 14px; + font-weight: 600; + color: #111827; + } + + .packaging-mode-dot { + width: 8px; + height: 8px; + background: #94a3b8; + border-radius: 50%; + } + + .packaging-mode-card.active .packaging-mode-dot { + background: #2563eb; + box-shadow: 0 0 0 4px rgb(37 99 235 / 16%); + } + + .packaging-mode-card-hint { + font-size: 12px; + line-height: 1.55; + color: #556070; + } + + .packaging-mode-effect { + padding: 8px 10px; + font-size: 12px; + line-height: 1.5; + color: #1f3a8a; + background: #f5f8ff; + border: 1px solid #dbe7ff; + border-radius: 8px; } .packaging-tier-block { diff --git a/apps/web-antd/src/views/store/fees/styles/responsive.less b/apps/web-antd/src/views/store/fees/styles/responsive.less index 46a3754..2bfd25f 100644 --- a/apps/web-antd/src/views/store/fees/styles/responsive.less +++ b/apps/web-antd/src/views/store/fees/styles/responsive.less @@ -23,14 +23,25 @@ width: 100%; } - .packaging-mode-toggle-row { - display: flex; - justify-content: space-between; + .packaging-mode-panel { + gap: 10px; + padding: 12px; + } + + .packaging-mode-header { + flex-direction: column; + gap: 6px; + align-items: flex-start; + } + + .packaging-mode-grid { + grid-template-columns: 1fr; width: 100%; } - .mode-toggle-label { - text-align: center; + .packaging-mode-card { + min-height: 84px; + padding: 10px 11px; } } } diff --git a/apps/web-antd/src/views/store/pickup/components/PickupModeSwitch.vue b/apps/web-antd/src/views/store/pickup/components/PickupModeSwitch.vue index 1f9c9ed..fb5ee9b 100644 --- a/apps/web-antd/src/views/store/pickup/components/PickupModeSwitch.vue +++ b/apps/web-antd/src/views/store/pickup/components/PickupModeSwitch.vue @@ -6,8 +6,6 @@ */ import type { PickupMode } from '#/api/store-pickup'; -import { Switch } from 'ant-design-vue'; - interface Props { disabled?: boolean; isSwitching?: boolean; @@ -24,54 +22,53 @@ const emit = defineEmits<{ function getModeLabel(mode: PickupMode) { return props.options.find((item) => item.value === mode)?.label ?? mode; } + +function getModeHint(mode: PickupMode) { + return mode === 'big' + ? '按时段统一预约,适合固定营业节奏' + : '按精细时间点预约,适合高峰分流'; +} diff --git a/apps/web-antd/src/views/store/pickup/composables/useStorePickupPage.ts b/apps/web-antd/src/views/store/pickup/composables/useStorePickupPage.ts index bac044b..1e06b53 100644 --- a/apps/web-antd/src/views/store/pickup/composables/useStorePickupPage.ts +++ b/apps/web-antd/src/views/store/pickup/composables/useStorePickupPage.ts @@ -18,7 +18,7 @@ import type { import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue'; -import { message } from 'ant-design-vue'; +import { message, Modal } from 'ant-design-vue'; import { savePickupModeApi } from '#/api/store-pickup'; @@ -243,43 +243,67 @@ export function useStorePickupPage() { selectedStoreId.value = value; } - async function setPickupMode(value: 'big' | 'fine') { + function getPickupModeConfirmContent(value: 'big' | 'fine') { + if (value === 'big') { + return '切换后将按大时段统一预约,精细时段规则与预览将暂不生效。'; + } + return '切换后将按精细时段预约,大时段配置将暂不生效。'; + } + + function setPickupMode(value: 'big' | 'fine') { if (!canOperate.value) return; if (value === pickupMode.value) return; if (!selectedStoreId.value) return; const currentStoreId = selectedStoreId.value; const previousMode = pickupMode.value; - pickupMode.value = value; - isModeSwitching.value = true; - try { - await savePickupModeApi({ - storeId: currentStoreId, - mode: value, - }); + Modal.confirm({ + title: '确认切换自提预约模式?', + content: getPickupModeConfirmContent(value), + okText: '确认切换', + cancelText: '取消', + async onOk() { + if (isModeSwitching.value) return; + isModeSwitching.value = true; + pickupMode.value = value; - if (selectedStoreId.value === currentStoreId) { - isConfigured.value = true; - loadedStoreId.value = currentStoreId; - snapshot.value = createSettingsSnapshot({ - mode: value, - basicSettings, - bigSlots: bigSlots.value, - fineRule, - previewDays: previewDays.value, - }); - message.success('自提模式已切换'); - } - } catch (error) { - console.error(error); - if (selectedStoreId.value === currentStoreId) { - pickupMode.value = previousMode; - message.error('自提模式切换失败,请稍后重试'); - } - } finally { - isModeSwitching.value = false; - } + try { + await savePickupModeApi({ + storeId: currentStoreId, + mode: value, + }); + + if (selectedStoreId.value === currentStoreId) { + isConfigured.value = true; + loadedStoreId.value = currentStoreId; + if (snapshot.value) { + snapshot.value = { + ...snapshot.value, + mode: value, + }; + } else { + snapshot.value = createSettingsSnapshot({ + mode: value, + basicSettings, + bigSlots: bigSlots.value, + fineRule, + previewDays: previewDays.value, + }); + } + message.success('自提预约模式已切换'); + } + } catch (error) { + console.error(error); + if (selectedStoreId.value === currentStoreId) { + pickupMode.value = previousMode; + } + message.error('自提预约模式切换失败,请稍后重试'); + } finally { + isModeSwitching.value = false; + } + }, + }); } function setAllowSameDayPickup(value: boolean) { diff --git a/apps/web-antd/src/views/store/pickup/styles/mode.less b/apps/web-antd/src/views/store/pickup/styles/mode.less index c85dd4e..604e1e7 100644 --- a/apps/web-antd/src/views/store/pickup/styles/mode.less +++ b/apps/web-antd/src/views/store/pickup/styles/mode.less @@ -1,71 +1,105 @@ /* 文件职责:自提设置页面模式切换样式。 */ .page-store-pickup { - .pickup-mode-switch-wrap { + .pickup-mode-panel { display: flex; flex-direction: column; - gap: 10px; + gap: 12px; + padding: 14px; margin-bottom: 16px; + background: #fff; + border: 1px solid #e5e9f0; + border-radius: 12px; } - .pickup-mode-switch { - display: inline-flex; + .pickup-mode-header { + display: flex; gap: 10px; align-items: center; - padding: 3px; - background: #f8f9fb; - border-radius: 8px; + justify-content: space-between; } - .pickup-mode-item { - min-width: 118px; - padding: 6px 18px; - font-size: 13px; - color: #4b5563; - cursor: pointer; - background: transparent; - border: none; - border-radius: 6px; - transition: all 0.2s ease; - } - - .pickup-mode-item:disabled { - cursor: not-allowed; - opacity: 0.6; - } - - .pickup-mode-item.active { - font-weight: 600; - color: #1677ff; - background: #fff; - box-shadow: 0 1px 2px rgb(15 23 42 / 10%); - } - - .pickup-mode-guide { - padding: 10px 12px; - border: 1px solid transparent; - border-radius: 8px; - } - - .pickup-mode-guide-big { - background: #f5f9ff; - border-color: #bfdbfe; - } - - .pickup-mode-guide-fine { - background: #effff7; - border-color: #bbf7d0; - } - - .pickup-mode-guide .guide-title { - margin-bottom: 2px; - font-size: 13px; + .pickup-mode-title { + font-size: 14px; font-weight: 600; color: #1f2937; } - .pickup-mode-guide .guide-desc { + .pickup-mode-badge { + padding: 3px 10px; + font-size: 12px; + line-height: 1.5; + color: #155eef; + background: #eff4ff; + border: 1px solid #c7d7fe; + border-radius: 999px; + } + + .pickup-mode-panel.switching .pickup-mode-badge { + color: #9a3412; + background: #fff7ed; + border-color: #fdba74; + } + + .pickup-mode-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + } + + .pickup-mode-card { + display: flex; + flex-direction: column; + gap: 6px; + min-height: 92px; + padding: 12px; + text-align: left; + cursor: pointer; + background: #fff; + border: 1px solid #d8dde6; + border-radius: 10px; + transition: all 0.18s ease; + } + + .pickup-mode-card:disabled { + cursor: not-allowed; + opacity: 0.6; + } + + .pickup-mode-card:hover:not(:disabled) { + border-color: #86a6ff; + box-shadow: 0 4px 12px rgb(17 24 39 / 8%); + } + + .pickup-mode-card.active { + background: linear-gradient(160deg, #ecf3ff 0%, #f8fbff 100%); + border-color: #5b8bff; + box-shadow: 0 6px 18px rgb(32 94 255 / 15%); + } + + .pickup-mode-card-title { + display: inline-flex; + gap: 8px; + align-items: center; + font-size: 14px; + font-weight: 600; + color: #111827; + } + + .pickup-mode-dot { + width: 8px; + height: 8px; + background: #94a3b8; + border-radius: 50%; + } + + .pickup-mode-card.active .pickup-mode-dot { + background: #2563eb; + box-shadow: 0 0 0 4px rgb(37 99 235 / 16%); + } + + .pickup-mode-card-hint { font-size: 12px; line-height: 1.55; - color: #4b5563; + color: #556070; } } diff --git a/apps/web-antd/src/views/store/pickup/styles/responsive.less b/apps/web-antd/src/views/store/pickup/styles/responsive.less index b9f4202..c281f87 100644 --- a/apps/web-antd/src/views/store/pickup/styles/responsive.less +++ b/apps/web-antd/src/views/store/pickup/styles/responsive.less @@ -7,20 +7,25 @@ } @media (max-width: 768px) { - .pickup-mode-switch-wrap { - gap: 8px; + .pickup-mode-panel { + gap: 10px; + padding: 12px; } - .pickup-mode-switch { - display: flex; - justify-content: space-between; + .pickup-mode-header { + flex-direction: column; + gap: 6px; + align-items: flex-start; + } + + .pickup-mode-grid { + grid-template-columns: 1fr; width: 100%; } - .pickup-mode-item { - flex: 1; - min-width: 0; - text-align: center; + .pickup-mode-card { + min-height: 84px; + padding: 10px 11px; } .pickup-form-row {