feat(project): pickup mode switch with auto-save and rollback
This commit is contained in:
@@ -101,6 +101,12 @@ export interface SavePickupFineRuleParams {
|
|||||||
storeId: string;
|
storeId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 保存自提模式参数 */
|
||||||
|
export interface SavePickupModeParams {
|
||||||
|
mode: PickupMode;
|
||||||
|
storeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** 复制自提设置参数 */
|
/** 复制自提设置参数 */
|
||||||
export interface CopyStorePickupSettingsParams {
|
export interface CopyStorePickupSettingsParams {
|
||||||
sourceStoreId: string;
|
sourceStoreId: string;
|
||||||
@@ -131,6 +137,11 @@ export async function savePickupFineRuleApi(data: SavePickupFineRuleParams) {
|
|||||||
return requestClient.post('/store/pickup/fine-rule/save', data);
|
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(
|
export async function copyStorePickupSettingsApi(
|
||||||
data: CopyStorePickupSettingsParams,
|
data: CopyStorePickupSettingsParams,
|
||||||
|
|||||||
@@ -6,7 +6,11 @@
|
|||||||
*/
|
*/
|
||||||
import type { PickupMode } from '#/api/store-pickup';
|
import type { PickupMode } from '#/api/store-pickup';
|
||||||
|
|
||||||
|
import { Switch } from 'ant-design-vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
disabled?: boolean;
|
||||||
|
isSwitching?: boolean;
|
||||||
mode: PickupMode;
|
mode: PickupMode;
|
||||||
options: Array<{ label: string; value: PickupMode }>;
|
options: Array<{ label: string; value: PickupMode }>;
|
||||||
}
|
}
|
||||||
@@ -16,19 +20,58 @@ const props = defineProps<Props>();
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: 'change', mode: PickupMode): void;
|
(event: 'change', mode: PickupMode): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
function getModeLabel(mode: PickupMode) {
|
||||||
|
return props.options.find((item) => item.value === mode)?.label ?? mode;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div class="pickup-mode-switch-wrap">
|
||||||
<div class="pickup-mode-switch">
|
<div class="pickup-mode-switch">
|
||||||
<button
|
<button
|
||||||
v-for="item in props.options"
|
|
||||||
:key="item.value"
|
|
||||||
type="button"
|
type="button"
|
||||||
class="pickup-mode-item"
|
class="pickup-mode-item"
|
||||||
:class="{ active: props.mode === item.value }"
|
:class="{ active: props.mode === 'big' }"
|
||||||
@click="emit('change', item.value)"
|
:disabled="props.disabled || props.isSwitching"
|
||||||
|
@click="emit('change', 'big')"
|
||||||
>
|
>
|
||||||
{{ item.label }}
|
{{ 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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="pickup-mode-guide"
|
||||||
|
:class="
|
||||||
|
props.mode === 'fine'
|
||||||
|
? 'pickup-mode-guide-fine'
|
||||||
|
: 'pickup-mode-guide-big'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<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>
|
</template>
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ import type {
|
|||||||
|
|
||||||
import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue';
|
import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { savePickupModeApi } from '#/api/store-pickup';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ALL_WEEK_DAYS,
|
ALL_WEEK_DAYS,
|
||||||
DEFAULT_FINE_RULE,
|
DEFAULT_FINE_RULE,
|
||||||
@@ -35,6 +39,7 @@ import {
|
|||||||
cloneBasicSettings,
|
cloneBasicSettings,
|
||||||
cloneFineRule,
|
cloneFineRule,
|
||||||
clonePreviewDays,
|
clonePreviewDays,
|
||||||
|
createSettingsSnapshot,
|
||||||
createSlotId,
|
createSlotId,
|
||||||
formatDayOfWeeksText,
|
formatDayOfWeeksText,
|
||||||
} from './pickup-page/helpers';
|
} from './pickup-page/helpers';
|
||||||
@@ -47,6 +52,7 @@ export function useStorePickupPage() {
|
|||||||
const isSavingBasic = ref(false);
|
const isSavingBasic = ref(false);
|
||||||
const isSavingSlots = ref(false);
|
const isSavingSlots = ref(false);
|
||||||
const isSavingFineRule = ref(false);
|
const isSavingFineRule = ref(false);
|
||||||
|
const isModeSwitching = ref(false);
|
||||||
const isCopySubmitting = ref(false);
|
const isCopySubmitting = ref(false);
|
||||||
const isConfigured = ref(false);
|
const isConfigured = ref(false);
|
||||||
const loadedStoreId = ref('');
|
const loadedStoreId = ref('');
|
||||||
@@ -134,7 +140,8 @@ export function useStorePickupPage() {
|
|||||||
!isPageLoading.value &&
|
!isPageLoading.value &&
|
||||||
!isSavingBasic.value &&
|
!isSavingBasic.value &&
|
||||||
!isSavingSlots.value &&
|
!isSavingSlots.value &&
|
||||||
!isSavingFineRule.value,
|
!isSavingFineRule.value &&
|
||||||
|
!isModeSwitching.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
function clearSettings() {
|
function clearSettings() {
|
||||||
@@ -236,9 +243,43 @@ export function useStorePickupPage() {
|
|||||||
selectedStoreId.value = value;
|
selectedStoreId.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPickupMode(value: 'big' | 'fine') {
|
async function setPickupMode(value: 'big' | 'fine') {
|
||||||
if (!canOperate.value) return;
|
if (!canOperate.value) return;
|
||||||
|
if (value === pickupMode.value) return;
|
||||||
|
if (!selectedStoreId.value) return;
|
||||||
|
|
||||||
|
const currentStoreId = selectedStoreId.value;
|
||||||
|
const previousMode = pickupMode.value;
|
||||||
pickupMode.value = 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) {
|
function setAllowSameDayPickup(value: boolean) {
|
||||||
@@ -289,12 +330,14 @@ export function useStorePickupPage() {
|
|||||||
// 8. 门店切换时自动刷新配置。
|
// 8. 门店切换时自动刷新配置。
|
||||||
watch(selectedStoreId, async (storeId) => {
|
watch(selectedStoreId, async (storeId) => {
|
||||||
if (!storeId) {
|
if (!storeId) {
|
||||||
|
isModeSwitching.value = false;
|
||||||
loadedStoreId.value = '';
|
loadedStoreId.value = '';
|
||||||
isConfigured.value = false;
|
isConfigured.value = false;
|
||||||
clearSettings();
|
clearSettings();
|
||||||
snapshot.value = null;
|
snapshot.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
isModeSwitching.value = false;
|
||||||
loadedStoreId.value = '';
|
loadedStoreId.value = '';
|
||||||
isConfigured.value = false;
|
isConfigured.value = false;
|
||||||
snapshot.value = null;
|
snapshot.value = null;
|
||||||
@@ -351,6 +394,7 @@ export function useStorePickupPage() {
|
|||||||
isPageLoading,
|
isPageLoading,
|
||||||
isSavingBasic,
|
isSavingBasic,
|
||||||
isSavingFineRule,
|
isSavingFineRule,
|
||||||
|
isModeSwitching,
|
||||||
isSavingSlots,
|
isSavingSlots,
|
||||||
isSlotDaySelected,
|
isSlotDaySelected,
|
||||||
isSlotDrawerOpen,
|
isSlotDrawerOpen,
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const {
|
|||||||
isPageLoading,
|
isPageLoading,
|
||||||
isSavingBasic,
|
isSavingBasic,
|
||||||
isSavingFineRule,
|
isSavingFineRule,
|
||||||
|
isModeSwitching,
|
||||||
isSavingSlots,
|
isSavingSlots,
|
||||||
isSlotDaySelected,
|
isSlotDaySelected,
|
||||||
isSlotDrawerOpen,
|
isSlotDrawerOpen,
|
||||||
@@ -129,6 +130,8 @@ const {
|
|||||||
<PickupModeSwitch
|
<PickupModeSwitch
|
||||||
:mode="pickupMode"
|
:mode="pickupMode"
|
||||||
:options="PICKUP_MODE_OPTIONS"
|
:options="PICKUP_MODE_OPTIONS"
|
||||||
|
:disabled="!canOperate"
|
||||||
|
:is-switching="isModeSwitching"
|
||||||
@change="setPickupMode"
|
@change="setPickupMode"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
/* 文件职责:自提设置页面模式切换样式。 */
|
/* 文件职责:自提设置页面模式切换样式。 */
|
||||||
.page-store-pickup {
|
.page-store-pickup {
|
||||||
|
.pickup-mode-switch-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.pickup-mode-switch {
|
.pickup-mode-switch {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: 2px;
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
margin-bottom: 16px;
|
|
||||||
background: #f8f9fb;
|
background: #f8f9fb;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
@@ -21,10 +28,44 @@
|
|||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pickup-mode-item:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
.pickup-mode-item.active {
|
.pickup-mode-item.active {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1677ff;
|
color: #1677ff;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 1px 2px rgb(15 23 42 / 10%);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
.pickup-mode-switch-wrap {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.pickup-mode-switch {
|
.pickup-mode-switch {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pickup-mode-item {
|
.pickup-mode-item {
|
||||||
|
|||||||
Reference in New Issue
Block a user