fix(project): remove pickup mock preview and sync with api

This commit is contained in:
2026-02-19 18:21:36 +08:00
parent 627d292d86
commit fc1739a00d
6 changed files with 41 additions and 259 deletions

View File

@@ -22,7 +22,7 @@ const emit = defineEmits<{
(event: 'selectDate', date: string): void;
}>();
const previewSubtitle = '根据规则自动生成,以下为预览效果';
const previewSubtitle = '来自后端规则计算(未扣减实际预约)';
function getPreviewStatusText(slot: PickupPreviewSlotDto) {
if (slot.status === 'expired') return '已过期';

View File

@@ -7,7 +7,6 @@ import type {
PickupBasicSettingsDto,
PickupFineRuleDto,
PickupMode,
PickupSlotDto,
PickupWeekDay,
} from '#/api/store-pickup';
import type { PickupWeekDayOption } from '#/views/store/pickup/types';
@@ -49,64 +48,6 @@ export const DEFAULT_PICKUP_BASIC_SETTINGS: PickupBasicSettingsDto = {
maxItemsPerOrder: 20,
};
export const DEFAULT_BIG_SLOTS: PickupSlotDto[] = [
{
id: 'slot-morning',
name: '上午时段',
startTime: '09:00',
endTime: '11:30',
cutoffMinutes: 30,
capacity: 20,
reservedCount: 5,
dayOfWeeks: [...WEEKDAY_ONLY],
enabled: true,
},
{
id: 'slot-noon',
name: '午间时段',
startTime: '11:30',
endTime: '14:00',
cutoffMinutes: 20,
capacity: 30,
reservedCount: 12,
dayOfWeeks: [...ALL_WEEK_DAYS],
enabled: true,
},
{
id: 'slot-afternoon',
name: '下午时段',
startTime: '14:00',
endTime: '17:00',
cutoffMinutes: 30,
capacity: 15,
reservedCount: 3,
dayOfWeeks: [...WEEKDAY_ONLY],
enabled: true,
},
{
id: 'slot-evening',
name: '晚间时段',
startTime: '17:00',
endTime: '20:30',
cutoffMinutes: 30,
capacity: 25,
reservedCount: 8,
dayOfWeeks: [...ALL_WEEK_DAYS],
enabled: true,
},
{
id: 'slot-weekend',
name: '周末特惠',
startTime: '10:00',
endTime: '15:00',
cutoffMinutes: 45,
capacity: 40,
reservedCount: 18,
dayOfWeeks: [...WEEKEND_ONLY],
enabled: false,
},
];
export const DEFAULT_FINE_RULE: PickupFineRuleDto = {
intervalMinutes: 30,
slotCapacity: 5,

View File

@@ -26,7 +26,6 @@ import {
} from '#/api/store-pickup';
import {
DEFAULT_BIG_SLOTS,
DEFAULT_FINE_RULE,
DEFAULT_PICKUP_BASIC_SETTINGS,
DEFAULT_PICKUP_MODE,
@@ -37,7 +36,6 @@ import {
cloneFineRule,
clonePreviewDays,
createSettingsSnapshot,
generatePreviewDays,
sortSlots,
} from './helpers';
@@ -90,9 +88,9 @@ export function createDataActions(options: CreateDataActionsOptions) {
function applyDefaultSettings() {
options.mode.value = DEFAULT_PICKUP_MODE;
syncBasicSettings(cloneBasicSettings(DEFAULT_PICKUP_BASIC_SETTINGS));
options.bigSlots.value = sortSlots(cloneBigSlots(DEFAULT_BIG_SLOTS));
options.bigSlots.value = [];
syncFineRule(cloneFineRule(DEFAULT_FINE_RULE));
options.previewDays.value = generatePreviewDays(options.fineRule);
options.previewDays.value = [];
}
/** 应用快照到当前页面状态。 */
@@ -118,9 +116,7 @@ export function createDataActions(options: CreateDataActionsOptions) {
...result.basicSettings,
});
options.bigSlots.value = sortSlots(
result.bigSlots?.length
? result.bigSlots
: cloneBigSlots(DEFAULT_BIG_SLOTS),
result.bigSlots?.length ? result.bigSlots : [],
);
syncFineRule({
...DEFAULT_FINE_RULE,
@@ -129,7 +125,7 @@ export function createDataActions(options: CreateDataActionsOptions) {
options.previewDays.value =
result.previewDays?.length > 0
? clonePreviewDays(result.previewDays)
: generatePreviewDays(options.fineRule);
: [];
options.snapshot.value = buildCurrentSnapshot();
} catch (error) {
@@ -189,12 +185,13 @@ export function createDataActions(options: CreateDataActionsOptions) {
if (!options.selectedStoreId.value) return;
options.isSavingBasic.value = true;
try {
const currentStoreId = options.selectedStoreId.value;
await savePickupBasicSettingsApi({
storeId: options.selectedStoreId.value,
storeId: currentStoreId,
mode: options.mode.value,
basicSettings: cloneBasicSettings(options.basicSettings),
});
options.snapshot.value = buildCurrentSnapshot();
await loadStoreSettings(currentStoreId);
message.success('基本设置已保存');
} catch (error) {
console.error(error);
@@ -208,12 +205,13 @@ export function createDataActions(options: CreateDataActionsOptions) {
if (!options.selectedStoreId.value) return;
options.isSavingSlots.value = true;
try {
const currentStoreId = options.selectedStoreId.value;
await savePickupSlotsApi({
storeId: options.selectedStoreId.value,
storeId: currentStoreId,
mode: options.mode.value,
slots: cloneBigSlots(options.bigSlots.value),
});
options.snapshot.value = buildCurrentSnapshot();
await loadStoreSettings(currentStoreId);
message.success('大时段配置已保存');
} catch (error) {
console.error(error);
@@ -227,13 +225,13 @@ export function createDataActions(options: CreateDataActionsOptions) {
if (!options.selectedStoreId.value) return;
options.isSavingFineRule.value = true;
try {
const currentStoreId = options.selectedStoreId.value;
await savePickupFineRuleApi({
storeId: options.selectedStoreId.value,
storeId: currentStoreId,
mode: options.mode.value,
fineRule: cloneFineRule(options.fineRule),
});
options.previewDays.value = generatePreviewDays(options.fineRule);
options.snapshot.value = buildCurrentSnapshot();
await loadStoreSettings(currentStoreId);
message.success('精细规则已保存');
} catch (error) {
console.error(error);

View File

@@ -5,20 +5,15 @@ import type { Ref } from 'vue';
* 1. 管理精细规则字段与适用星期选择。
* 2. 处理规则校验与保存提交流程。
*/
import type {
PickupFineRuleDto,
PickupPreviewDayDto,
PickupWeekDay,
} from '#/api/store-pickup';
import type { PickupFineRuleDto, PickupWeekDay } from '#/api/store-pickup';
import { message } from 'ant-design-vue';
import { ALL_WEEK_DAYS, WEEKDAY_ONLY, WEEKEND_ONLY } from './constants';
import { generatePreviewDays, parseTimeToMinutes } from './helpers';
import { parseTimeToMinutes } from './helpers';
interface CreateFineRuleActionsOptions {
fineRule: PickupFineRuleDto;
previewDays: Ref<PickupPreviewDayDto[]>;
saveFineRule: () => Promise<void>;
selectedPreviewDate: Ref<string>;
}
@@ -29,22 +24,18 @@ export function createFineRuleActions(options: CreateFineRuleActionsOptions) {
5,
Math.floor(Number(value || 5)),
);
refreshPreviewDays();
}
function setFineSlotCapacity(value: number) {
options.fineRule.slotCapacity = Math.max(1, Math.floor(Number(value || 1)));
refreshPreviewDays();
}
function setFineDayStartTime(value: string) {
options.fineRule.dayStartTime = value;
refreshPreviewDays();
}
function setFineDayEndTime(value: string) {
options.fineRule.dayEndTime = value;
refreshPreviewDays();
}
function setFineMinAdvanceHours(value: number) {
@@ -52,7 +43,6 @@ export function createFineRuleActions(options: CreateFineRuleActionsOptions) {
0,
Math.floor(Number(value || 0)),
);
refreshPreviewDays();
}
function isFineDaySelected(day: PickupWeekDay) {
@@ -63,7 +53,6 @@ export function createFineRuleActions(options: CreateFineRuleActionsOptions) {
options.fineRule.dayOfWeeks = options.fineRule.dayOfWeeks.includes(day)
? options.fineRule.dayOfWeeks.filter((item) => item !== day)
: [...options.fineRule.dayOfWeeks, day].toSorted((a, b) => a - b);
refreshPreviewDays();
}
function quickSelectFineDays(mode: 'all' | 'weekday' | 'weekend') {
@@ -74,29 +63,12 @@ export function createFineRuleActions(options: CreateFineRuleActionsOptions) {
} else {
options.fineRule.dayOfWeeks = [...WEEKEND_ONLY];
}
refreshPreviewDays();
}
function setSelectedPreviewDate(date: string) {
options.selectedPreviewDate.value = date;
}
/** 重新计算预览,保障规则修改后页面即时反馈。 */
function refreshPreviewDays() {
options.previewDays.value = generatePreviewDays(options.fineRule);
if (options.previewDays.value.length === 0) {
options.selectedPreviewDate.value = '';
return;
}
const hasCurrent = options.previewDays.value.some(
(day) => day.date === options.selectedPreviewDate.value,
);
if (!hasCurrent) {
const firstDay = options.previewDays.value[0];
options.selectedPreviewDate.value = firstDay?.date ?? '';
}
}
/** 校验精细规则并提交保存。 */
async function handleSaveFineRule() {
// 1. 核心字段校验。
@@ -128,7 +100,6 @@ export function createFineRuleActions(options: CreateFineRuleActionsOptions) {
handleSaveFineRule,
isFineDaySelected,
quickSelectFineDays,
refreshPreviewDays,
setFineDayEndTime,
setFineDayStartTime,
setFineIntervalMinutes,

View File

@@ -1,14 +1,12 @@
/**
* 文件职责:自提设置页面纯函数工具。
* 1. 负责克隆、格式化、校验、排序等纯逻辑。
* 2. 负责根据精细规则生成预览数据。
* 2. 不直接请求接口,不生成 mock 预览数据。
*/
import type {
PickupBasicSettingsDto,
PickupFineRuleDto,
PickupPreviewDayDto,
PickupPreviewSlotDto,
PickupPreviewStatus,
PickupSlotDto,
PickupWeekDay,
} from '#/api/store-pickup';
@@ -150,142 +148,7 @@ export function validateSlotForm(payload: {
return '';
}
/** 生成 3 天预览数据。 */
export function generatePreviewDays(
fineRule: PickupFineRuleDto,
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 isEnabledDay = fineRule.dayOfWeeks.includes(dayOfWeek);
const slots = isEnabledDay
? generateDaySlots({
date,
dateKey,
fineRule,
})
: [];
return {
date: dateKey,
label: `${date.getMonth() + 1}/${date.getDate()}`,
subLabel: resolvePreviewSubLabel(index, dayOfWeek),
slots,
};
});
}
/** 是否为有效自提日索引。 */
function toPickupWeekDay(date: Date): PickupWeekDay {
const jsDay = date.getDay(); // 0=周日
const mapping: PickupWeekDay[] = [6, 0, 1, 2, 3, 4, 5];
return mapping[jsDay] ?? 0;
}
/** 生成某日预览时段。 */
function generateDaySlots(payload: {
date: Date;
dateKey: string;
fineRule: PickupFineRuleDto;
}): PickupPreviewSlotDto[] {
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);
const status = resolvePreviewStatus({
date: payload.date,
fineRule: payload.fineRule,
remainingCount,
time,
});
return {
time,
status,
remainingCount,
};
});
}
/** 计算预览时段状态。 */
function resolvePreviewStatus(payload: {
date: Date;
fineRule: PickupFineRuleDto;
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 isSameDaySet(a: PickupWeekDay[], b: PickupWeekDay[]) {
if (a.length !== b.length) return false;
return a.every((day, index) => day === b[index]);
}
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 resolvePreviewSubLabel(offset: number, dayOfWeek: PickupWeekDay) {
const dayText = WEEKDAY_OPTIONS.find(
(item) => item.value === dayOfWeek,
)?.label;
if (offset === 0) return `${dayText} 今天`;
if (offset === 1) return `${dayText} 明天`;
if (offset === 2) return `${dayText} 后天`;
return dayText ?? '';
}

View File

@@ -5,7 +5,11 @@ import type { StoreListItemDto } from '#/api/store';
* 2. 组合数据加载、复制、大时段与精细规则动作。
* 3. 对外暴露视图层可直接消费的状态与方法。
*/
import type { PickupBasicSettingsDto, PickupSlotDto } from '#/api/store-pickup';
import type {
PickupBasicSettingsDto,
PickupPreviewDayDto,
PickupSlotDto,
} from '#/api/store-pickup';
import type {
PickupDrawerMode,
PickupSettingsSnapshot,
@@ -16,7 +20,6 @@ import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue';
import {
ALL_WEEK_DAYS,
DEFAULT_BIG_SLOTS,
DEFAULT_FINE_RULE,
DEFAULT_PICKUP_BASIC_SETTINGS,
DEFAULT_PICKUP_MODE,
@@ -30,13 +33,10 @@ import { createFineRuleActions } from './pickup-page/fine-rule-actions';
import {
calcReservedPercent,
cloneBasicSettings,
cloneBigSlots,
cloneFineRule,
clonePreviewDays,
createSlotId,
formatDayOfWeeksText,
generatePreviewDays,
sortSlots,
} from './pickup-page/helpers';
import { createSlotActions } from './pickup-page/slot-actions';
@@ -56,11 +56,9 @@ export function useStorePickupPage() {
const basicSettings = reactive<PickupBasicSettingsDto>(
cloneBasicSettings(DEFAULT_PICKUP_BASIC_SETTINGS),
);
const bigSlots = ref<PickupSlotDto[]>(
sortSlots(cloneBigSlots(DEFAULT_BIG_SLOTS)),
);
const bigSlots = ref<PickupSlotDto[]>([]);
const fineRule = reactive(cloneFineRule(DEFAULT_FINE_RULE));
const previewDays = ref(generatePreviewDays(fineRule));
const previewDays = ref<PickupPreviewDayDto[]>([]);
const selectedPreviewDate = ref(previewDays.value[0]?.date ?? '');
const snapshot = ref<null | PickupSettingsSnapshot>(null);
@@ -187,7 +185,6 @@ export function useStorePickupPage() {
handleSaveFineRule,
isFineDaySelected,
quickSelectFineDays,
refreshPreviewDays,
setFineDayEndTime,
setFineDayStartTime,
setFineIntervalMinutes,
@@ -197,7 +194,6 @@ export function useStorePickupPage() {
toggleFineDay,
} = createFineRuleActions({
fineRule,
previewDays,
saveFineRule,
selectedPreviewDate,
});
@@ -252,7 +248,7 @@ export function useStorePickupPage() {
previewDays.value =
snapshot.value?.previewDays && snapshot.value.previewDays.length > 0
? clonePreviewDays(snapshot.value.previewDays)
: generatePreviewDays(fineRule);
: [];
selectedPreviewDate.value = previewDays.value[0]?.date ?? '';
}
@@ -265,9 +261,9 @@ export function useStorePickupPage() {
basicSettings.bookingDays = DEFAULT_PICKUP_BASIC_SETTINGS.bookingDays;
basicSettings.maxItemsPerOrder =
DEFAULT_PICKUP_BASIC_SETTINGS.maxItemsPerOrder;
bigSlots.value = sortSlots(cloneBigSlots(DEFAULT_BIG_SLOTS));
bigSlots.value = [];
Object.assign(fineRule, cloneFineRule(DEFAULT_FINE_RULE));
previewDays.value = generatePreviewDays(fineRule);
previewDays.value = [];
selectedPreviewDate.value = previewDays.value[0]?.date ?? '';
snapshot.value = null;
return;
@@ -276,6 +272,20 @@ export function useStorePickupPage() {
selectedPreviewDate.value = previewDays.value[0]?.date ?? '';
});
watch(previewDays, (days) => {
if (days.length === 0) {
selectedPreviewDate.value = '';
return;
}
const hasCurrent = days.some(
(item) => item.date === selectedPreviewDate.value,
);
if (!hasCurrent) {
selectedPreviewDate.value = days[0]?.date ?? '';
}
});
// 9. 页面首屏初始化。
onMounted(loadStores);
// 10. 路由回到当前页时刷新门店列表,避免使用旧缓存。
@@ -316,7 +326,6 @@ export function useStorePickupPage() {
previewDays,
quickSelectFineDays,
quickSelectSlotDays,
refreshPreviewDays,
resetBasicSettings,
resetFineRule,
resetFromSnapshot,