feat(project): pickup mode switch with auto-save and rollback

This commit is contained in:
2026-02-20 10:09:58 +08:00
parent 22d1a44683
commit 344ebc3910
6 changed files with 161 additions and 14 deletions

View File

@@ -101,6 +101,12 @@ export interface SavePickupFineRuleParams {
storeId: string;
}
/** 保存自提模式参数 */
export interface SavePickupModeParams {
mode: PickupMode;
storeId: string;
}
/** 复制自提设置参数 */
export interface CopyStorePickupSettingsParams {
sourceStoreId: string;
@@ -131,6 +137,11 @@ export async function savePickupFineRuleApi(data: SavePickupFineRuleParams) {
return requestClient.post('/store/pickup/fine-rule/save', data);
}
/** 保存自提模式 */
export async function savePickupModeApi(data: SavePickupModeParams) {
return requestClient.post('/store/pickup/mode/save', data);
}
/** 复制到其他门店 */
export async function copyStorePickupSettingsApi(
data: CopyStorePickupSettingsParams,

View File

@@ -6,7 +6,11 @@
*/
import type { PickupMode } from '#/api/store-pickup';
import { Switch } from 'ant-design-vue';
interface Props {
disabled?: boolean;
isSwitching?: boolean;
mode: PickupMode;
options: Array<{ label: string; value: PickupMode }>;
}
@@ -16,19 +20,58 @@ const props = defineProps<Props>();
const emit = defineEmits<{
(event: 'change', mode: PickupMode): void;
}>();
function getModeLabel(mode: PickupMode) {
return props.options.find((item) => item.value === mode)?.label ?? mode;
}
</script>
<template>
<div class="pickup-mode-switch">
<button
v-for="item in props.options"
:key="item.value"
type="button"
class="pickup-mode-item"
:class="{ active: props.mode === item.value }"
@click="emit('change', item.value)"
<div class="pickup-mode-switch-wrap">
<div class="pickup-mode-switch">
<button
type="button"
class="pickup-mode-item"
:class="{ active: props.mode === 'big' }"
:disabled="props.disabled || props.isSwitching"
@click="emit('change', 'big')"
>
{{ getModeLabel('big') }}
</button>
<Switch
:checked="props.mode === 'fine'"
:loading="props.isSwitching"
:disabled="props.disabled || props.isSwitching"
@update:checked="(checked) => emit('change', checked ? 'fine' : 'big')"
/>
<button
type="button"
class="pickup-mode-item"
:class="{ active: props.mode === 'fine' }"
:disabled="props.disabled || props.isSwitching"
@click="emit('change', 'fine')"
>
{{ getModeLabel('fine') }}
</button>
</div>
<div
class="pickup-mode-guide"
:class="
props.mode === 'fine'
? 'pickup-mode-guide-fine'
: 'pickup-mode-guide-big'
"
>
{{ item.label }}
</button>
<div class="guide-title">当前生效规则</div>
<div v-if="props.mode === 'big'" class="guide-desc">
顾客按大时段进行预约系统按已配置时段控制可约时间与容量
</div>
<div v-else class="guide-desc">
顾客按精细时间窗口预约系统按精细规则自动生成可约时间点
</div>
</div>
</div>
</template>

View File

@@ -18,6 +18,10 @@ import type {
import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import { savePickupModeApi } from '#/api/store-pickup';
import {
ALL_WEEK_DAYS,
DEFAULT_FINE_RULE,
@@ -35,6 +39,7 @@ import {
cloneBasicSettings,
cloneFineRule,
clonePreviewDays,
createSettingsSnapshot,
createSlotId,
formatDayOfWeeksText,
} from './pickup-page/helpers';
@@ -47,6 +52,7 @@ export function useStorePickupPage() {
const isSavingBasic = ref(false);
const isSavingSlots = ref(false);
const isSavingFineRule = ref(false);
const isModeSwitching = ref(false);
const isCopySubmitting = ref(false);
const isConfigured = ref(false);
const loadedStoreId = ref('');
@@ -134,7 +140,8 @@ export function useStorePickupPage() {
!isPageLoading.value &&
!isSavingBasic.value &&
!isSavingSlots.value &&
!isSavingFineRule.value,
!isSavingFineRule.value &&
!isModeSwitching.value,
);
function clearSettings() {
@@ -236,9 +243,43 @@ export function useStorePickupPage() {
selectedStoreId.value = value;
}
function setPickupMode(value: 'big' | 'fine') {
async 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,
});
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;
}
}
function setAllowSameDayPickup(value: boolean) {
@@ -289,12 +330,14 @@ export function useStorePickupPage() {
// 8. 门店切换时自动刷新配置。
watch(selectedStoreId, async (storeId) => {
if (!storeId) {
isModeSwitching.value = false;
loadedStoreId.value = '';
isConfigured.value = false;
clearSettings();
snapshot.value = null;
return;
}
isModeSwitching.value = false;
loadedStoreId.value = '';
isConfigured.value = false;
snapshot.value = null;
@@ -351,6 +394,7 @@ export function useStorePickupPage() {
isPageLoading,
isSavingBasic,
isSavingFineRule,
isModeSwitching,
isSavingSlots,
isSlotDaySelected,
isSlotDrawerOpen,

View File

@@ -46,6 +46,7 @@ const {
isPageLoading,
isSavingBasic,
isSavingFineRule,
isModeSwitching,
isSavingSlots,
isSlotDaySelected,
isSlotDrawerOpen,
@@ -129,6 +130,8 @@ const {
<PickupModeSwitch
:mode="pickupMode"
:options="PICKUP_MODE_OPTIONS"
:disabled="!canOperate"
:is-switching="isModeSwitching"
@change="setPickupMode"
/>

View File

@@ -1,10 +1,17 @@
/* 文件职责:自提设置页面模式切换样式。 */
.page-store-pickup {
.pickup-mode-switch-wrap {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 16px;
}
.pickup-mode-switch {
display: inline-flex;
gap: 2px;
gap: 10px;
align-items: center;
padding: 3px;
margin-bottom: 16px;
background: #f8f9fb;
border-radius: 8px;
}
@@ -21,10 +28,44 @@
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-radius: 8px;
border: 1px solid transparent;
}
.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;
font-weight: 600;
color: #1f2937;
}
.pickup-mode-guide .guide-desc {
font-size: 12px;
line-height: 1.55;
color: #4b5563;
}
}

View File

@@ -7,9 +7,14 @@
}
@media (max-width: 768px) {
.pickup-mode-switch-wrap {
gap: 8px;
}
.pickup-mode-switch {
display: flex;
width: 100%;
justify-content: space-between;
}
.pickup-mode-item {