refactor(project): remove store mock fallback flows and unify real-data states
This commit is contained in:
@@ -45,8 +45,9 @@ export interface DeliveryGeneralSettingsDto {
|
|||||||
|
|
||||||
/** 门店配送设置聚合 */
|
/** 门店配送设置聚合 */
|
||||||
export interface StoreDeliverySettingsDto {
|
export interface StoreDeliverySettingsDto {
|
||||||
generalSettings: DeliveryGeneralSettingsDto;
|
generalSettings: DeliveryGeneralSettingsDto | null;
|
||||||
mode: DeliveryMode;
|
isConfigured: boolean;
|
||||||
|
mode: DeliveryMode | null;
|
||||||
/** 半径配送中心点纬度 */
|
/** 半径配送中心点纬度 */
|
||||||
radiusCenterLatitude: null | number;
|
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 {
|
export interface CopyStoreDeliverySettingsParams {
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ export interface DineInTableDto {
|
|||||||
/** 门店堂食设置聚合 */
|
/** 门店堂食设置聚合 */
|
||||||
export interface StoreDineInSettingsDto {
|
export interface StoreDineInSettingsDto {
|
||||||
areas: DineInAreaDto[];
|
areas: DineInAreaDto[];
|
||||||
basicSettings: DineInBasicSettingsDto;
|
basicSettings: DineInBasicSettingsDto | null;
|
||||||
|
isConfigured: boolean;
|
||||||
storeId: string;
|
storeId: string;
|
||||||
tables: DineInTableDto[];
|
tables: DineInTableDto[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,12 +60,33 @@ export interface StoreFeesSettingsDto {
|
|||||||
packagingFeeMode: PackagingFeeMode;
|
packagingFeeMode: PackagingFeeMode;
|
||||||
/** 包装费阶梯 */
|
/** 包装费阶梯 */
|
||||||
packagingFeeTiers: PackagingFeeTierDto[];
|
packagingFeeTiers: PackagingFeeTierDto[];
|
||||||
|
/** 是否已配置 */
|
||||||
|
isConfigured: boolean;
|
||||||
/** 门店 ID */
|
/** 门店 ID */
|
||||||
storeId: string;
|
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 {
|
export interface CopyStoreFeesSettingsParams {
|
||||||
|
|||||||
@@ -71,10 +71,11 @@ export interface PickupPreviewDayDto {
|
|||||||
|
|
||||||
/** 门店自提设置聚合 */
|
/** 门店自提设置聚合 */
|
||||||
export interface StorePickupSettingsDto {
|
export interface StorePickupSettingsDto {
|
||||||
basicSettings: PickupBasicSettingsDto;
|
basicSettings: null | PickupBasicSettingsDto;
|
||||||
bigSlots: PickupSlotDto[];
|
bigSlots: PickupSlotDto[];
|
||||||
fineRule: PickupFineRuleDto;
|
fineRule: null | PickupFineRuleDto;
|
||||||
mode: PickupMode;
|
isConfigured: boolean;
|
||||||
|
mode: null | PickupMode;
|
||||||
previewDays: PickupPreviewDayDto[];
|
previewDays: PickupPreviewDayDto[];
|
||||||
storeId: string;
|
storeId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const emit = defineEmits<{
|
|||||||
(event: 'update:open', value: boolean): void;
|
(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_MODAL_Z_INDEX = 10_000;
|
||||||
const MAP_DEBUG_PREFIX = '[TenantUI-DeliveryMap]';
|
const MAP_DEBUG_PREFIX = '[TenantUI-DeliveryMap]';
|
||||||
|
|
||||||
|
|||||||
@@ -28,106 +28,13 @@ export const TIER_COLOR_PALETTE = [
|
|||||||
|
|
||||||
export const DEFAULT_DELIVERY_MODE: DeliveryMode = 'radius';
|
export const DEFAULT_DELIVERY_MODE: DeliveryMode = 'radius';
|
||||||
|
|
||||||
export const DEFAULT_RADIUS_TIERS: RadiusTierDto[] = [
|
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',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function createPolygonGeoJson(coordinates: Array<[number, number]>) {
|
export const DEFAULT_POLYGON_ZONES: PolygonZoneDto[] = [];
|
||||||
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_GENERAL_SETTINGS: DeliveryGeneralSettingsDto = {
|
export const DEFAULT_GENERAL_SETTINGS: DeliveryGeneralSettingsDto = {
|
||||||
freeDeliveryThreshold: 30,
|
freeDeliveryThreshold: null,
|
||||||
maxDeliveryDistance: 5,
|
maxDeliveryDistance: 0,
|
||||||
hourlyCapacityLimit: 50,
|
hourlyCapacityLimit: 1,
|
||||||
etaAdjustmentMinutes: 10,
|
etaAdjustmentMinutes: 0,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,11 +38,14 @@ import {
|
|||||||
} from './helpers';
|
} from './helpers';
|
||||||
|
|
||||||
interface CreateDataActionsOptions {
|
interface CreateDataActionsOptions {
|
||||||
|
clearSettings: () => void;
|
||||||
editingMode: Ref<DeliveryMode>;
|
editingMode: Ref<DeliveryMode>;
|
||||||
generalSettings: DeliveryGeneralSettingsDto;
|
generalSettings: DeliveryGeneralSettingsDto;
|
||||||
|
isConfigured: Ref<boolean>;
|
||||||
isSaving: Ref<boolean>;
|
isSaving: Ref<boolean>;
|
||||||
isSettingsLoading: Ref<boolean>;
|
isSettingsLoading: Ref<boolean>;
|
||||||
isStoreLoading: Ref<boolean>;
|
isStoreLoading: Ref<boolean>;
|
||||||
|
loadedStoreId: Ref<string>;
|
||||||
mode: Ref<DeliveryMode>;
|
mode: Ref<DeliveryMode>;
|
||||||
radiusCenterLatitude: Ref<null | number>;
|
radiusCenterLatitude: Ref<null | number>;
|
||||||
radiusCenterLongitude: Ref<null | number>;
|
radiusCenterLongitude: Ref<null | number>;
|
||||||
@@ -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) {
|
async function loadStoreSettings(storeId: string) {
|
||||||
options.isSettingsLoading.value = true;
|
options.isSettingsLoading.value = true;
|
||||||
@@ -104,29 +96,41 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
|||||||
const result = await getStoreDeliverySettingsApi(storeId);
|
const result = await getStoreDeliverySettingsApi(storeId);
|
||||||
if (options.selectedStoreId.value !== currentStoreId) return;
|
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.mode.value = result.mode ?? DEFAULT_DELIVERY_MODE;
|
||||||
options.editingMode.value = options.mode.value;
|
options.editingMode.value = options.mode.value;
|
||||||
options.radiusCenterLatitude.value = result.radiusCenterLatitude ?? null;
|
options.radiusCenterLatitude.value = result.radiusCenterLatitude ?? null;
|
||||||
options.radiusCenterLongitude.value =
|
options.radiusCenterLongitude.value =
|
||||||
result.radiusCenterLongitude ?? null;
|
result.radiusCenterLongitude ?? null;
|
||||||
options.radiusTiers.value = sortRadiusTiers(
|
options.radiusTiers.value = sortRadiusTiers(
|
||||||
result.radiusTiers?.length ? result.radiusTiers : DEFAULT_RADIUS_TIERS,
|
result.radiusTiers ?? DEFAULT_RADIUS_TIERS,
|
||||||
);
|
);
|
||||||
options.polygonZones.value = sortPolygonZones(
|
options.polygonZones.value = sortPolygonZones(
|
||||||
result.polygonZones?.length
|
result.polygonZones ?? clonePolygonZones(DEFAULT_POLYGON_ZONES),
|
||||||
? result.polygonZones
|
|
||||||
: clonePolygonZones(DEFAULT_POLYGON_ZONES),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
syncGeneralSettings({
|
syncGeneralSettings(
|
||||||
...DEFAULT_GENERAL_SETTINGS,
|
result.generalSettings
|
||||||
...result.generalSettings,
|
? cloneGeneralSettings(result.generalSettings)
|
||||||
});
|
: cloneGeneralSettings(DEFAULT_GENERAL_SETTINGS),
|
||||||
|
);
|
||||||
|
options.isConfigured.value = true;
|
||||||
|
options.loadedStoreId.value = currentStoreId;
|
||||||
options.snapshot.value = buildCurrentSnapshot();
|
options.snapshot.value = buildCurrentSnapshot();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
applyDefaultSettings();
|
options.clearSettings();
|
||||||
options.snapshot.value = buildCurrentSnapshot();
|
options.isConfigured.value = false;
|
||||||
|
options.loadedStoreId.value = '';
|
||||||
|
options.snapshot.value = null;
|
||||||
|
message.error('加载配送设置失败,请稍后重试');
|
||||||
} finally {
|
} finally {
|
||||||
options.isSettingsLoading.value = false;
|
options.isSettingsLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -148,8 +152,10 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
|||||||
|
|
||||||
if (options.stores.value.length === 0) {
|
if (options.stores.value.length === 0) {
|
||||||
options.selectedStoreId.value = '';
|
options.selectedStoreId.value = '';
|
||||||
|
options.loadedStoreId.value = '';
|
||||||
|
options.isConfigured.value = false;
|
||||||
options.snapshot.value = null;
|
options.snapshot.value = null;
|
||||||
applyDefaultSettings();
|
options.clearSettings();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,8 +178,11 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
options.stores.value = [];
|
options.stores.value = [];
|
||||||
options.selectedStoreId.value = '';
|
options.selectedStoreId.value = '';
|
||||||
|
options.loadedStoreId.value = '';
|
||||||
|
options.isConfigured.value = false;
|
||||||
options.snapshot.value = null;
|
options.snapshot.value = null;
|
||||||
applyDefaultSettings();
|
options.clearSettings();
|
||||||
|
message.error('加载门店失败,请稍后重试');
|
||||||
} finally {
|
} finally {
|
||||||
options.isStoreLoading.value = false;
|
options.isStoreLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -209,7 +218,7 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
|||||||
/** 重置到最近一次加载/保存后的快照。 */
|
/** 重置到最近一次加载/保存后的快照。 */
|
||||||
function resetFromSnapshot() {
|
function resetFromSnapshot() {
|
||||||
if (!options.snapshot.value) {
|
if (!options.snapshot.value) {
|
||||||
applyDefaultSettings();
|
options.clearSettings();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
applySnapshot(options.snapshot.value);
|
applySnapshot(options.snapshot.value);
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export function createTierActions(options: CreateTierActionsOptions) {
|
|||||||
// 1. 校验区间与字段合法性。
|
// 1. 校验区间与字段合法性。
|
||||||
if (options.tierForm.maxDistance <= options.tierForm.minDistance) {
|
if (options.tierForm.maxDistance <= options.tierForm.minDistance) {
|
||||||
message.error('结束距离必须大于起始距离');
|
message.error('结束距离必须大于起始距离');
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -103,7 +103,7 @@ export function createTierActions(options: CreateTierActionsOptions) {
|
|||||||
options.tierForm.minOrderAmount < 0
|
options.tierForm.minOrderAmount < 0
|
||||||
) {
|
) {
|
||||||
message.error('金额字段不能小于 0');
|
message.error('金额字段不能小于 0');
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 校验与现有梯度区间冲突。
|
// 2. 校验与现有梯度区间冲突。
|
||||||
@@ -116,7 +116,7 @@ export function createTierActions(options: CreateTierActionsOptions) {
|
|||||||
});
|
});
|
||||||
if (hasOverlap) {
|
if (hasOverlap) {
|
||||||
message.error('距离区间与已有梯度重叠,请调整后重试');
|
message.error('距离区间与已有梯度重叠,请调整后重试');
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 组装记录并写回列表。
|
// 3. 组装记录并写回列表。
|
||||||
@@ -143,18 +143,20 @@ export function createTierActions(options: CreateTierActionsOptions) {
|
|||||||
message.success(
|
message.success(
|
||||||
options.tierDrawerMode.value === 'edit' ? '梯度已更新' : '梯度已添加',
|
options.tierDrawerMode.value === 'edit' ? '梯度已更新' : '梯度已添加',
|
||||||
);
|
);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 删除指定梯度。 */
|
/** 删除指定梯度。 */
|
||||||
function handleDeleteTier(tierId: string) {
|
function handleDeleteTier(tierId: string) {
|
||||||
if (options.radiusTiers.value.length <= 1) {
|
if (options.radiusTiers.value.length <= 1) {
|
||||||
message.warning('至少保留一个梯度');
|
message.warning('至少保留一个梯度');
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
options.radiusTiers.value = options.radiusTiers.value.filter(
|
options.radiusTiers.value = options.radiusTiers.value.filter(
|
||||||
(item) => item.id !== tierId,
|
(item) => item.id !== tierId,
|
||||||
);
|
);
|
||||||
message.success('梯度已删除');
|
message.success('梯度已删除');
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -101,12 +101,12 @@ export function createZoneActions(options: CreateZoneActionsOptions) {
|
|||||||
const normalizedName = options.zoneForm.name.trim();
|
const normalizedName = options.zoneForm.name.trim();
|
||||||
if (!normalizedName) {
|
if (!normalizedName) {
|
||||||
message.error('请输入区域名称');
|
message.error('请输入区域名称');
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (countPolygonsInGeoJson(options.zoneForm.polygonGeoJson) <= 0) {
|
if (countPolygonsInGeoJson(options.zoneForm.polygonGeoJson) <= 0) {
|
||||||
message.error('请先绘制配送区域');
|
message.error('请先绘制配送区域');
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 优先级冲突校验。
|
// 2. 优先级冲突校验。
|
||||||
@@ -116,7 +116,7 @@ export function createZoneActions(options: CreateZoneActionsOptions) {
|
|||||||
});
|
});
|
||||||
if (hasPriorityConflict) {
|
if (hasPriorityConflict) {
|
||||||
message.error('优先级已存在,请调整后重试');
|
message.error('优先级已存在,请调整后重试');
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 写回列表。
|
// 3. 写回列表。
|
||||||
@@ -144,6 +144,7 @@ export function createZoneActions(options: CreateZoneActionsOptions) {
|
|||||||
message.success(
|
message.success(
|
||||||
options.zoneDrawerMode.value === 'edit' ? '区域已更新' : '区域已添加',
|
options.zoneDrawerMode.value === 'edit' ? '区域已更新' : '区域已添加',
|
||||||
);
|
);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 删除指定区域。 */
|
/** 删除指定区域。 */
|
||||||
@@ -152,6 +153,7 @@ export function createZoneActions(options: CreateZoneActionsOptions) {
|
|||||||
(item) => item.id !== zoneId,
|
(item) => item.id !== zoneId,
|
||||||
);
|
);
|
||||||
message.success('区域已删除');
|
message.success('区域已删除');
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export function useStoreDeliveryPage() {
|
|||||||
const isSettingsLoading = ref(false);
|
const isSettingsLoading = ref(false);
|
||||||
const isSaving = ref(false);
|
const isSaving = ref(false);
|
||||||
const isCopySubmitting = ref(false);
|
const isCopySubmitting = ref(false);
|
||||||
|
const isConfigured = ref(false);
|
||||||
|
const loadedStoreId = ref('');
|
||||||
|
|
||||||
// 2. 页面主业务数据。
|
// 2. 页面主业务数据。
|
||||||
const stores = ref<StoreListItemDto[]>([]);
|
const stores = ref<StoreListItemDto[]>([]);
|
||||||
@@ -180,6 +182,19 @@ export function useStoreDeliveryPage() {
|
|||||||
|
|
||||||
const isRadiusMode = computed(() => editingMode.value === 'radius');
|
const isRadiusMode = computed(() => editingMode.value === 'radius');
|
||||||
const isPageLoading = computed(() => isSettingsLoading.value);
|
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(() =>
|
const tierDrawerTitle = computed(() =>
|
||||||
tierDrawerMode.value === 'edit' ? '编辑梯度' : '添加梯度',
|
tierDrawerMode.value === 'edit' ? '编辑梯度' : '添加梯度',
|
||||||
@@ -188,6 +203,19 @@ export function useStoreDeliveryPage() {
|
|||||||
zoneDrawerMode.value === 'edit' ? '编辑区域' : '新增区域',
|
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. 数据域动作装配。
|
// 5. 数据域动作装配。
|
||||||
const {
|
const {
|
||||||
loadStoreSettings,
|
loadStoreSettings,
|
||||||
@@ -195,11 +223,14 @@ export function useStoreDeliveryPage() {
|
|||||||
resetFromSnapshot,
|
resetFromSnapshot,
|
||||||
saveCurrentSettings,
|
saveCurrentSettings,
|
||||||
} = createDataActions({
|
} = createDataActions({
|
||||||
|
clearSettings,
|
||||||
editingMode,
|
editingMode,
|
||||||
generalSettings,
|
generalSettings,
|
||||||
|
isConfigured,
|
||||||
isSaving,
|
isSaving,
|
||||||
isSettingsLoading,
|
isSettingsLoading,
|
||||||
isStoreLoading,
|
isStoreLoading,
|
||||||
|
loadedStoreId,
|
||||||
mode: deliveryMode,
|
mode: deliveryMode,
|
||||||
radiusCenterLatitude,
|
radiusCenterLatitude,
|
||||||
radiusCenterLongitude,
|
radiusCenterLongitude,
|
||||||
@@ -224,8 +255,8 @@ export function useStoreDeliveryPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleDeleteTier,
|
handleDeleteTier: handleDeleteTierLocal,
|
||||||
handleTierSubmit,
|
handleTierSubmit: handleTierSubmitLocal,
|
||||||
openTierDrawer,
|
openTierDrawer,
|
||||||
setTierColor,
|
setTierColor,
|
||||||
setTierDeliveryFee,
|
setTierDeliveryFee,
|
||||||
@@ -245,8 +276,8 @@ export function useStoreDeliveryPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleDeleteZone,
|
handleDeleteZone: handleDeleteZoneLocal,
|
||||||
handleZoneSubmit,
|
handleZoneSubmit: handleZoneSubmitLocal,
|
||||||
openZoneDrawer,
|
openZoneDrawer,
|
||||||
setZoneColor,
|
setZoneColor,
|
||||||
setZoneDeliveryFee,
|
setZoneDeliveryFee,
|
||||||
@@ -294,6 +325,7 @@ export function useStoreDeliveryPage() {
|
|||||||
|
|
||||||
// 切换“当前生效模式”,二次确认后保存,防止误操作。
|
// 切换“当前生效模式”,二次确认后保存,防止误操作。
|
||||||
function setDeliveryMode(value: DeliveryMode) {
|
function setDeliveryMode(value: DeliveryMode) {
|
||||||
|
if (!canOperate.value) return;
|
||||||
if (value === deliveryMode.value) return;
|
if (value === deliveryMode.value) return;
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '确认切换当前启用的配送模式?',
|
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. 门店切换时自动刷新配置。
|
// 7. 门店切换时自动刷新配置。
|
||||||
watch(selectedStoreId, async (storeId) => {
|
watch(selectedStoreId, async (storeId) => {
|
||||||
if (!storeId) {
|
if (!storeId) {
|
||||||
deliveryMode.value = DEFAULT_DELIVERY_MODE;
|
loadedStoreId.value = '';
|
||||||
editingMode.value = DEFAULT_DELIVERY_MODE;
|
isConfigured.value = false;
|
||||||
radiusCenterLatitude.value = null;
|
clearSettings();
|
||||||
radiusCenterLongitude.value = null;
|
|
||||||
radiusTiers.value = cloneRadiusTiers(DEFAULT_RADIUS_TIERS);
|
|
||||||
polygonZones.value = clonePolygonZones(DEFAULT_POLYGON_ZONES);
|
|
||||||
Object.assign(
|
|
||||||
generalSettings,
|
|
||||||
cloneGeneralSettings(DEFAULT_GENERAL_SETTINGS),
|
|
||||||
);
|
|
||||||
snapshot.value = null;
|
snapshot.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
loadedStoreId.value = '';
|
||||||
|
isConfigured.value = false;
|
||||||
|
snapshot.value = null;
|
||||||
|
clearSettings();
|
||||||
await loadStoreSettings(storeId);
|
await loadStoreSettings(storeId);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -392,6 +449,7 @@ export function useStoreDeliveryPage() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
DELIVERY_MODE_OPTIONS,
|
DELIVERY_MODE_OPTIONS,
|
||||||
|
canOperate,
|
||||||
copyCandidates,
|
copyCandidates,
|
||||||
copyTargetStoreIds,
|
copyTargetStoreIds,
|
||||||
deliveryMode,
|
deliveryMode,
|
||||||
@@ -409,6 +467,9 @@ export function useStoreDeliveryPage() {
|
|||||||
isCopyIndeterminate,
|
isCopyIndeterminate,
|
||||||
isCopyModalOpen,
|
isCopyModalOpen,
|
||||||
isCopySubmitting,
|
isCopySubmitting,
|
||||||
|
isConfigured,
|
||||||
|
hasLoadedStoreSettings,
|
||||||
|
hasSelectedStore,
|
||||||
isPageLoading,
|
isPageLoading,
|
||||||
isRadiusMode,
|
isRadiusMode,
|
||||||
isSaving,
|
isSaving,
|
||||||
@@ -422,6 +483,7 @@ export function useStoreDeliveryPage() {
|
|||||||
openCopyModal,
|
openCopyModal,
|
||||||
openTierDrawer,
|
openTierDrawer,
|
||||||
openZoneDrawer,
|
openZoneDrawer,
|
||||||
|
loadedStoreId,
|
||||||
polygonZones,
|
polygonZones,
|
||||||
radiusCenterLatitude,
|
radiusCenterLatitude,
|
||||||
radiusCenterLongitude,
|
radiusCenterLongitude,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { useStoreDeliveryPage } from './composables/useStoreDeliveryPage';
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
DELIVERY_MODE_OPTIONS,
|
DELIVERY_MODE_OPTIONS,
|
||||||
|
canOperate,
|
||||||
copyCandidates,
|
copyCandidates,
|
||||||
copyTargetStoreIds,
|
copyTargetStoreIds,
|
||||||
deliveryMode,
|
deliveryMode,
|
||||||
@@ -37,6 +38,8 @@ const {
|
|||||||
isCopyIndeterminate,
|
isCopyIndeterminate,
|
||||||
isCopyModalOpen,
|
isCopyModalOpen,
|
||||||
isCopySubmitting,
|
isCopySubmitting,
|
||||||
|
isConfigured,
|
||||||
|
hasLoadedStoreSettings,
|
||||||
isPageLoading,
|
isPageLoading,
|
||||||
isRadiusMode,
|
isRadiusMode,
|
||||||
isSaving,
|
isSaving,
|
||||||
@@ -98,7 +101,9 @@ const {
|
|||||||
:selected-store-id="selectedStoreId"
|
:selected-store-id="selectedStoreId"
|
||||||
:store-options="storeOptions"
|
:store-options="storeOptions"
|
||||||
:is-store-loading="isStoreLoading"
|
:is-store-loading="isStoreLoading"
|
||||||
:copy-disabled="!selectedStoreId || copyCandidates.length === 0"
|
:copy-disabled="
|
||||||
|
!canOperate || !isConfigured || copyCandidates.length === 0
|
||||||
|
"
|
||||||
@update:selected-store-id="setSelectedStoreId"
|
@update:selected-store-id="setSelectedStoreId"
|
||||||
@copy="openCopyModal"
|
@copy="openCopyModal"
|
||||||
/>
|
/>
|
||||||
@@ -111,6 +116,12 @@ const {
|
|||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<Spin :spinning="isPageLoading">
|
<Spin :spinning="isPageLoading">
|
||||||
|
<Card v-if="hasLoadedStoreSettings && !isConfigured" :bordered="false">
|
||||||
|
<Empty
|
||||||
|
description="当前门店尚未配置配送规则。请先填写规则并保存,之后可执行复制。"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<div class="delivery-cards-stack">
|
<div class="delivery-cards-stack">
|
||||||
<DeliveryModeCard
|
<DeliveryModeCard
|
||||||
:active-mode="deliveryMode"
|
:active-mode="deliveryMode"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { DineInAreaDto } from '#/api/store-dinein';
|
|||||||
import { Button, Card, Empty, Popconfirm } from 'ant-design-vue';
|
import { Button, Card, Empty, Popconfirm } from 'ant-design-vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
canOperate: boolean;
|
||||||
getAreaTableCount: (areaId: string) => number;
|
getAreaTableCount: (areaId: string) => number;
|
||||||
isSaving: boolean;
|
isSaving: boolean;
|
||||||
selectedArea?: DineInAreaDto;
|
selectedArea?: DineInAreaDto;
|
||||||
@@ -32,7 +33,14 @@ const emit = defineEmits<{
|
|||||||
<span class="section-title">区域管理</span>
|
<span class="section-title">区域管理</span>
|
||||||
</template>
|
</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<Button type="primary" size="small" @click="emit('add')">添加区域</Button>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
|
@click="emit('add')"
|
||||||
|
>
|
||||||
|
添加区域
|
||||||
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="props.areas.length > 0">
|
<template v-if="props.areas.length > 0">
|
||||||
@@ -43,6 +51,7 @@ const emit = defineEmits<{
|
|||||||
type="button"
|
type="button"
|
||||||
class="dinein-area-pill"
|
class="dinein-area-pill"
|
||||||
:class="{ active: props.selectedAreaId === area.id }"
|
:class="{ active: props.selectedAreaId === area.id }"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
@click="emit('selectArea', area.id)"
|
@click="emit('selectArea', area.id)"
|
||||||
>
|
>
|
||||||
{{ area.name }} ({{ props.getAreaTableCount(area.id) }}桌)
|
{{ area.name }} ({{ props.getAreaTableCount(area.id) }}桌)
|
||||||
@@ -58,6 +67,7 @@ const emit = defineEmits<{
|
|||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
size="small"
|
size="small"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
@click="emit('edit', props.selectedArea)"
|
@click="emit('edit', props.selectedArea)"
|
||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
@@ -66,9 +76,16 @@ const emit = defineEmits<{
|
|||||||
title="确认删除该区域吗?"
|
title="确认删除该区域吗?"
|
||||||
ok-text="确认"
|
ok-text="确认"
|
||||||
cancel-text="取消"
|
cancel-text="取消"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
@confirm="emit('delete', props.selectedArea)"
|
@confirm="emit('delete', props.selectedArea)"
|
||||||
>
|
>
|
||||||
<Button type="link" danger size="small" :loading="props.isSaving">
|
<Button
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
|
:loading="props.isSaving"
|
||||||
|
>
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { DineInBasicSettingsDto } from '#/api/store-dinein';
|
|||||||
import { Button, Card, InputNumber, Switch } from 'ant-design-vue';
|
import { Button, Card, InputNumber, Switch } from 'ant-design-vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
canOperate: boolean;
|
||||||
isSaving: boolean;
|
isSaving: boolean;
|
||||||
onSetDefaultDiningMinutes: (value: number) => void;
|
onSetDefaultDiningMinutes: (value: number) => void;
|
||||||
onSetEnabled: (value: boolean) => void;
|
onSetEnabled: (value: boolean) => void;
|
||||||
@@ -40,6 +41,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
<div class="dinein-form-control">
|
<div class="dinein-form-control">
|
||||||
<Switch
|
<Switch
|
||||||
:checked="props.settings.enabled"
|
:checked="props.settings.enabled"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
@update:checked="(value) => props.onSetEnabled(Boolean(value))"
|
@update:checked="(value) => props.onSetEnabled(Boolean(value))"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,6 +55,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
:min="1"
|
:min="1"
|
||||||
:precision="0"
|
:precision="0"
|
||||||
:controls="false"
|
:controls="false"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
class="dinein-number-input"
|
class="dinein-number-input"
|
||||||
@update:value="
|
@update:value="
|
||||||
(value) => props.onSetDefaultDiningMinutes(toNumber(value, 90))
|
(value) => props.onSetDefaultDiningMinutes(toNumber(value, 90))
|
||||||
@@ -70,6 +73,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
:min="0"
|
:min="0"
|
||||||
:precision="0"
|
:precision="0"
|
||||||
:controls="false"
|
:controls="false"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
class="dinein-number-input"
|
class="dinein-number-input"
|
||||||
@update:value="
|
@update:value="
|
||||||
(value) => props.onSetOvertimeReminderMinutes(toNumber(value, 10))
|
(value) => props.onSetOvertimeReminderMinutes(toNumber(value, 10))
|
||||||
@@ -81,8 +85,18 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dinein-form-actions">
|
<div class="dinein-form-actions">
|
||||||
<Button :disabled="props.isSaving" @click="emit('reset')">重置</Button>
|
<Button
|
||||||
<Button type="primary" :loading="props.isSaving" @click="emit('save')">
|
:disabled="props.isSaving || !props.canOperate"
|
||||||
|
@click="emit('reset')"
|
||||||
|
>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
:loading="props.isSaving"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
|
@click="emit('save')"
|
||||||
|
>
|
||||||
保存设置
|
保存设置
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import type { DineInStatusOption } from '#/views/store/dine-in/types';
|
|||||||
import { Button, Card, Empty, Popconfirm, Tag } from 'ant-design-vue';
|
import { Button, Card, Empty, Popconfirm, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
canOperate: boolean;
|
||||||
|
hasSelectedArea: boolean;
|
||||||
isSaving: boolean;
|
isSaving: boolean;
|
||||||
resolveStatusClassName: (status: DineInTableDto['status']) => string;
|
resolveStatusClassName: (status: DineInTableDto['status']) => string;
|
||||||
statusMap: Record<DineInTableDto['status'], DineInStatusOption>;
|
statusMap: Record<DineInTableDto['status'], DineInStatusOption>;
|
||||||
@@ -34,8 +36,19 @@ const emit = defineEmits<{
|
|||||||
</template>
|
</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<div class="dinein-table-header-actions">
|
<div class="dinein-table-header-actions">
|
||||||
<Button size="small" @click="emit('batch')">批量生成</Button>
|
<Button
|
||||||
<Button type="primary" size="small" @click="emit('add')">
|
size="small"
|
||||||
|
:disabled="!props.canOperate || !props.hasSelectedArea"
|
||||||
|
@click="emit('batch')"
|
||||||
|
>
|
||||||
|
批量生成
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:disabled="!props.canOperate || !props.hasSelectedArea"
|
||||||
|
@click="emit('add')"
|
||||||
|
>
|
||||||
添加桌位
|
添加桌位
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,19 +81,36 @@ const emit = defineEmits<{
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dinein-table-footer">
|
<div class="dinein-table-footer">
|
||||||
<Button size="small" type="text" @click="emit('qrcode', table)">
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
|
@click="emit('qrcode', table)"
|
||||||
|
>
|
||||||
二维码
|
二维码
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="small" type="text" @click="emit('edit', table)">
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
|
@click="emit('edit', table)"
|
||||||
|
>
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="确认删除该桌位吗?"
|
title="确认删除该桌位吗?"
|
||||||
ok-text="确认"
|
ok-text="确认"
|
||||||
cancel-text="取消"
|
cancel-text="取消"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
@confirm="emit('delete', table.id)"
|
@confirm="emit('delete', table.id)"
|
||||||
>
|
>
|
||||||
<Button size="small" type="text" danger :loading="props.isSaving">
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
:disabled="!props.canOperate"
|
||||||
|
:loading="props.isSaving"
|
||||||
|
>
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
@@ -89,6 +119,11 @@ const emit = defineEmits<{
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<Empty v-else description="当前区域暂无桌位" />
|
<Empty
|
||||||
|
v-else
|
||||||
|
:description="
|
||||||
|
props.hasSelectedArea ? '当前区域暂无桌位' : '请先添加并选择区域'
|
||||||
|
"
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -15,12 +15,7 @@ import { message } from 'ant-design-vue';
|
|||||||
|
|
||||||
import { deleteDineInAreaApi, saveDineInAreaApi } from '#/api/store-dinein';
|
import { deleteDineInAreaApi, saveDineInAreaApi } from '#/api/store-dinein';
|
||||||
|
|
||||||
import {
|
import { countAreaTables, sortAreas, validateAreaForm } from './helpers';
|
||||||
countAreaTables,
|
|
||||||
createDineInId,
|
|
||||||
sortAreas,
|
|
||||||
validateAreaForm,
|
|
||||||
} from './helpers';
|
|
||||||
|
|
||||||
interface CreateAreaActionsOptions {
|
interface CreateAreaActionsOptions {
|
||||||
areaDrawerMode: Ref<DineInAreaDrawerMode>;
|
areaDrawerMode: Ref<DineInAreaDrawerMode>;
|
||||||
@@ -91,30 +86,27 @@ export function createAreaActions(options: CreateAreaActionsOptions) {
|
|||||||
|
|
||||||
options.isSavingArea.value = true;
|
options.isSavingArea.value = true;
|
||||||
try {
|
try {
|
||||||
const areaId = options.areaForm.id || createDineInId('area');
|
const savedArea = await saveDineInAreaApi({
|
||||||
const areaPayload: DineInAreaDto = {
|
storeId: options.selectedStoreId.value,
|
||||||
id: areaId,
|
area: {
|
||||||
|
id: options.areaForm.id || undefined,
|
||||||
name: options.areaForm.name.trim(),
|
name: options.areaForm.name.trim(),
|
||||||
description: options.areaForm.description.trim(),
|
description: options.areaForm.description.trim(),
|
||||||
sort: Math.max(1, Math.floor(options.areaForm.sort)),
|
sort: Math.max(1, Math.floor(options.areaForm.sort)),
|
||||||
};
|
},
|
||||||
|
|
||||||
await saveDineInAreaApi({
|
|
||||||
storeId: options.selectedStoreId.value,
|
|
||||||
area: areaPayload,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
options.areas.value =
|
options.areas.value =
|
||||||
options.areaDrawerMode.value === 'edit' && options.areaForm.id
|
options.areaDrawerMode.value === 'edit' && options.areaForm.id
|
||||||
? sortAreas(
|
? sortAreas(
|
||||||
options.areas.value.map((item) =>
|
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) {
|
if (!options.selectedAreaId.value) {
|
||||||
options.selectedAreaId.value = areaPayload.id;
|
options.selectedAreaId.value = savedArea.id;
|
||||||
}
|
}
|
||||||
options.fixSelectedArea();
|
options.fixSelectedArea();
|
||||||
options.updateSnapshot();
|
options.updateSnapshot();
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* 文件职责:堂食管理页面静态常量。
|
* 文件职责:堂食管理页面静态常量。
|
||||||
* 1. 维护默认区域、桌位、基础设置。
|
* 1. 维护状态、座位数、标签建议等固定枚举。
|
||||||
* 2. 提供状态、座位数等选项映射。
|
* 2. 不包含任何业务数据兜底。
|
||||||
*/
|
*/
|
||||||
import type {
|
import type {
|
||||||
DineInAreaDto,
|
|
||||||
DineInBasicSettingsDto,
|
|
||||||
DineInEditableStatus,
|
DineInEditableStatus,
|
||||||
DineInTableDto,
|
|
||||||
DineInTableStatus,
|
DineInTableStatus,
|
||||||
} from '#/api/store-dinein';
|
} from '#/api/store-dinein';
|
||||||
import type {
|
import type {
|
||||||
@@ -55,100 +52,6 @@ export const DINE_IN_EDITABLE_STATUS_OPTIONS: Array<{
|
|||||||
{ label: '停用', value: 'disabled' },
|
{ 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 = [
|
export const TABLE_TAG_SUGGESTIONS = [
|
||||||
'VIP',
|
'VIP',
|
||||||
'包厢',
|
'包厢',
|
||||||
|
|||||||
@@ -22,14 +22,7 @@ import {
|
|||||||
} from '#/api/store-dinein';
|
} from '#/api/store-dinein';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_DINE_IN_AREAS,
|
|
||||||
DEFAULT_DINE_IN_BASIC_SETTINGS,
|
|
||||||
DEFAULT_DINE_IN_TABLES,
|
|
||||||
} from './constants';
|
|
||||||
import {
|
|
||||||
cloneAreas,
|
|
||||||
cloneBasicSettings,
|
cloneBasicSettings,
|
||||||
cloneTables,
|
|
||||||
createSettingsSnapshot,
|
createSettingsSnapshot,
|
||||||
sortAreas,
|
sortAreas,
|
||||||
sortTables,
|
sortTables,
|
||||||
@@ -38,9 +31,12 @@ import {
|
|||||||
interface CreateDataActionsOptions {
|
interface CreateDataActionsOptions {
|
||||||
areas: Ref<DineInAreaDto[]>;
|
areas: Ref<DineInAreaDto[]>;
|
||||||
basicSettings: DineInBasicSettingsDto;
|
basicSettings: DineInBasicSettingsDto;
|
||||||
|
clearSettings: () => void;
|
||||||
|
isConfigured: Ref<boolean>;
|
||||||
isPageLoading: Ref<boolean>;
|
isPageLoading: Ref<boolean>;
|
||||||
isSavingBasic: Ref<boolean>;
|
isSavingBasic: Ref<boolean>;
|
||||||
isStoreLoading: Ref<boolean>;
|
isStoreLoading: Ref<boolean>;
|
||||||
|
loadedStoreId: Ref<string>;
|
||||||
selectedAreaId: Ref<string>;
|
selectedAreaId: Ref<string>;
|
||||||
selectedStoreId: Ref<string>;
|
selectedStoreId: Ref<string>;
|
||||||
snapshot: Ref<DineInSettingsSnapshot | null>;
|
snapshot: Ref<DineInSettingsSnapshot | null>;
|
||||||
@@ -57,14 +53,6 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
|||||||
next.overtimeReminderMinutes;
|
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() {
|
function buildCurrentSnapshot() {
|
||||||
return createSettingsSnapshot({
|
return createSettingsSnapshot({
|
||||||
@@ -96,27 +84,31 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
|||||||
const result = await getStoreDineInSettingsApi(storeId);
|
const result = await getStoreDineInSettingsApi(storeId);
|
||||||
if (options.selectedStoreId.value !== currentStoreId) return;
|
if (options.selectedStoreId.value !== currentStoreId) return;
|
||||||
|
|
||||||
options.areas.value = sortAreas(
|
if (!result.isConfigured) {
|
||||||
result.areas?.length > 0
|
options.clearSettings();
|
||||||
? result.areas
|
options.isConfigured.value = false;
|
||||||
: cloneAreas(DEFAULT_DINE_IN_AREAS),
|
options.loadedStoreId.value = currentStoreId;
|
||||||
);
|
options.snapshot.value = buildCurrentSnapshot();
|
||||||
options.tables.value = sortTables(
|
return;
|
||||||
result.tables?.length > 0
|
}
|
||||||
? result.tables
|
|
||||||
: cloneTables(DEFAULT_DINE_IN_TABLES),
|
options.areas.value = sortAreas(result.areas ?? []);
|
||||||
);
|
options.tables.value = sortTables(result.tables ?? []);
|
||||||
syncBasicSettings({
|
if (result.basicSettings) {
|
||||||
...DEFAULT_DINE_IN_BASIC_SETTINGS,
|
syncBasicSettings(cloneBasicSettings(result.basicSettings));
|
||||||
...result.basicSettings,
|
}
|
||||||
});
|
|
||||||
fixSelectedArea();
|
fixSelectedArea();
|
||||||
|
options.isConfigured.value = true;
|
||||||
|
options.loadedStoreId.value = currentStoreId;
|
||||||
|
|
||||||
options.snapshot.value = buildCurrentSnapshot();
|
options.snapshot.value = buildCurrentSnapshot();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
applyDefaultSettings();
|
options.isConfigured.value = false;
|
||||||
options.snapshot.value = buildCurrentSnapshot();
|
options.loadedStoreId.value = '';
|
||||||
|
options.snapshot.value = null;
|
||||||
|
options.clearSettings();
|
||||||
|
message.error('加载堂食设置失败,请稍后重试');
|
||||||
} finally {
|
} finally {
|
||||||
options.isPageLoading.value = false;
|
options.isPageLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -138,7 +130,8 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
|||||||
|
|
||||||
if (options.stores.value.length === 0) {
|
if (options.stores.value.length === 0) {
|
||||||
options.selectedStoreId.value = '';
|
options.selectedStoreId.value = '';
|
||||||
applyDefaultSettings();
|
options.isConfigured.value = false;
|
||||||
|
options.clearSettings();
|
||||||
options.snapshot.value = null;
|
options.snapshot.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -156,9 +149,11 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
message.error('加载门店失败,请稍后重试');
|
||||||
options.stores.value = [];
|
options.stores.value = [];
|
||||||
options.selectedStoreId.value = '';
|
options.selectedStoreId.value = '';
|
||||||
applyDefaultSettings();
|
options.isConfigured.value = false;
|
||||||
|
options.clearSettings();
|
||||||
options.snapshot.value = null;
|
options.snapshot.value = null;
|
||||||
} finally {
|
} finally {
|
||||||
options.isStoreLoading.value = false;
|
options.isStoreLoading.value = false;
|
||||||
@@ -174,6 +169,7 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
|||||||
storeId: options.selectedStoreId.value,
|
storeId: options.selectedStoreId.value,
|
||||||
basicSettings: cloneBasicSettings(options.basicSettings),
|
basicSettings: cloneBasicSettings(options.basicSettings),
|
||||||
});
|
});
|
||||||
|
options.isConfigured.value = true;
|
||||||
options.snapshot.value = buildCurrentSnapshot();
|
options.snapshot.value = buildCurrentSnapshot();
|
||||||
message.success('堂食设置已保存');
|
message.success('堂食设置已保存');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -185,10 +181,11 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
|||||||
|
|
||||||
/** 重置基础设置到最近快照。 */
|
/** 重置基础设置到最近快照。 */
|
||||||
function resetBasicSettings() {
|
function resetBasicSettings() {
|
||||||
const source =
|
if (!options.snapshot.value) {
|
||||||
options.snapshot.value?.basicSettings ??
|
message.warning('暂无可恢复的已保存配置');
|
||||||
cloneBasicSettings(DEFAULT_DINE_IN_BASIC_SETTINGS);
|
return;
|
||||||
syncBasicSettings(source);
|
}
|
||||||
|
syncBasicSettings(options.snapshot.value.basicSettings);
|
||||||
message.success('已恢复到最近一次保存状态');
|
message.success('已恢复到最近一次保存状态');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,11 +58,6 @@ export function sortTables(source: DineInTableDto[]) {
|
|||||||
return cloneTables(source).toSorted((a, b) => a.code.localeCompare(b.code));
|
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) {
|
export function normalizeTableCode(code: string) {
|
||||||
return code.trim().toUpperCase();
|
return code.trim().toUpperCase();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ComputedRef, Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件职责:堂食桌位动作。
|
* 文件职责:堂食桌位动作。
|
||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
} from '#/api/store-dinein';
|
} from '#/api/store-dinein';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createDineInId,
|
|
||||||
generateBatchCodes,
|
generateBatchCodes,
|
||||||
normalizeTableCode,
|
normalizeTableCode,
|
||||||
sortTables,
|
sortTables,
|
||||||
@@ -36,7 +35,6 @@ import {
|
|||||||
interface CreateTableActionsOptions {
|
interface CreateTableActionsOptions {
|
||||||
areas: Ref<DineInAreaDto[]>;
|
areas: Ref<DineInAreaDto[]>;
|
||||||
batchForm: DineInBatchFormState;
|
batchForm: DineInBatchFormState;
|
||||||
batchPreviewCodes: ComputedRef<string[]>;
|
|
||||||
isBatchModalOpen: Ref<boolean>;
|
isBatchModalOpen: Ref<boolean>;
|
||||||
isSavingBatch: Ref<boolean>;
|
isSavingBatch: Ref<boolean>;
|
||||||
isSavingTable: Ref<boolean>;
|
isSavingTable: Ref<boolean>;
|
||||||
@@ -120,7 +118,6 @@ export function createTableActions(options: CreateTableActionsOptions) {
|
|||||||
|
|
||||||
options.isSavingTable.value = true;
|
options.isSavingTable.value = true;
|
||||||
try {
|
try {
|
||||||
const tableId = options.tableForm.id || createDineInId('table');
|
|
||||||
let nextStatus: DineInTableStatus = options.tableForm.sourceStatus;
|
let nextStatus: DineInTableStatus = options.tableForm.sourceStatus;
|
||||||
if (options.tableForm.isDisabled) {
|
if (options.tableForm.isDisabled) {
|
||||||
nextStatus = 'disabled';
|
nextStatus = 'disabled';
|
||||||
@@ -128,28 +125,26 @@ export function createTableActions(options: CreateTableActionsOptions) {
|
|||||||
nextStatus = 'free';
|
nextStatus = 'free';
|
||||||
}
|
}
|
||||||
|
|
||||||
const tablePayload: DineInTableDto = {
|
const savedTable = await saveDineInTableApi({
|
||||||
id: tableId,
|
storeId: options.selectedStoreId.value,
|
||||||
|
table: {
|
||||||
|
id: options.tableForm.id || undefined,
|
||||||
code: normalizeTableCode(options.tableForm.code),
|
code: normalizeTableCode(options.tableForm.code),
|
||||||
areaId: options.tableForm.areaId,
|
areaId: options.tableForm.areaId,
|
||||||
seats: options.tableForm.seats,
|
seats: options.tableForm.seats,
|
||||||
status: nextStatus,
|
status: nextStatus,
|
||||||
tags: [...options.tableForm.tags],
|
tags: [...options.tableForm.tags],
|
||||||
};
|
},
|
||||||
|
|
||||||
await saveDineInTableApi({
|
|
||||||
storeId: options.selectedStoreId.value,
|
|
||||||
table: tablePayload,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
options.tables.value =
|
options.tables.value =
|
||||||
options.tableDrawerMode.value === 'edit' && options.tableForm.id
|
options.tableDrawerMode.value === 'edit' && options.tableForm.id
|
||||||
? sortTables(
|
? sortTables(
|
||||||
options.tables.value.map((item) =>
|
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.updateSnapshot();
|
||||||
options.isTableDrawerOpen.value = false;
|
options.isTableDrawerOpen.value = false;
|
||||||
@@ -237,7 +232,7 @@ export function createTableActions(options: CreateTableActionsOptions) {
|
|||||||
|
|
||||||
options.isSavingBatch.value = true;
|
options.isSavingBatch.value = true;
|
||||||
try {
|
try {
|
||||||
await batchCreateDineInTablesApi({
|
const result = await batchCreateDineInTablesApi({
|
||||||
storeId: options.selectedStoreId.value,
|
storeId: options.selectedStoreId.value,
|
||||||
areaId: options.batchForm.areaId,
|
areaId: options.batchForm.areaId,
|
||||||
codePrefix: options.batchForm.codePrefix,
|
codePrefix: options.batchForm.codePrefix,
|
||||||
@@ -246,15 +241,7 @@ export function createTableActions(options: CreateTableActionsOptions) {
|
|||||||
seats: options.batchForm.seats,
|
seats: options.batchForm.seats,
|
||||||
});
|
});
|
||||||
|
|
||||||
const createdTables: DineInTableDto[] =
|
const createdTables: DineInTableDto[] = result.createdTables ?? [];
|
||||||
options.batchPreviewCodes.value.map((code) => ({
|
|
||||||
id: createDineInId('table'),
|
|
||||||
areaId: options.batchForm.areaId,
|
|
||||||
code,
|
|
||||||
seats: options.batchForm.seats,
|
|
||||||
status: 'free',
|
|
||||||
tags: [],
|
|
||||||
}));
|
|
||||||
|
|
||||||
options.tables.value = sortTables([
|
options.tables.value = sortTables([
|
||||||
...options.tables.value,
|
...options.tables.value,
|
||||||
|
|||||||
@@ -23,9 +23,6 @@ import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue';
|
|||||||
|
|
||||||
import { createAreaActions } from './dinein-page/area-actions';
|
import { createAreaActions } from './dinein-page/area-actions';
|
||||||
import {
|
import {
|
||||||
DEFAULT_DINE_IN_AREAS,
|
|
||||||
DEFAULT_DINE_IN_BASIC_SETTINGS,
|
|
||||||
DEFAULT_DINE_IN_TABLES,
|
|
||||||
DINE_IN_SEATS_OPTIONS,
|
DINE_IN_SEATS_OPTIONS,
|
||||||
DINE_IN_STATUS_MAP,
|
DINE_IN_STATUS_MAP,
|
||||||
TABLE_TAG_SUGGESTIONS,
|
TABLE_TAG_SUGGESTIONS,
|
||||||
@@ -33,18 +30,19 @@ import {
|
|||||||
import { createCopyActions } from './dinein-page/copy-actions';
|
import { createCopyActions } from './dinein-page/copy-actions';
|
||||||
import { createDataActions } from './dinein-page/data-actions';
|
import { createDataActions } from './dinein-page/data-actions';
|
||||||
import {
|
import {
|
||||||
cloneAreas,
|
|
||||||
cloneBasicSettings,
|
cloneBasicSettings,
|
||||||
cloneTables,
|
|
||||||
countAreaTables,
|
countAreaTables,
|
||||||
createSettingsSnapshot,
|
|
||||||
generateBatchCodes,
|
generateBatchCodes,
|
||||||
resolveStatusClassName,
|
resolveStatusClassName,
|
||||||
sortAreas,
|
|
||||||
sortTables,
|
|
||||||
} from './dinein-page/helpers';
|
} from './dinein-page/helpers';
|
||||||
import { createTableActions } from './dinein-page/table-actions';
|
import { createTableActions } from './dinein-page/table-actions';
|
||||||
|
|
||||||
|
const EMPTY_BASIC_SETTINGS: DineInBasicSettingsDto = {
|
||||||
|
enabled: false,
|
||||||
|
defaultDiningMinutes: 0,
|
||||||
|
overtimeReminderMinutes: 0,
|
||||||
|
};
|
||||||
|
|
||||||
export function useStoreDineInPage() {
|
export function useStoreDineInPage() {
|
||||||
// 1. 页面 loading / submitting 状态。
|
// 1. 页面 loading / submitting 状态。
|
||||||
const isStoreLoading = ref(false);
|
const isStoreLoading = ref(false);
|
||||||
@@ -54,27 +52,19 @@ export function useStoreDineInPage() {
|
|||||||
const isSavingTable = ref(false);
|
const isSavingTable = ref(false);
|
||||||
const isSavingBatch = ref(false);
|
const isSavingBatch = ref(false);
|
||||||
const isCopySubmitting = ref(false);
|
const isCopySubmitting = ref(false);
|
||||||
|
const isConfigured = ref(false);
|
||||||
|
|
||||||
// 2. 页面核心业务数据。
|
// 2. 页面核心业务数据。
|
||||||
const stores = ref<StoreListItemDto[]>([]);
|
const stores = ref<StoreListItemDto[]>([]);
|
||||||
const selectedStoreId = ref('');
|
const selectedStoreId = ref('');
|
||||||
const areas = ref<DineInAreaDto[]>(
|
const loadedStoreId = ref('');
|
||||||
sortAreas(cloneAreas(DEFAULT_DINE_IN_AREAS)),
|
const areas = ref<DineInAreaDto[]>([]);
|
||||||
);
|
const tables = ref<DineInTableDto[]>([]);
|
||||||
const tables = ref<DineInTableDto[]>(
|
|
||||||
sortTables(cloneTables(DEFAULT_DINE_IN_TABLES)),
|
|
||||||
);
|
|
||||||
const basicSettings = reactive<DineInBasicSettingsDto>(
|
const basicSettings = reactive<DineInBasicSettingsDto>(
|
||||||
cloneBasicSettings(DEFAULT_DINE_IN_BASIC_SETTINGS),
|
cloneBasicSettings(EMPTY_BASIC_SETTINGS),
|
||||||
);
|
|
||||||
const selectedAreaId = ref(areas.value[0]?.id ?? '');
|
|
||||||
const snapshot = ref<DineInSettingsSnapshot | null>(
|
|
||||||
createSettingsSnapshot({
|
|
||||||
areas: areas.value,
|
|
||||||
tables: tables.value,
|
|
||||||
basicSettings,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
const selectedAreaId = ref('');
|
||||||
|
const snapshot = ref<DineInSettingsSnapshot | null>(null);
|
||||||
|
|
||||||
// 3. 复制弹窗状态。
|
// 3. 复制弹窗状态。
|
||||||
const isCopyModalOpen = ref(false);
|
const isCopyModalOpen = ref(false);
|
||||||
@@ -121,6 +111,20 @@ export function useStoreDineInPage() {
|
|||||||
stores.value.find((store) => store.id === selectedStoreId.value)?.name ??
|
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(
|
const selectedArea = computed(
|
||||||
() =>
|
() =>
|
||||||
@@ -180,6 +184,18 @@ export function useStoreDineInPage() {
|
|||||||
tableDrawerMode.value === 'edit' ? '保存修改' : '确认添加',
|
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. 数据域动作装配。
|
// 7. 数据域动作装配。
|
||||||
const {
|
const {
|
||||||
buildCurrentSnapshot,
|
buildCurrentSnapshot,
|
||||||
@@ -191,9 +207,12 @@ export function useStoreDineInPage() {
|
|||||||
} = createDataActions({
|
} = createDataActions({
|
||||||
areas,
|
areas,
|
||||||
basicSettings,
|
basicSettings,
|
||||||
|
clearSettings,
|
||||||
|
isConfigured,
|
||||||
isPageLoading,
|
isPageLoading,
|
||||||
isSavingBasic,
|
isSavingBasic,
|
||||||
isStoreLoading,
|
isStoreLoading,
|
||||||
|
loadedStoreId,
|
||||||
selectedAreaId,
|
selectedAreaId,
|
||||||
selectedStoreId,
|
selectedStoreId,
|
||||||
snapshot,
|
snapshot,
|
||||||
@@ -261,7 +280,6 @@ export function useStoreDineInPage() {
|
|||||||
} = createTableActions({
|
} = createTableActions({
|
||||||
areas,
|
areas,
|
||||||
batchForm,
|
batchForm,
|
||||||
batchPreviewCodes,
|
|
||||||
isBatchModalOpen,
|
isBatchModalOpen,
|
||||||
isSavingBatch,
|
isSavingBatch,
|
||||||
isSavingTable,
|
isSavingTable,
|
||||||
@@ -304,17 +322,12 @@ export function useStoreDineInPage() {
|
|||||||
// 9. 门店切换时自动刷新配置。
|
// 9. 门店切换时自动刷新配置。
|
||||||
watch(selectedStoreId, async (storeId) => {
|
watch(selectedStoreId, async (storeId) => {
|
||||||
if (!storeId) {
|
if (!storeId) {
|
||||||
areas.value = sortAreas(cloneAreas(DEFAULT_DINE_IN_AREAS));
|
clearSettings();
|
||||||
tables.value = sortTables(cloneTables(DEFAULT_DINE_IN_TABLES));
|
isConfigured.value = false;
|
||||||
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 ?? '';
|
|
||||||
snapshot.value = null;
|
snapshot.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
isConfigured.value = false;
|
||||||
await loadStoreSettings(storeId);
|
await loadStoreSettings(storeId);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -335,6 +348,7 @@ export function useStoreDineInPage() {
|
|||||||
basicSettings,
|
basicSettings,
|
||||||
batchForm,
|
batchForm,
|
||||||
batchPreviewCodes,
|
batchPreviewCodes,
|
||||||
|
canOperate,
|
||||||
copyCandidates,
|
copyCandidates,
|
||||||
copyTargetStoreIds,
|
copyTargetStoreIds,
|
||||||
filteredTables,
|
filteredTables,
|
||||||
@@ -351,6 +365,7 @@ export function useStoreDineInPage() {
|
|||||||
isCopyIndeterminate,
|
isCopyIndeterminate,
|
||||||
isCopyModalOpen,
|
isCopyModalOpen,
|
||||||
isCopySubmitting,
|
isCopySubmitting,
|
||||||
|
isConfigured,
|
||||||
isPageLoading,
|
isPageLoading,
|
||||||
isSavingArea,
|
isSavingArea,
|
||||||
isSavingBasic,
|
isSavingBasic,
|
||||||
@@ -368,6 +383,9 @@ export function useStoreDineInPage() {
|
|||||||
selectedArea,
|
selectedArea,
|
||||||
selectedAreaId,
|
selectedAreaId,
|
||||||
selectedAreaTableCount,
|
selectedAreaTableCount,
|
||||||
|
hasSelectedStore,
|
||||||
|
hasLoadedStoreSettings,
|
||||||
|
loadedStoreId,
|
||||||
selectedStoreId,
|
selectedStoreId,
|
||||||
selectedStoreName,
|
selectedStoreName,
|
||||||
setAreaDescription,
|
setAreaDescription,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const {
|
|||||||
basicSettings,
|
basicSettings,
|
||||||
batchForm,
|
batchForm,
|
||||||
batchPreviewCodes,
|
batchPreviewCodes,
|
||||||
|
canOperate,
|
||||||
copyCandidates,
|
copyCandidates,
|
||||||
copyTargetStoreIds,
|
copyTargetStoreIds,
|
||||||
filteredTables,
|
filteredTables,
|
||||||
@@ -42,12 +43,14 @@ const {
|
|||||||
handleSubmitArea,
|
handleSubmitArea,
|
||||||
handleSubmitBatch,
|
handleSubmitBatch,
|
||||||
handleSubmitTable,
|
handleSubmitTable,
|
||||||
|
hasLoadedStoreSettings,
|
||||||
isAreaDrawerOpen,
|
isAreaDrawerOpen,
|
||||||
isBatchModalOpen,
|
isBatchModalOpen,
|
||||||
isCopyAllChecked,
|
isCopyAllChecked,
|
||||||
isCopyIndeterminate,
|
isCopyIndeterminate,
|
||||||
isCopyModalOpen,
|
isCopyModalOpen,
|
||||||
isCopySubmitting,
|
isCopySubmitting,
|
||||||
|
isConfigured,
|
||||||
isPageLoading,
|
isPageLoading,
|
||||||
isSavingArea,
|
isSavingArea,
|
||||||
isSavingBasic,
|
isSavingBasic,
|
||||||
@@ -125,7 +128,9 @@ function onViewQrCode(tableCode: string) {
|
|||||||
:selected-store-id="selectedStoreId"
|
:selected-store-id="selectedStoreId"
|
||||||
:store-options="storeOptions"
|
:store-options="storeOptions"
|
||||||
:is-store-loading="isStoreLoading"
|
:is-store-loading="isStoreLoading"
|
||||||
:copy-disabled="!selectedStoreId || copyCandidates.length === 0"
|
:copy-disabled="
|
||||||
|
!canOperate || !isConfigured || copyCandidates.length === 0
|
||||||
|
"
|
||||||
@update:selected-store-id="setSelectedStoreId"
|
@update:selected-store-id="setSelectedStoreId"
|
||||||
@copy="openCopyModal"
|
@copy="openCopyModal"
|
||||||
/>
|
/>
|
||||||
@@ -138,7 +143,14 @@ function onViewQrCode(tableCode: string) {
|
|||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<Spin :spinning="isPageLoading">
|
<Spin :spinning="isPageLoading">
|
||||||
|
<Card v-if="hasLoadedStoreSettings && !isConfigured" :bordered="false">
|
||||||
|
<Empty
|
||||||
|
description="当前门店尚未配置堂食规则。请先填写并保存,之后可执行复制。"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<DineInAreaSection
|
<DineInAreaSection
|
||||||
|
:can-operate="canOperate"
|
||||||
:areas="areas"
|
:areas="areas"
|
||||||
:selected-area-id="selectedAreaId"
|
:selected-area-id="selectedAreaId"
|
||||||
:selected-area="selectedArea"
|
:selected-area="selectedArea"
|
||||||
@@ -151,6 +163,8 @@ function onViewQrCode(tableCode: string) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<DineInTableGridSection
|
<DineInTableGridSection
|
||||||
|
:can-operate="canOperate"
|
||||||
|
:has-selected-area="Boolean(selectedArea)"
|
||||||
:tables="filteredTables"
|
:tables="filteredTables"
|
||||||
:is-saving="isSavingTable"
|
:is-saving="isSavingTable"
|
||||||
:status-map="DINE_IN_STATUS_MAP"
|
:status-map="DINE_IN_STATUS_MAP"
|
||||||
@@ -163,6 +177,7 @@ function onViewQrCode(tableCode: string) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<DineInBasicSettingsCard
|
<DineInBasicSettingsCard
|
||||||
|
:can-operate="canOperate"
|
||||||
:settings="basicSettings"
|
:settings="basicSettings"
|
||||||
:is-saving="isSavingBasic"
|
:is-saving="isSavingBasic"
|
||||||
:on-set-enabled="setDineInEnabled"
|
:on-set-enabled="setDineInEnabled"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Button, Card, InputNumber } from 'ant-design-vue';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
baseDeliveryFee: number;
|
baseDeliveryFee: number;
|
||||||
|
canOperate: boolean;
|
||||||
freeDeliveryThreshold: null | number;
|
freeDeliveryThreshold: null | number;
|
||||||
isSaving: boolean;
|
isSaving: boolean;
|
||||||
minimumOrderAmount: number;
|
minimumOrderAmount: number;
|
||||||
@@ -46,6 +47,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
:precision="2"
|
:precision="2"
|
||||||
:step="1"
|
:step="1"
|
||||||
:controls="false"
|
:controls="false"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
class="fees-input"
|
class="fees-input"
|
||||||
placeholder="如:15.00"
|
placeholder="如:15.00"
|
||||||
@update:value="
|
@update:value="
|
||||||
@@ -69,6 +71,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
:precision="2"
|
:precision="2"
|
||||||
:step="1"
|
:step="1"
|
||||||
:controls="false"
|
:controls="false"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
class="fees-input"
|
class="fees-input"
|
||||||
placeholder="如:3.00"
|
placeholder="如:3.00"
|
||||||
@update:value="
|
@update:value="
|
||||||
@@ -92,6 +95,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
:precision="2"
|
:precision="2"
|
||||||
:step="1"
|
:step="1"
|
||||||
:controls="false"
|
:controls="false"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
class="fees-input"
|
class="fees-input"
|
||||||
placeholder="如:30.00"
|
placeholder="如:30.00"
|
||||||
@update:value="
|
@update:value="
|
||||||
@@ -109,8 +113,18 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="fees-actions">
|
<div class="fees-actions">
|
||||||
<Button :disabled="props.isSaving" @click="emit('reset')">重置</Button>
|
<Button
|
||||||
<Button type="primary" :loading="props.isSaving" @click="emit('save')">
|
:disabled="props.isSaving || !props.canOperate"
|
||||||
|
@click="emit('reset')"
|
||||||
|
>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
:loading="props.isSaving"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
|
@click="emit('save')"
|
||||||
|
>
|
||||||
保存设置
|
保存设置
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import { Button, Card, InputNumber, Switch } from 'ant-design-vue';
|
import { Button, Card, InputNumber, Switch } from 'ant-design-vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
canOperate: boolean;
|
||||||
cutleryAmount: number;
|
cutleryAmount: number;
|
||||||
cutleryEnabled: boolean;
|
cutleryEnabled: boolean;
|
||||||
isSaving: boolean;
|
isSaving: boolean;
|
||||||
@@ -41,6 +42,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
<div class="other-fee-row">
|
<div class="other-fee-row">
|
||||||
<Switch
|
<Switch
|
||||||
:checked="props.cutleryEnabled"
|
:checked="props.cutleryEnabled"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
@update:checked="(value) => props.onSetCutleryEnabled(Boolean(value))"
|
@update:checked="(value) => props.onSetCutleryEnabled(Boolean(value))"
|
||||||
/>
|
/>
|
||||||
<div class="other-fee-meta">
|
<div class="other-fee-meta">
|
||||||
@@ -57,7 +59,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
:precision="2"
|
:precision="2"
|
||||||
:step="0.5"
|
:step="0.5"
|
||||||
:controls="false"
|
:controls="false"
|
||||||
:disabled="!props.cutleryEnabled"
|
:disabled="!props.cutleryEnabled || !props.canOperate"
|
||||||
class="other-fee-input"
|
class="other-fee-input"
|
||||||
placeholder="如:1.00"
|
placeholder="如:1.00"
|
||||||
@update:value="
|
@update:value="
|
||||||
@@ -71,6 +73,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
<div class="other-fee-row">
|
<div class="other-fee-row">
|
||||||
<Switch
|
<Switch
|
||||||
:checked="props.rushEnabled"
|
:checked="props.rushEnabled"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
@update:checked="(value) => props.onSetRushEnabled(Boolean(value))"
|
@update:checked="(value) => props.onSetRushEnabled(Boolean(value))"
|
||||||
/>
|
/>
|
||||||
<div class="other-fee-meta">
|
<div class="other-fee-meta">
|
||||||
@@ -85,7 +88,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
:precision="2"
|
:precision="2"
|
||||||
:step="0.5"
|
:step="0.5"
|
||||||
:controls="false"
|
:controls="false"
|
||||||
:disabled="!props.rushEnabled"
|
:disabled="!props.rushEnabled || !props.canOperate"
|
||||||
class="other-fee-input"
|
class="other-fee-input"
|
||||||
placeholder="如:3.00"
|
placeholder="如:3.00"
|
||||||
@update:value="
|
@update:value="
|
||||||
@@ -96,8 +99,18 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="fees-actions">
|
<div class="fees-actions">
|
||||||
<Button :disabled="props.isSaving" @click="emit('reset')">重置</Button>
|
<Button
|
||||||
<Button type="primary" :loading="props.isSaving" @click="emit('save')">
|
:disabled="props.isSaving || !props.canOperate"
|
||||||
|
@click="emit('reset')"
|
||||||
|
>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
:loading="props.isSaving"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
|
@click="emit('save')"
|
||||||
|
>
|
||||||
保存设置
|
保存设置
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { PackagingFeeTierDto } from '#/api/store-fees';
|
|||||||
import { Button, Card, InputNumber, Switch } from 'ant-design-vue';
|
import { Button, Card, InputNumber, Switch } from 'ant-design-vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
canOperate: boolean;
|
||||||
fixedPackagingFee: number;
|
fixedPackagingFee: number;
|
||||||
formatCurrency: (value: number) => string;
|
formatCurrency: (value: number) => string;
|
||||||
formatTierRange: (tier: PackagingFeeTierDto) => string;
|
formatTierRange: (tier: PackagingFeeTierDto) => string;
|
||||||
@@ -31,11 +32,6 @@ const emit = defineEmits<{
|
|||||||
(event: 'save'): void;
|
(event: 'save'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const packagingModes: Array<{ label: string; value: 'item' | 'order' }> = [
|
|
||||||
{ label: '按订单收取', value: 'order' },
|
|
||||||
{ label: '按商品收取', value: 'item' },
|
|
||||||
];
|
|
||||||
|
|
||||||
function toNumber(value: null | number | string, fallback = 0) {
|
function toNumber(value: null | number | string, fallback = 0) {
|
||||||
const parsed = Number(value);
|
const parsed = Number(value);
|
||||||
return Number.isFinite(parsed) ? parsed : fallback;
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
@@ -48,17 +44,51 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
<span class="section-title">包装费设置</span>
|
<span class="section-title">包装费设置</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="packaging-mode-switch">
|
<div class="packaging-mode-toggle-row">
|
||||||
<button
|
<button
|
||||||
v-for="mode in packagingModes"
|
|
||||||
:key="mode.value"
|
|
||||||
type="button"
|
type="button"
|
||||||
class="mode-switch-item"
|
class="mode-toggle-label"
|
||||||
:class="{ active: props.packagingMode === mode.value }"
|
:class="{ active: props.packagingMode === 'order' }"
|
||||||
@click="props.onSetPackagingMode(mode.value)"
|
:disabled="!props.canOperate"
|
||||||
|
@click="props.onSetPackagingMode('order')"
|
||||||
>
|
>
|
||||||
{{ mode.label }}
|
按订单收取
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
:checked="props.packagingMode === 'item'"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
|
@update:checked="
|
||||||
|
(checked) => props.onSetPackagingMode(checked ? 'item' : 'order')
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="mode-toggle-label"
|
||||||
|
:class="{ active: props.packagingMode === 'item' }"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
|
@click="props.onSetPackagingMode('item')"
|
||||||
|
>
|
||||||
|
按商品收取
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="packaging-mode-guide"
|
||||||
|
:class="
|
||||||
|
props.packagingMode === 'item'
|
||||||
|
? 'packaging-mode-guide-item'
|
||||||
|
: 'packaging-mode-guide-order'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="guide-title">当前生效规则</div>
|
||||||
|
<div v-if="props.packagingMode === 'order'" class="guide-desc">
|
||||||
|
订单将按固定/阶梯包装费计算;商品包装费配置暂不生效。
|
||||||
|
</div>
|
||||||
|
<div v-else class="guide-desc">
|
||||||
|
订单包装费将汇总商品维度配置;本页固定/阶梯包装费暂不生效。
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="props.packagingMode === 'order'">
|
<template v-if="props.packagingMode === 'order'">
|
||||||
@@ -73,6 +103,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
:precision="2"
|
:precision="2"
|
||||||
:step="0.5"
|
:step="0.5"
|
||||||
:controls="false"
|
:controls="false"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
class="fees-input"
|
class="fees-input"
|
||||||
placeholder="如:2.00"
|
placeholder="如:2.00"
|
||||||
@update:value="
|
@update:value="
|
||||||
@@ -91,6 +122,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
<div class="packaging-tier-toggle-row">
|
<div class="packaging-tier-toggle-row">
|
||||||
<Switch
|
<Switch
|
||||||
:checked="props.tieredEnabled"
|
:checked="props.tieredEnabled"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
@update:checked="
|
@update:checked="
|
||||||
(value) => props.onSetTieredEnabled(Boolean(value))
|
(value) => props.onSetTieredEnabled(Boolean(value))
|
||||||
"
|
"
|
||||||
@@ -117,12 +149,15 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
<td>{{ props.formatTierRange(tier) }}</td>
|
<td>{{ props.formatTierRange(tier) }}</td>
|
||||||
<td>{{ props.formatCurrency(tier.fee) }}</td>
|
<td>{{ props.formatCurrency(tier.fee) }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a class="fees-table-link" @click="emit('editTier', tier)">
|
<a
|
||||||
|
class="fees-table-link"
|
||||||
|
@click="props.canOperate && emit('editTier', tier)"
|
||||||
|
>
|
||||||
编辑
|
编辑
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
class="fees-table-link danger"
|
class="fees-table-link danger"
|
||||||
@click="emit('deleteTier', tier)"
|
@click="props.canOperate && emit('deleteTier', tier)"
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</a>
|
</a>
|
||||||
@@ -133,7 +168,9 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="packaging-tier-add-row">
|
<div class="packaging-tier-add-row">
|
||||||
<Button @click="emit('addTier')">+ 添加阶梯</Button>
|
<Button :disabled="!props.canOperate" @click="emit('addTier')">
|
||||||
|
+ 添加阶梯
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -149,8 +186,18 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="fees-actions">
|
<div class="fees-actions">
|
||||||
<Button :disabled="props.isSaving" @click="emit('reset')">重置</Button>
|
<Button
|
||||||
<Button type="primary" :loading="props.isSaving" @click="emit('save')">
|
:disabled="props.isSaving || !props.canOperate"
|
||||||
|
@click="emit('reset')"
|
||||||
|
>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
:loading="props.isSaving"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
|
@click="emit('save')"
|
||||||
|
>
|
||||||
保存设置
|
保存设置
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { PackagingFeeTierFormState } from '#/views/store/fees/types';
|
|||||||
import { Button, Drawer, InputNumber } from 'ant-design-vue';
|
import { Button, Drawer, InputNumber } from 'ant-design-vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
canOperate: boolean;
|
||||||
form: PackagingFeeTierFormState;
|
form: PackagingFeeTierFormState;
|
||||||
isSaving: boolean;
|
isSaving: boolean;
|
||||||
onSetFee: (value: number) => void;
|
onSetFee: (value: number) => void;
|
||||||
@@ -49,6 +50,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
:precision="2"
|
:precision="2"
|
||||||
:step="1"
|
:step="1"
|
||||||
:controls="false"
|
:controls="false"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
class="drawer-input"
|
class="drawer-input"
|
||||||
placeholder="起始金额"
|
placeholder="起始金额"
|
||||||
@update:value="
|
@update:value="
|
||||||
@@ -63,6 +65,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
:precision="2"
|
:precision="2"
|
||||||
:step="1"
|
:step="1"
|
||||||
:controls="false"
|
:controls="false"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
class="drawer-input"
|
class="drawer-input"
|
||||||
placeholder="结束金额(留空表示无上限)"
|
placeholder="结束金额(留空表示无上限)"
|
||||||
@update:value="
|
@update:value="
|
||||||
@@ -90,6 +93,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
:precision="2"
|
:precision="2"
|
||||||
:step="0.5"
|
:step="0.5"
|
||||||
:controls="false"
|
:controls="false"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
class="drawer-input"
|
class="drawer-input"
|
||||||
placeholder="如:2.00"
|
placeholder="如:2.00"
|
||||||
@update:value="
|
@update:value="
|
||||||
@@ -102,12 +106,16 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="drawer-footer">
|
<div class="drawer-footer">
|
||||||
<Button :disabled="props.isSaving" @click="emit('update:open', false)">
|
<Button
|
||||||
|
:disabled="props.isSaving || !props.canOperate"
|
||||||
|
@click="emit('update:open', false)"
|
||||||
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
:loading="props.isSaving"
|
:loading="props.isSaving"
|
||||||
|
:disabled="!props.canOperate"
|
||||||
@click="emit('submit')"
|
@click="emit('submit')"
|
||||||
>
|
>
|
||||||
{{ props.form.id ? '保存修改' : '新增并保存' }}
|
{{ props.form.id ? '保存修改' : '新增并保存' }}
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import type {
|
import type { PackagingFeeMode } from '#/api/store-fees';
|
||||||
PackagingFeeMode,
|
|
||||||
PackagingFeeTierDto,
|
|
||||||
StoreFeesSettingsDto,
|
|
||||||
} from '#/api/store-fees';
|
|
||||||
|
|
||||||
/** 文件职责:费用设置页面常量定义。 */
|
/** 文件职责:费用设置页面常量定义。 */
|
||||||
|
|
||||||
@@ -15,47 +11,3 @@ export const PACKAGING_MODE_OPTIONS: Array<{
|
|||||||
{ label: '按订单收取', value: 'order' },
|
{ label: '按订单收取', value: 'order' },
|
||||||
{ label: '按商品收取', value: 'item' },
|
{ label: '按商品收取', value: 'item' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DEFAULT_PACKAGING_TIERS: PackagingFeeTierDto[] = [
|
|
||||||
{
|
|
||||||
id: 'packaging-tier-1',
|
|
||||||
minAmount: 0,
|
|
||||||
maxAmount: 30,
|
|
||||||
fee: 2,
|
|
||||||
sort: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'packaging-tier-2',
|
|
||||||
minAmount: 30,
|
|
||||||
maxAmount: 60,
|
|
||||||
fee: 3,
|
|
||||||
sort: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'packaging-tier-3',
|
|
||||||
minAmount: 60,
|
|
||||||
maxAmount: null,
|
|
||||||
fee: 5,
|
|
||||||
sort: 3,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const DEFAULT_FEES_SETTINGS: Omit<StoreFeesSettingsDto, 'storeId'> = {
|
|
||||||
minimumOrderAmount: 15,
|
|
||||||
baseDeliveryFee: 3,
|
|
||||||
freeDeliveryThreshold: 30,
|
|
||||||
packagingFeeMode: 'order',
|
|
||||||
orderPackagingFeeMode: 'tiered',
|
|
||||||
fixedPackagingFee: 2,
|
|
||||||
packagingFeeTiers: DEFAULT_PACKAGING_TIERS,
|
|
||||||
otherFees: {
|
|
||||||
cutlery: {
|
|
||||||
enabled: false,
|
|
||||||
amount: 1,
|
|
||||||
},
|
|
||||||
rush: {
|
|
||||||
enabled: false,
|
|
||||||
amount: 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import type { StoreListItemDto } from '#/api/store';
|
|||||||
* 1. 加载门店列表与门店费用配置。
|
* 1. 加载门店列表与门店费用配置。
|
||||||
* 2. 保存费用配置并维护快照。
|
* 2. 保存费用配置并维护快照。
|
||||||
*/
|
*/
|
||||||
import type { StoreFeesSettingsDto } from '#/api/store-fees';
|
import type {
|
||||||
|
SaveStoreFeesSettingsParams,
|
||||||
|
StoreFeesSettingsDto,
|
||||||
|
} from '#/api/store-fees';
|
||||||
import type {
|
import type {
|
||||||
StoreFeesFormState,
|
StoreFeesFormState,
|
||||||
StoreFeesSettingsSnapshot,
|
StoreFeesSettingsSnapshot,
|
||||||
@@ -20,7 +23,6 @@ import {
|
|||||||
saveStoreFeesSettingsApi,
|
saveStoreFeesSettingsApi,
|
||||||
} from '#/api/store-fees';
|
} from '#/api/store-fees';
|
||||||
|
|
||||||
import { DEFAULT_FEES_SETTINGS, DEFAULT_PACKAGING_TIERS } from './constants';
|
|
||||||
import {
|
import {
|
||||||
cloneOtherFees,
|
cloneOtherFees,
|
||||||
cloneTiers,
|
cloneTiers,
|
||||||
@@ -30,14 +32,28 @@ import {
|
|||||||
} from './helpers';
|
} from './helpers';
|
||||||
|
|
||||||
interface CreateDataActionsOptions {
|
interface CreateDataActionsOptions {
|
||||||
|
clearSettings: () => void;
|
||||||
form: StoreFeesFormState;
|
form: StoreFeesFormState;
|
||||||
|
isConfigured: Ref<boolean>;
|
||||||
isPageLoading: Ref<boolean>;
|
isPageLoading: Ref<boolean>;
|
||||||
isStoreLoading: Ref<boolean>;
|
isStoreLoading: Ref<boolean>;
|
||||||
|
loadedStoreId: Ref<string>;
|
||||||
selectedStoreId: Ref<string>;
|
selectedStoreId: Ref<string>;
|
||||||
snapshot: Ref<null | StoreFeesSettingsSnapshot>;
|
snapshot: Ref<null | StoreFeesSettingsSnapshot>;
|
||||||
stores: Ref<StoreListItemDto[]>;
|
stores: Ref<StoreListItemDto[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EMPTY_OTHER_FEES = {
|
||||||
|
cutlery: {
|
||||||
|
enabled: false,
|
||||||
|
amount: 0,
|
||||||
|
},
|
||||||
|
rush: {
|
||||||
|
enabled: false,
|
||||||
|
amount: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export function createDataActions(options: CreateDataActionsOptions) {
|
export function createDataActions(options: CreateDataActionsOptions) {
|
||||||
/** 同步页面表单,保持 reactive 引用不变。 */
|
/** 同步页面表单,保持 reactive 引用不变。 */
|
||||||
function syncForm(next: StoreFeesFormState) {
|
function syncForm(next: StoreFeesFormState) {
|
||||||
@@ -64,65 +80,52 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
|||||||
return createSettingsSnapshot(options.form);
|
return createSettingsSnapshot(options.form);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 应用默认配置。 */
|
|
||||||
function applyDefaultSettings() {
|
|
||||||
syncForm({
|
|
||||||
...DEFAULT_FEES_SETTINGS,
|
|
||||||
packagingFeeTiers: cloneTiers(DEFAULT_PACKAGING_TIERS),
|
|
||||||
otherFees: cloneOtherFees(DEFAULT_FEES_SETTINGS.otherFees),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 将接口返回值转为页面表单态。 */
|
/** 将接口返回值转为页面表单态。 */
|
||||||
function normalizeSettings(
|
function normalizeSettings(
|
||||||
source: null | Partial<StoreFeesSettingsDto> | undefined,
|
source: null | Partial<StoreFeesSettingsDto> | undefined,
|
||||||
): StoreFeesFormState {
|
): StoreFeesFormState {
|
||||||
|
const packagingFeeMode =
|
||||||
|
source?.packagingFeeMode === 'item' ||
|
||||||
|
source?.packagingFeeMode === 'order'
|
||||||
|
? source.packagingFeeMode
|
||||||
|
: 'order';
|
||||||
|
const orderPackagingFeeMode =
|
||||||
|
source?.orderPackagingFeeMode === 'fixed' ||
|
||||||
|
source?.orderPackagingFeeMode === 'tiered'
|
||||||
|
? source.orderPackagingFeeMode
|
||||||
|
: 'fixed';
|
||||||
|
|
||||||
|
const otherFees = cloneOtherFees({
|
||||||
|
cutlery: {
|
||||||
|
enabled: Boolean(source?.otherFees?.cutlery?.enabled),
|
||||||
|
amount: normalizeMoney(source?.otherFees?.cutlery?.amount ?? 0, 0),
|
||||||
|
},
|
||||||
|
rush: {
|
||||||
|
enabled: Boolean(source?.otherFees?.rush?.enabled),
|
||||||
|
amount: normalizeMoney(source?.otherFees?.rush?.amount ?? 0, 0),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
minimumOrderAmount: normalizeMoney(
|
minimumOrderAmount: normalizeMoney(source?.minimumOrderAmount ?? 0, 0),
|
||||||
source?.minimumOrderAmount ?? DEFAULT_FEES_SETTINGS.minimumOrderAmount,
|
baseDeliveryFee: normalizeMoney(source?.baseDeliveryFee ?? 0, 0),
|
||||||
DEFAULT_FEES_SETTINGS.minimumOrderAmount,
|
|
||||||
),
|
|
||||||
baseDeliveryFee: normalizeMoney(
|
|
||||||
source?.baseDeliveryFee ?? DEFAULT_FEES_SETTINGS.baseDeliveryFee,
|
|
||||||
DEFAULT_FEES_SETTINGS.baseDeliveryFee,
|
|
||||||
),
|
|
||||||
freeDeliveryThreshold:
|
freeDeliveryThreshold:
|
||||||
source?.freeDeliveryThreshold === null ||
|
source?.freeDeliveryThreshold === null ||
|
||||||
source?.freeDeliveryThreshold === undefined
|
source?.freeDeliveryThreshold === undefined
|
||||||
? null
|
? null
|
||||||
: normalizeMoney(
|
: normalizeMoney(source.freeDeliveryThreshold, 0),
|
||||||
source.freeDeliveryThreshold,
|
packagingFeeMode,
|
||||||
DEFAULT_FEES_SETTINGS.freeDeliveryThreshold ?? 0,
|
orderPackagingFeeMode,
|
||||||
),
|
fixedPackagingFee: normalizeMoney(source?.fixedPackagingFee ?? 0, 0),
|
||||||
packagingFeeMode:
|
packagingFeeTiers: sortTiers(cloneTiers(source?.packagingFeeTiers ?? [])),
|
||||||
source?.packagingFeeMode === 'item' ||
|
otherFees: source?.otherFees
|
||||||
source?.packagingFeeMode === 'order'
|
? otherFees
|
||||||
? source.packagingFeeMode
|
: cloneOtherFees(EMPTY_OTHER_FEES),
|
||||||
: DEFAULT_FEES_SETTINGS.packagingFeeMode,
|
|
||||||
orderPackagingFeeMode:
|
|
||||||
source?.orderPackagingFeeMode === 'fixed' ||
|
|
||||||
source?.orderPackagingFeeMode === 'tiered'
|
|
||||||
? source.orderPackagingFeeMode
|
|
||||||
: DEFAULT_FEES_SETTINGS.orderPackagingFeeMode,
|
|
||||||
fixedPackagingFee: normalizeMoney(
|
|
||||||
source?.fixedPackagingFee ?? DEFAULT_FEES_SETTINGS.fixedPackagingFee,
|
|
||||||
DEFAULT_FEES_SETTINGS.fixedPackagingFee,
|
|
||||||
),
|
|
||||||
packagingFeeTiers: sortTiers(
|
|
||||||
cloneTiers(
|
|
||||||
source?.packagingFeeTiers?.length
|
|
||||||
? source.packagingFeeTiers
|
|
||||||
: DEFAULT_PACKAGING_TIERS,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
otherFees: cloneOtherFees(
|
|
||||||
source?.otherFees ?? DEFAULT_FEES_SETTINGS.otherFees,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 按当前门店构建保存参数。 */
|
/** 按当前门店构建保存参数。 */
|
||||||
function buildSavePayload(storeId: string): StoreFeesSettingsDto {
|
function buildSavePayload(storeId: string): SaveStoreFeesSettingsParams {
|
||||||
return {
|
return {
|
||||||
storeId,
|
storeId,
|
||||||
minimumOrderAmount: options.form.minimumOrderAmount,
|
minimumOrderAmount: options.form.minimumOrderAmount,
|
||||||
@@ -144,12 +147,25 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
|||||||
const result = await getStoreFeesSettingsApi(storeId);
|
const result = await getStoreFeesSettingsApi(storeId);
|
||||||
if (options.selectedStoreId.value !== currentStoreId) return;
|
if (options.selectedStoreId.value !== currentStoreId) return;
|
||||||
|
|
||||||
|
if (!result.isConfigured) {
|
||||||
|
options.clearSettings();
|
||||||
|
options.isConfigured.value = false;
|
||||||
|
options.loadedStoreId.value = currentStoreId;
|
||||||
|
options.snapshot.value = buildCurrentSnapshot();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
syncForm(normalizeSettings(result));
|
syncForm(normalizeSettings(result));
|
||||||
|
options.isConfigured.value = true;
|
||||||
|
options.loadedStoreId.value = currentStoreId;
|
||||||
options.snapshot.value = buildCurrentSnapshot();
|
options.snapshot.value = buildCurrentSnapshot();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
applyDefaultSettings();
|
options.isConfigured.value = false;
|
||||||
options.snapshot.value = buildCurrentSnapshot();
|
options.loadedStoreId.value = '';
|
||||||
|
options.snapshot.value = null;
|
||||||
|
options.clearSettings();
|
||||||
|
message.error('加载费用设置失败,请稍后重试');
|
||||||
} finally {
|
} finally {
|
||||||
options.isPageLoading.value = false;
|
options.isPageLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -171,8 +187,9 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
|||||||
|
|
||||||
if (options.stores.value.length === 0) {
|
if (options.stores.value.length === 0) {
|
||||||
options.selectedStoreId.value = '';
|
options.selectedStoreId.value = '';
|
||||||
|
options.isConfigured.value = false;
|
||||||
options.snapshot.value = null;
|
options.snapshot.value = null;
|
||||||
applyDefaultSettings();
|
options.clearSettings();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,10 +206,12 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
message.error('加载门店失败,请稍后重试');
|
||||||
options.stores.value = [];
|
options.stores.value = [];
|
||||||
options.selectedStoreId.value = '';
|
options.selectedStoreId.value = '';
|
||||||
|
options.isConfigured.value = false;
|
||||||
options.snapshot.value = null;
|
options.snapshot.value = null;
|
||||||
applyDefaultSettings();
|
options.clearSettings();
|
||||||
} finally {
|
} finally {
|
||||||
options.isStoreLoading.value = false;
|
options.isStoreLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -205,6 +224,7 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
|||||||
const payload = buildSavePayload(options.selectedStoreId.value);
|
const payload = buildSavePayload(options.selectedStoreId.value);
|
||||||
const result = await saveStoreFeesSettingsApi(payload);
|
const result = await saveStoreFeesSettingsApi(payload);
|
||||||
syncForm(normalizeSettings(result ?? payload));
|
syncForm(normalizeSettings(result ?? payload));
|
||||||
|
options.isConfigured.value = true;
|
||||||
options.snapshot.value = buildCurrentSnapshot();
|
options.snapshot.value = buildCurrentSnapshot();
|
||||||
message.success(successText);
|
message.success(successText);
|
||||||
return true;
|
return true;
|
||||||
@@ -218,7 +238,7 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
|||||||
/** 重置到最近一次快照。 */
|
/** 重置到最近一次快照。 */
|
||||||
function resetFromSnapshot() {
|
function resetFromSnapshot() {
|
||||||
if (!options.snapshot.value) {
|
if (!options.snapshot.value) {
|
||||||
applyDefaultSettings();
|
message.warning('暂无可恢复的已保存配置');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
syncForm(options.snapshot.value);
|
syncForm(options.snapshot.value);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import type {
|
|||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
import { DEFAULT_PACKAGING_TIERS } from './constants';
|
|
||||||
import {
|
import {
|
||||||
cloneTiers,
|
cloneTiers,
|
||||||
createTierId,
|
createTierId,
|
||||||
@@ -51,7 +50,7 @@ export function createPackagingActions(options: CreatePackagingActionsOptions) {
|
|||||||
options.tierForm.id = '';
|
options.tierForm.id = '';
|
||||||
options.tierForm.minAmount = defaultMin;
|
options.tierForm.minAmount = defaultMin;
|
||||||
options.tierForm.maxAmount = null;
|
options.tierForm.maxAmount = null;
|
||||||
options.tierForm.fee = normalizeMoney(lastTier?.fee ?? 2, 2);
|
options.tierForm.fee = normalizeMoney(lastTier?.fee ?? 0, 0);
|
||||||
options.isTierDrawerOpen.value = true;
|
options.isTierDrawerOpen.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,9 +95,6 @@ export function createPackagingActions(options: CreatePackagingActionsOptions) {
|
|||||||
/** 切换是否启用阶梯包装费。 */
|
/** 切换是否启用阶梯包装费。 */
|
||||||
function toggleTiered(checked: boolean) {
|
function toggleTiered(checked: boolean) {
|
||||||
options.form.orderPackagingFeeMode = checked ? 'tiered' : 'fixed';
|
options.form.orderPackagingFeeMode = checked ? 'tiered' : 'fixed';
|
||||||
if (checked && options.form.packagingFeeTiers.length === 0) {
|
|
||||||
options.form.packagingFeeTiers = cloneTiers(DEFAULT_PACKAGING_TIERS);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 删除阶梯。 */
|
/** 删除阶梯。 */
|
||||||
|
|||||||
@@ -15,12 +15,9 @@ 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 { message, Modal } from 'ant-design-vue';
|
||||||
|
|
||||||
import {
|
import { PACKAGING_MODE_OPTIONS } from './fees-page/constants';
|
||||||
DEFAULT_FEES_SETTINGS,
|
|
||||||
PACKAGING_MODE_OPTIONS,
|
|
||||||
} from './fees-page/constants';
|
|
||||||
import { createCopyActions } from './fees-page/copy-actions';
|
import { createCopyActions } from './fees-page/copy-actions';
|
||||||
import { createDataActions } from './fees-page/data-actions';
|
import { createDataActions } from './fees-page/data-actions';
|
||||||
import {
|
import {
|
||||||
@@ -33,6 +30,28 @@ import {
|
|||||||
} from './fees-page/helpers';
|
} from './fees-page/helpers';
|
||||||
import { createPackagingActions } from './fees-page/packaging-actions';
|
import { createPackagingActions } from './fees-page/packaging-actions';
|
||||||
|
|
||||||
|
const EMPTY_FEES_SETTINGS: StoreFeesFormState = {
|
||||||
|
minimumOrderAmount: 0,
|
||||||
|
baseDeliveryFee: 0,
|
||||||
|
freeDeliveryThreshold: null,
|
||||||
|
packagingFeeMode: 'order',
|
||||||
|
orderPackagingFeeMode: 'fixed',
|
||||||
|
fixedPackagingFee: 0,
|
||||||
|
packagingFeeTiers: [],
|
||||||
|
otherFees: {
|
||||||
|
cutlery: {
|
||||||
|
enabled: false,
|
||||||
|
amount: 0,
|
||||||
|
},
|
||||||
|
rush: {
|
||||||
|
enabled: false,
|
||||||
|
amount: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const PACKAGING_MODE_SWITCH_CONFIRM_KEY =
|
||||||
|
'store-fees-packaging-mode-switch-confirmed';
|
||||||
|
|
||||||
export function useStoreFeesPage() {
|
export function useStoreFeesPage() {
|
||||||
// 1. 页面 loading / submitting 状态。
|
// 1. 页面 loading / submitting 状态。
|
||||||
const isStoreLoading = ref(false);
|
const isStoreLoading = ref(false);
|
||||||
@@ -41,13 +60,13 @@ export function useStoreFeesPage() {
|
|||||||
const isSavingPackaging = ref(false);
|
const isSavingPackaging = ref(false);
|
||||||
const isSavingOther = ref(false);
|
const isSavingOther = ref(false);
|
||||||
const isCopySubmitting = ref(false);
|
const isCopySubmitting = ref(false);
|
||||||
|
const isConfigured = ref(false);
|
||||||
|
|
||||||
// 2. 页面核心业务数据。
|
// 2. 页面核心业务数据。
|
||||||
const stores = ref<StoreListItemDto[]>([]);
|
const stores = ref<StoreListItemDto[]>([]);
|
||||||
const selectedStoreId = ref('');
|
const selectedStoreId = ref('');
|
||||||
const form = reactive<StoreFeesFormState>(
|
const loadedStoreId = ref('');
|
||||||
cloneFeesForm(DEFAULT_FEES_SETTINGS),
|
const form = reactive<StoreFeesFormState>(cloneFeesForm(EMPTY_FEES_SETTINGS));
|
||||||
);
|
|
||||||
const snapshot = ref<null | StoreFeesSettingsSnapshot>(null);
|
const snapshot = ref<null | StoreFeesSettingsSnapshot>(null);
|
||||||
|
|
||||||
// 3. 复制弹窗状态。
|
// 3. 复制弹窗状态。
|
||||||
@@ -61,7 +80,7 @@ export function useStoreFeesPage() {
|
|||||||
id: '',
|
id: '',
|
||||||
minAmount: 0,
|
minAmount: 0,
|
||||||
maxAmount: null,
|
maxAmount: null,
|
||||||
fee: 2,
|
fee: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5. 页面衍生视图数据。
|
// 5. 页面衍生视图数据。
|
||||||
@@ -74,6 +93,19 @@ export function useStoreFeesPage() {
|
|||||||
stores.value.find((store) => store.id === selectedStoreId.value)?.name ??
|
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 &&
|
||||||
|
!isSavingDelivery.value &&
|
||||||
|
!isSavingPackaging.value &&
|
||||||
|
!isSavingOther.value,
|
||||||
|
);
|
||||||
|
|
||||||
const copyCandidates = computed(() =>
|
const copyCandidates = computed(() =>
|
||||||
stores.value.filter((store) => store.id !== selectedStoreId.value),
|
stores.value.filter((store) => store.id !== selectedStoreId.value),
|
||||||
@@ -97,6 +129,37 @@ export function useStoreFeesPage() {
|
|||||||
|
|
||||||
const isOrderMode = computed(() => form.packagingFeeMode === 'order');
|
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 '切换后订单包装费将汇总商品维度配置,本页固定/阶梯包装费将暂不生效。';
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSettings() {
|
||||||
|
loadedStoreId.value = '';
|
||||||
|
Object.assign(form, cloneFeesForm(EMPTY_FEES_SETTINGS));
|
||||||
|
isTierDrawerOpen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
// 6. 动作装配。
|
// 6. 动作装配。
|
||||||
const {
|
const {
|
||||||
loadStoreSettings,
|
loadStoreSettings,
|
||||||
@@ -104,9 +167,12 @@ export function useStoreFeesPage() {
|
|||||||
resetFromSnapshot,
|
resetFromSnapshot,
|
||||||
saveCurrentSettings,
|
saveCurrentSettings,
|
||||||
} = createDataActions({
|
} = createDataActions({
|
||||||
|
clearSettings,
|
||||||
form,
|
form,
|
||||||
|
isConfigured,
|
||||||
isPageLoading,
|
isPageLoading,
|
||||||
isStoreLoading,
|
isStoreLoading,
|
||||||
|
loadedStoreId,
|
||||||
selectedStoreId,
|
selectedStoreId,
|
||||||
snapshot,
|
snapshot,
|
||||||
stores,
|
stores,
|
||||||
@@ -169,7 +235,25 @@ export function useStoreFeesPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setPackagingMode(value: PackagingFeeMode) {
|
function setPackagingMode(value: PackagingFeeMode) {
|
||||||
setPackagingFeeMode(value);
|
if (!canOperate.value) return;
|
||||||
|
if (value === form.packagingFeeMode) return;
|
||||||
|
|
||||||
|
const applyMode = () => setPackagingFeeMode(value);
|
||||||
|
if (hasConfirmedPackagingModeSwitch()) {
|
||||||
|
applyMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认切换包装费收取方式?',
|
||||||
|
content: getPackagingModeConfirmContent(value),
|
||||||
|
okText: '确认切换',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk() {
|
||||||
|
applyMode();
|
||||||
|
markPackagingModeSwitchConfirmed();
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCutleryEnabled(value: boolean) {
|
function setCutleryEnabled(value: boolean) {
|
||||||
@@ -196,7 +280,11 @@ export function useStoreFeesPage() {
|
|||||||
|
|
||||||
/** 重置“起送与配送费”分区。 */
|
/** 重置“起送与配送费”分区。 */
|
||||||
function resetDeliverySection() {
|
function resetDeliverySection() {
|
||||||
const source = snapshot.value ?? cloneFeesForm(DEFAULT_FEES_SETTINGS);
|
if (!snapshot.value) {
|
||||||
|
message.warning('暂无可恢复的已保存配置');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const source = snapshot.value;
|
||||||
form.minimumOrderAmount = source.minimumOrderAmount;
|
form.minimumOrderAmount = source.minimumOrderAmount;
|
||||||
form.baseDeliveryFee = source.baseDeliveryFee;
|
form.baseDeliveryFee = source.baseDeliveryFee;
|
||||||
form.freeDeliveryThreshold = source.freeDeliveryThreshold;
|
form.freeDeliveryThreshold = source.freeDeliveryThreshold;
|
||||||
@@ -205,7 +293,11 @@ export function useStoreFeesPage() {
|
|||||||
|
|
||||||
/** 重置“包装费设置”分区。 */
|
/** 重置“包装费设置”分区。 */
|
||||||
function resetPackagingSection() {
|
function resetPackagingSection() {
|
||||||
const source = snapshot.value ?? cloneFeesForm(DEFAULT_FEES_SETTINGS);
|
if (!snapshot.value) {
|
||||||
|
message.warning('暂无可恢复的已保存配置');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const source = snapshot.value;
|
||||||
form.packagingFeeMode = source.packagingFeeMode;
|
form.packagingFeeMode = source.packagingFeeMode;
|
||||||
form.orderPackagingFeeMode = source.orderPackagingFeeMode;
|
form.orderPackagingFeeMode = source.orderPackagingFeeMode;
|
||||||
form.fixedPackagingFee = source.fixedPackagingFee;
|
form.fixedPackagingFee = source.fixedPackagingFee;
|
||||||
@@ -215,14 +307,18 @@ export function useStoreFeesPage() {
|
|||||||
|
|
||||||
/** 重置“其他费用”分区。 */
|
/** 重置“其他费用”分区。 */
|
||||||
function resetOtherSection() {
|
function resetOtherSection() {
|
||||||
const source = snapshot.value ?? cloneFeesForm(DEFAULT_FEES_SETTINGS);
|
if (!snapshot.value) {
|
||||||
|
message.warning('暂无可恢复的已保存配置');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const source = snapshot.value;
|
||||||
form.otherFees = cloneOtherFees(source.otherFees);
|
form.otherFees = cloneOtherFees(source.otherFees);
|
||||||
message.success('已重置其他费用');
|
message.success('已重置其他费用');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 保存“起送与配送费”分区。 */
|
/** 保存“起送与配送费”分区。 */
|
||||||
async function saveDeliverySection() {
|
async function saveDeliverySection() {
|
||||||
if (!selectedStoreId.value) return;
|
if (!canOperate.value) return;
|
||||||
isSavingDelivery.value = true;
|
isSavingDelivery.value = true;
|
||||||
try {
|
try {
|
||||||
await saveCurrentSettings('起送与配送费已保存');
|
await saveCurrentSettings('起送与配送费已保存');
|
||||||
@@ -233,7 +329,7 @@ export function useStoreFeesPage() {
|
|||||||
|
|
||||||
/** 保存“包装费设置”分区。 */
|
/** 保存“包装费设置”分区。 */
|
||||||
async function savePackagingSection() {
|
async function savePackagingSection() {
|
||||||
if (!selectedStoreId.value) return;
|
if (!canOperate.value) return;
|
||||||
if (!validateCurrentPackaging()) return;
|
if (!validateCurrentPackaging()) return;
|
||||||
isSavingPackaging.value = true;
|
isSavingPackaging.value = true;
|
||||||
try {
|
try {
|
||||||
@@ -245,7 +341,7 @@ export function useStoreFeesPage() {
|
|||||||
|
|
||||||
/** 保存“其他费用”分区。 */
|
/** 保存“其他费用”分区。 */
|
||||||
async function saveOtherSection() {
|
async function saveOtherSection() {
|
||||||
if (!selectedStoreId.value) return;
|
if (!canOperate.value) return;
|
||||||
isSavingOther.value = true;
|
isSavingOther.value = true;
|
||||||
try {
|
try {
|
||||||
await saveCurrentSettings('其他费用已保存');
|
await saveCurrentSettings('其他费用已保存');
|
||||||
@@ -274,11 +370,14 @@ export function useStoreFeesPage() {
|
|||||||
/** 切换门店时同步拉取配置。 */
|
/** 切换门店时同步拉取配置。 */
|
||||||
watch(selectedStoreId, async (storeId) => {
|
watch(selectedStoreId, async (storeId) => {
|
||||||
if (!storeId) {
|
if (!storeId) {
|
||||||
Object.assign(form, cloneFeesForm(DEFAULT_FEES_SETTINGS));
|
clearSettings();
|
||||||
|
isConfigured.value = false;
|
||||||
snapshot.value = null;
|
snapshot.value = null;
|
||||||
isTierDrawerOpen.value = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
loadedStoreId.value = '';
|
||||||
|
isConfigured.value = false;
|
||||||
|
isTierDrawerOpen.value = false;
|
||||||
await loadStoreSettings(storeId);
|
await loadStoreSettings(storeId);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -291,6 +390,7 @@ export function useStoreFeesPage() {
|
|||||||
PACKAGING_MODE_OPTIONS,
|
PACKAGING_MODE_OPTIONS,
|
||||||
copyCandidates,
|
copyCandidates,
|
||||||
copyTargetStoreIds,
|
copyTargetStoreIds,
|
||||||
|
canOperate,
|
||||||
form,
|
form,
|
||||||
formatCurrency,
|
formatCurrency,
|
||||||
formatTierRange,
|
formatTierRange,
|
||||||
@@ -301,6 +401,7 @@ export function useStoreFeesPage() {
|
|||||||
isCopyIndeterminate,
|
isCopyIndeterminate,
|
||||||
isCopyModalOpen,
|
isCopyModalOpen,
|
||||||
isCopySubmitting,
|
isCopySubmitting,
|
||||||
|
isConfigured,
|
||||||
isOrderMode,
|
isOrderMode,
|
||||||
isPageLoading,
|
isPageLoading,
|
||||||
isSavingDelivery,
|
isSavingDelivery,
|
||||||
@@ -308,6 +409,7 @@ export function useStoreFeesPage() {
|
|||||||
isSavingPackaging,
|
isSavingPackaging,
|
||||||
isStoreLoading,
|
isStoreLoading,
|
||||||
isTierDrawerOpen,
|
isTierDrawerOpen,
|
||||||
|
loadedStoreId,
|
||||||
onDeleteTier,
|
onDeleteTier,
|
||||||
openCopyModal,
|
openCopyModal,
|
||||||
openTierDrawer,
|
openTierDrawer,
|
||||||
@@ -318,6 +420,8 @@ export function useStoreFeesPage() {
|
|||||||
saveDeliverySection,
|
saveDeliverySection,
|
||||||
saveOtherSection,
|
saveOtherSection,
|
||||||
savePackagingSection,
|
savePackagingSection,
|
||||||
|
hasLoadedStoreSettings,
|
||||||
|
hasSelectedStore,
|
||||||
selectedStoreId,
|
selectedStoreId,
|
||||||
selectedStoreName,
|
selectedStoreName,
|
||||||
setBaseDeliveryFee,
|
setBaseDeliveryFee,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import FeesTierDrawer from './components/FeesTierDrawer.vue';
|
|||||||
import { useStoreFeesPage } from './composables/useStoreFeesPage';
|
import { useStoreFeesPage } from './composables/useStoreFeesPage';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
canOperate,
|
||||||
copyCandidates,
|
copyCandidates,
|
||||||
copyTargetStoreIds,
|
copyTargetStoreIds,
|
||||||
form,
|
form,
|
||||||
@@ -27,10 +28,12 @@ const {
|
|||||||
handleCopyCheckAll,
|
handleCopyCheckAll,
|
||||||
handleCopySubmit,
|
handleCopySubmit,
|
||||||
handleSubmitTier,
|
handleSubmitTier,
|
||||||
|
hasLoadedStoreSettings,
|
||||||
isCopyAllChecked,
|
isCopyAllChecked,
|
||||||
isCopyIndeterminate,
|
isCopyIndeterminate,
|
||||||
isCopyModalOpen,
|
isCopyModalOpen,
|
||||||
isCopySubmitting,
|
isCopySubmitting,
|
||||||
|
isConfigured,
|
||||||
isPageLoading,
|
isPageLoading,
|
||||||
isSavingDelivery,
|
isSavingDelivery,
|
||||||
isSavingOther,
|
isSavingOther,
|
||||||
@@ -81,7 +84,9 @@ function onEditTier(tier: PackagingFeeTierDto) {
|
|||||||
:selected-store-id="selectedStoreId"
|
:selected-store-id="selectedStoreId"
|
||||||
:store-options="storeOptions"
|
:store-options="storeOptions"
|
||||||
:is-store-loading="isStoreLoading"
|
:is-store-loading="isStoreLoading"
|
||||||
:copy-disabled="!selectedStoreId || copyCandidates.length === 0"
|
:copy-disabled="
|
||||||
|
!canOperate || !isConfigured || copyCandidates.length === 0
|
||||||
|
"
|
||||||
copy-button-text="复制费用设置到其他门店"
|
copy-button-text="复制费用设置到其他门店"
|
||||||
@update:selected-store-id="setSelectedStoreId"
|
@update:selected-store-id="setSelectedStoreId"
|
||||||
@copy="openCopyModal"
|
@copy="openCopyModal"
|
||||||
@@ -95,7 +100,14 @@ function onEditTier(tier: PackagingFeeTierDto) {
|
|||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<Spin :spinning="isPageLoading">
|
<Spin :spinning="isPageLoading">
|
||||||
|
<Card v-if="hasLoadedStoreSettings && !isConfigured" :bordered="false">
|
||||||
|
<Empty
|
||||||
|
description="当前门店尚未配置费用规则。请先填写并保存,之后可执行复制。"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<FeesDeliveryCard
|
<FeesDeliveryCard
|
||||||
|
:can-operate="canOperate"
|
||||||
:minimum-order-amount="form.minimumOrderAmount"
|
:minimum-order-amount="form.minimumOrderAmount"
|
||||||
:base-delivery-fee="form.baseDeliveryFee"
|
:base-delivery-fee="form.baseDeliveryFee"
|
||||||
:free-delivery-threshold="form.freeDeliveryThreshold"
|
:free-delivery-threshold="form.freeDeliveryThreshold"
|
||||||
@@ -108,6 +120,7 @@ function onEditTier(tier: PackagingFeeTierDto) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<FeesPackagingCard
|
<FeesPackagingCard
|
||||||
|
:can-operate="canOperate"
|
||||||
:packaging-mode="form.packagingFeeMode"
|
:packaging-mode="form.packagingFeeMode"
|
||||||
:tiered-enabled="form.orderPackagingFeeMode === 'tiered'"
|
:tiered-enabled="form.orderPackagingFeeMode === 'tiered'"
|
||||||
:fixed-packaging-fee="form.fixedPackagingFee"
|
:fixed-packaging-fee="form.fixedPackagingFee"
|
||||||
@@ -126,6 +139,7 @@ function onEditTier(tier: PackagingFeeTierDto) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<FeesOtherCard
|
<FeesOtherCard
|
||||||
|
:can-operate="canOperate"
|
||||||
:cutlery-enabled="form.otherFees.cutlery.enabled"
|
:cutlery-enabled="form.otherFees.cutlery.enabled"
|
||||||
:cutlery-amount="form.otherFees.cutlery.amount"
|
:cutlery-amount="form.otherFees.cutlery.amount"
|
||||||
:rush-enabled="form.otherFees.rush.enabled"
|
:rush-enabled="form.otherFees.rush.enabled"
|
||||||
@@ -142,6 +156,7 @@ function onEditTier(tier: PackagingFeeTierDto) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<FeesTierDrawer
|
<FeesTierDrawer
|
||||||
|
:can-operate="canOperate"
|
||||||
:open="isTierDrawerOpen"
|
:open="isTierDrawerOpen"
|
||||||
:title="tierDrawerTitle"
|
:title="tierDrawerTitle"
|
||||||
:form="tierForm"
|
:form="tierForm"
|
||||||
|
|||||||
@@ -1,30 +1,58 @@
|
|||||||
/* 文件职责:包装费卡片样式。 */
|
/* 文件职责:包装费卡片样式。 */
|
||||||
.page-store-fees {
|
.page-store-fees {
|
||||||
.packaging-mode-switch {
|
.packaging-mode-toggle-row {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
gap: 2px;
|
gap: 10px;
|
||||||
padding: 3px;
|
align-items: center;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 12px;
|
||||||
background: #f8f9fb;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-switch-item {
|
.mode-toggle-label {
|
||||||
padding: 6px 18px;
|
padding: 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #4b5563;
|
color: #4b5563;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
transition: color 0.2s ease;
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-switch-item.active {
|
.mode-toggle-label.active {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1677ff;
|
color: #1677ff;
|
||||||
background: #fff;
|
}
|
||||||
box-shadow: 0 1px 3px rgb(15 23 42 / 10%);
|
|
||||||
|
.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;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.packaging-mode-guide .guide-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #4b5563;
|
||||||
}
|
}
|
||||||
|
|
||||||
.packaging-tier-block {
|
.packaging-tier-block {
|
||||||
|
|||||||
@@ -23,13 +23,13 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.packaging-mode-switch {
|
.packaging-mode-toggle-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-switch-item {
|
.mode-toggle-label {
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,16 +43,16 @@ export const FINE_INTERVAL_OPTIONS: Array<{ label: string; value: number }> = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const DEFAULT_PICKUP_BASIC_SETTINGS: PickupBasicSettingsDto = {
|
export const DEFAULT_PICKUP_BASIC_SETTINGS: PickupBasicSettingsDto = {
|
||||||
allowSameDayPickup: true,
|
allowSameDayPickup: false,
|
||||||
bookingDays: 3,
|
bookingDays: 1,
|
||||||
maxItemsPerOrder: 20,
|
maxItemsPerOrder: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_FINE_RULE: PickupFineRuleDto = {
|
export const DEFAULT_FINE_RULE: PickupFineRuleDto = {
|
||||||
intervalMinutes: 30,
|
intervalMinutes: 30,
|
||||||
slotCapacity: 5,
|
slotCapacity: 1,
|
||||||
dayStartTime: '09:00',
|
dayStartTime: '',
|
||||||
dayEndTime: '20:30',
|
dayEndTime: '',
|
||||||
minAdvanceHours: 2,
|
minAdvanceHours: 0,
|
||||||
dayOfWeeks: [...ALL_WEEK_DAYS],
|
dayOfWeeks: [],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,12 +42,15 @@ import {
|
|||||||
interface CreateDataActionsOptions {
|
interface CreateDataActionsOptions {
|
||||||
basicSettings: PickupBasicSettingsDto;
|
basicSettings: PickupBasicSettingsDto;
|
||||||
bigSlots: Ref<PickupSlotDto[]>;
|
bigSlots: Ref<PickupSlotDto[]>;
|
||||||
|
clearSettings: () => void;
|
||||||
fineRule: PickupFineRuleDto;
|
fineRule: PickupFineRuleDto;
|
||||||
|
isConfigured: Ref<boolean>;
|
||||||
isPageLoading: Ref<boolean>;
|
isPageLoading: Ref<boolean>;
|
||||||
isSavingBasic: Ref<boolean>;
|
isSavingBasic: Ref<boolean>;
|
||||||
isSavingFineRule: Ref<boolean>;
|
isSavingFineRule: Ref<boolean>;
|
||||||
isSavingSlots: Ref<boolean>;
|
isSavingSlots: Ref<boolean>;
|
||||||
isStoreLoading: Ref<boolean>;
|
isStoreLoading: Ref<boolean>;
|
||||||
|
loadedStoreId: Ref<string>;
|
||||||
mode: Ref<PickupMode>;
|
mode: Ref<PickupMode>;
|
||||||
previewDays: Ref<PickupPreviewDayDto[]>;
|
previewDays: Ref<PickupPreviewDayDto[]>;
|
||||||
selectedStoreId: Ref<string>;
|
selectedStoreId: Ref<string>;
|
||||||
@@ -84,15 +87,6 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 应用默认配置(接口异常兜底)。 */
|
|
||||||
function applyDefaultSettings() {
|
|
||||||
options.mode.value = DEFAULT_PICKUP_MODE;
|
|
||||||
syncBasicSettings(cloneBasicSettings(DEFAULT_PICKUP_BASIC_SETTINGS));
|
|
||||||
options.bigSlots.value = [];
|
|
||||||
syncFineRule(cloneFineRule(DEFAULT_FINE_RULE));
|
|
||||||
options.previewDays.value = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 应用快照到当前页面状态。 */
|
/** 应用快照到当前页面状态。 */
|
||||||
function applySnapshot(snapshot: PickupSettingsSnapshot) {
|
function applySnapshot(snapshot: PickupSettingsSnapshot) {
|
||||||
options.mode.value = snapshot.mode;
|
options.mode.value = snapshot.mode;
|
||||||
@@ -110,28 +104,43 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
|||||||
const result = await getStorePickupSettingsApi(storeId);
|
const result = await getStorePickupSettingsApi(storeId);
|
||||||
if (options.selectedStoreId.value !== currentStoreId) return;
|
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_PICKUP_MODE;
|
options.mode.value = result.mode ?? DEFAULT_PICKUP_MODE;
|
||||||
syncBasicSettings({
|
syncBasicSettings(
|
||||||
...DEFAULT_PICKUP_BASIC_SETTINGS,
|
result.basicSettings
|
||||||
...result.basicSettings,
|
? cloneBasicSettings(result.basicSettings)
|
||||||
});
|
: cloneBasicSettings(DEFAULT_PICKUP_BASIC_SETTINGS),
|
||||||
|
);
|
||||||
options.bigSlots.value = sortSlots(
|
options.bigSlots.value = sortSlots(
|
||||||
result.bigSlots?.length ? result.bigSlots : [],
|
result.bigSlots?.length ? result.bigSlots : [],
|
||||||
);
|
);
|
||||||
syncFineRule({
|
syncFineRule(
|
||||||
...DEFAULT_FINE_RULE,
|
result.fineRule
|
||||||
...result.fineRule,
|
? cloneFineRule(result.fineRule)
|
||||||
});
|
: cloneFineRule(DEFAULT_FINE_RULE),
|
||||||
|
);
|
||||||
options.previewDays.value =
|
options.previewDays.value =
|
||||||
result.previewDays?.length > 0
|
result.previewDays?.length > 0
|
||||||
? clonePreviewDays(result.previewDays)
|
? clonePreviewDays(result.previewDays)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
options.isConfigured.value = true;
|
||||||
|
options.loadedStoreId.value = currentStoreId;
|
||||||
options.snapshot.value = buildCurrentSnapshot();
|
options.snapshot.value = buildCurrentSnapshot();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
applyDefaultSettings();
|
options.clearSettings();
|
||||||
options.snapshot.value = buildCurrentSnapshot();
|
options.isConfigured.value = false;
|
||||||
|
options.loadedStoreId.value = '';
|
||||||
|
options.snapshot.value = null;
|
||||||
|
message.error('加载自提设置失败,请稍后重试');
|
||||||
} finally {
|
} finally {
|
||||||
options.isPageLoading.value = false;
|
options.isPageLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -153,8 +162,10 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
|||||||
|
|
||||||
if (options.stores.value.length === 0) {
|
if (options.stores.value.length === 0) {
|
||||||
options.selectedStoreId.value = '';
|
options.selectedStoreId.value = '';
|
||||||
|
options.loadedStoreId.value = '';
|
||||||
|
options.isConfigured.value = false;
|
||||||
options.snapshot.value = null;
|
options.snapshot.value = null;
|
||||||
applyDefaultSettings();
|
options.clearSettings();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,8 +184,11 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
options.stores.value = [];
|
options.stores.value = [];
|
||||||
options.selectedStoreId.value = '';
|
options.selectedStoreId.value = '';
|
||||||
|
options.loadedStoreId.value = '';
|
||||||
|
options.isConfigured.value = false;
|
||||||
options.snapshot.value = null;
|
options.snapshot.value = null;
|
||||||
applyDefaultSettings();
|
options.clearSettings();
|
||||||
|
message.error('加载门店失败,请稍后重试');
|
||||||
} finally {
|
} finally {
|
||||||
options.isStoreLoading.value = false;
|
options.isStoreLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -243,7 +257,7 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
|||||||
/** 重置到最近一次快照。 */
|
/** 重置到最近一次快照。 */
|
||||||
function resetFromSnapshot() {
|
function resetFromSnapshot() {
|
||||||
if (!options.snapshot.value) {
|
if (!options.snapshot.value) {
|
||||||
applyDefaultSettings();
|
options.clearSettings();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
applySnapshot(options.snapshot.value);
|
applySnapshot(options.snapshot.value);
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ export function useStorePickupPage() {
|
|||||||
const isSavingSlots = ref(false);
|
const isSavingSlots = ref(false);
|
||||||
const isSavingFineRule = ref(false);
|
const isSavingFineRule = ref(false);
|
||||||
const isCopySubmitting = ref(false);
|
const isCopySubmitting = ref(false);
|
||||||
|
const isConfigured = ref(false);
|
||||||
|
const loadedStoreId = ref('');
|
||||||
|
|
||||||
// 2. 页面核心业务数据。
|
// 2. 页面核心业务数据。
|
||||||
const stores = ref<StoreListItemDto[]>([]);
|
const stores = ref<StoreListItemDto[]>([]);
|
||||||
@@ -119,6 +121,34 @@ export function useStorePickupPage() {
|
|||||||
? `编辑时段 - ${slotForm.name}`
|
? `编辑时段 - ${slotForm.name}`
|
||||||
: '添加时段',
|
: '添加时段',
|
||||||
);
|
);
|
||||||
|
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 &&
|
||||||
|
!isPageLoading.value &&
|
||||||
|
!isSavingBasic.value &&
|
||||||
|
!isSavingSlots.value &&
|
||||||
|
!isSavingFineRule.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
function clearSettings() {
|
||||||
|
pickupMode.value = DEFAULT_PICKUP_MODE;
|
||||||
|
basicSettings.allowSameDayPickup =
|
||||||
|
DEFAULT_PICKUP_BASIC_SETTINGS.allowSameDayPickup;
|
||||||
|
basicSettings.bookingDays = DEFAULT_PICKUP_BASIC_SETTINGS.bookingDays;
|
||||||
|
basicSettings.maxItemsPerOrder =
|
||||||
|
DEFAULT_PICKUP_BASIC_SETTINGS.maxItemsPerOrder;
|
||||||
|
bigSlots.value = [];
|
||||||
|
Object.assign(fineRule, cloneFineRule(DEFAULT_FINE_RULE));
|
||||||
|
previewDays.value = [];
|
||||||
|
selectedPreviewDate.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
// 6. 数据域动作装配。
|
// 6. 数据域动作装配。
|
||||||
const {
|
const {
|
||||||
@@ -131,12 +161,15 @@ export function useStorePickupPage() {
|
|||||||
} = createDataActions({
|
} = createDataActions({
|
||||||
basicSettings,
|
basicSettings,
|
||||||
bigSlots,
|
bigSlots,
|
||||||
|
clearSettings,
|
||||||
fineRule,
|
fineRule,
|
||||||
|
isConfigured,
|
||||||
isPageLoading,
|
isPageLoading,
|
||||||
isSavingBasic,
|
isSavingBasic,
|
||||||
isSavingFineRule,
|
isSavingFineRule,
|
||||||
isSavingSlots,
|
isSavingSlots,
|
||||||
isStoreLoading,
|
isStoreLoading,
|
||||||
|
loadedStoreId,
|
||||||
mode: pickupMode,
|
mode: pickupMode,
|
||||||
previewDays,
|
previewDays,
|
||||||
selectedStoreId,
|
selectedStoreId,
|
||||||
@@ -204,6 +237,7 @@ export function useStorePickupPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setPickupMode(value: 'big' | 'fine') {
|
function setPickupMode(value: 'big' | 'fine') {
|
||||||
|
if (!canOperate.value) return;
|
||||||
pickupMode.value = value;
|
pickupMode.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,19 +289,16 @@ export function useStorePickupPage() {
|
|||||||
// 8. 门店切换时自动刷新配置。
|
// 8. 门店切换时自动刷新配置。
|
||||||
watch(selectedStoreId, async (storeId) => {
|
watch(selectedStoreId, async (storeId) => {
|
||||||
if (!storeId) {
|
if (!storeId) {
|
||||||
pickupMode.value = DEFAULT_PICKUP_MODE;
|
loadedStoreId.value = '';
|
||||||
basicSettings.allowSameDayPickup =
|
isConfigured.value = false;
|
||||||
DEFAULT_PICKUP_BASIC_SETTINGS.allowSameDayPickup;
|
clearSettings();
|
||||||
basicSettings.bookingDays = DEFAULT_PICKUP_BASIC_SETTINGS.bookingDays;
|
|
||||||
basicSettings.maxItemsPerOrder =
|
|
||||||
DEFAULT_PICKUP_BASIC_SETTINGS.maxItemsPerOrder;
|
|
||||||
bigSlots.value = [];
|
|
||||||
Object.assign(fineRule, cloneFineRule(DEFAULT_FINE_RULE));
|
|
||||||
previewDays.value = [];
|
|
||||||
selectedPreviewDate.value = previewDays.value[0]?.date ?? '';
|
|
||||||
snapshot.value = null;
|
snapshot.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
loadedStoreId.value = '';
|
||||||
|
isConfigured.value = false;
|
||||||
|
snapshot.value = null;
|
||||||
|
clearSettings();
|
||||||
await loadStoreSettings(storeId);
|
await loadStoreSettings(storeId);
|
||||||
selectedPreviewDate.value = previewDays.value[0]?.date ?? '';
|
selectedPreviewDate.value = previewDays.value[0]?.date ?? '';
|
||||||
});
|
});
|
||||||
@@ -297,6 +328,7 @@ export function useStorePickupPage() {
|
|||||||
WEEKDAY_OPTIONS,
|
WEEKDAY_OPTIONS,
|
||||||
basicSettings,
|
basicSettings,
|
||||||
bigSlots,
|
bigSlots,
|
||||||
|
canOperate,
|
||||||
calcReservedPercent,
|
calcReservedPercent,
|
||||||
copyCandidates,
|
copyCandidates,
|
||||||
copyTargetStoreIds,
|
copyTargetStoreIds,
|
||||||
@@ -312,6 +344,9 @@ export function useStorePickupPage() {
|
|||||||
isCopyIndeterminate,
|
isCopyIndeterminate,
|
||||||
isCopyModalOpen,
|
isCopyModalOpen,
|
||||||
isCopySubmitting,
|
isCopySubmitting,
|
||||||
|
isConfigured,
|
||||||
|
hasLoadedStoreSettings,
|
||||||
|
hasSelectedStore,
|
||||||
isFineDaySelected,
|
isFineDaySelected,
|
||||||
isPageLoading,
|
isPageLoading,
|
||||||
isSavingBasic,
|
isSavingBasic,
|
||||||
@@ -322,6 +357,7 @@ export function useStorePickupPage() {
|
|||||||
isStoreLoading,
|
isStoreLoading,
|
||||||
openCopyModal,
|
openCopyModal,
|
||||||
openSlotDrawer,
|
openSlotDrawer,
|
||||||
|
loadedStoreId,
|
||||||
pickupMode,
|
pickupMode,
|
||||||
previewDays,
|
previewDays,
|
||||||
quickSelectFineDays,
|
quickSelectFineDays,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const {
|
|||||||
WEEKDAY_OPTIONS,
|
WEEKDAY_OPTIONS,
|
||||||
basicSettings,
|
basicSettings,
|
||||||
bigSlots,
|
bigSlots,
|
||||||
|
canOperate,
|
||||||
calcReservedPercent,
|
calcReservedPercent,
|
||||||
copyCandidates,
|
copyCandidates,
|
||||||
copyTargetStoreIds,
|
copyTargetStoreIds,
|
||||||
@@ -39,6 +40,8 @@ const {
|
|||||||
isCopyIndeterminate,
|
isCopyIndeterminate,
|
||||||
isCopyModalOpen,
|
isCopyModalOpen,
|
||||||
isCopySubmitting,
|
isCopySubmitting,
|
||||||
|
isConfigured,
|
||||||
|
hasLoadedStoreSettings,
|
||||||
isFineDaySelected,
|
isFineDaySelected,
|
||||||
isPageLoading,
|
isPageLoading,
|
||||||
isSavingBasic,
|
isSavingBasic,
|
||||||
@@ -92,7 +95,9 @@ const {
|
|||||||
:selected-store-id="selectedStoreId"
|
:selected-store-id="selectedStoreId"
|
||||||
:store-options="storeOptions"
|
:store-options="storeOptions"
|
||||||
:is-store-loading="isStoreLoading"
|
:is-store-loading="isStoreLoading"
|
||||||
:copy-disabled="!selectedStoreId || copyCandidates.length === 0"
|
:copy-disabled="
|
||||||
|
!canOperate || !isConfigured || copyCandidates.length === 0
|
||||||
|
"
|
||||||
@update:selected-store-id="setSelectedStoreId"
|
@update:selected-store-id="setSelectedStoreId"
|
||||||
@copy="openCopyModal"
|
@copy="openCopyModal"
|
||||||
/>
|
/>
|
||||||
@@ -105,6 +110,12 @@ const {
|
|||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<Spin :spinning="isPageLoading">
|
<Spin :spinning="isPageLoading">
|
||||||
|
<Card v-if="hasLoadedStoreSettings && !isConfigured" :bordered="false">
|
||||||
|
<Empty
|
||||||
|
description="当前门店尚未配置自提规则。请先完成配置并保存,之后可执行复制。"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<PickupBasicSettingsCard
|
<PickupBasicSettingsCard
|
||||||
:settings="basicSettings"
|
:settings="basicSettings"
|
||||||
:is-saving="isSavingBasic"
|
:is-saving="isSavingBasic"
|
||||||
|
|||||||
@@ -83,6 +83,30 @@ export function resolveShiftTimeByType(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 按班次类型构建单日排班(总是使用模板时间)。 */
|
||||||
|
export function buildDayShiftByType(payload: {
|
||||||
|
dayOfWeek: number;
|
||||||
|
shiftType: ShiftType;
|
||||||
|
templates: StoreShiftTemplatesDto;
|
||||||
|
}): StaffDayShiftDto {
|
||||||
|
if (payload.shiftType === 'off') {
|
||||||
|
return {
|
||||||
|
dayOfWeek: payload.dayOfWeek,
|
||||||
|
shiftType: 'off',
|
||||||
|
startTime: '',
|
||||||
|
endTime: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = payload.templates[payload.shiftType];
|
||||||
|
return {
|
||||||
|
dayOfWeek: payload.dayOfWeek,
|
||||||
|
shiftType: payload.shiftType,
|
||||||
|
startTime: template.startTime,
|
||||||
|
endTime: template.endTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** 归一化单日排班。 */
|
/** 归一化单日排班。 */
|
||||||
export function normalizeDayShift(payload: {
|
export function normalizeDayShift(payload: {
|
||||||
dayOfWeek: number;
|
dayOfWeek: number;
|
||||||
@@ -171,16 +195,9 @@ export function updateWeekRowShift(payload: {
|
|||||||
row: WeekEditorRow;
|
row: WeekEditorRow;
|
||||||
templates: StoreShiftTemplatesDto;
|
templates: StoreShiftTemplatesDto;
|
||||||
}) {
|
}) {
|
||||||
const currentShift = payload.row.shifts.find(
|
const nextShift = buildDayShiftByType({
|
||||||
(item) => item.dayOfWeek === payload.dayOfWeek,
|
|
||||||
);
|
|
||||||
const nextShift = normalizeDayShift({
|
|
||||||
dayOfWeek: payload.dayOfWeek,
|
|
||||||
shift: {
|
|
||||||
dayOfWeek: payload.dayOfWeek,
|
dayOfWeek: payload.dayOfWeek,
|
||||||
shiftType: payload.nextShiftType,
|
shiftType: payload.nextShiftType,
|
||||||
},
|
|
||||||
fallback: currentShift,
|
|
||||||
templates: payload.templates,
|
templates: payload.templates,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
|
|
||||||
import { SHIFT_CYCLE } from './constants';
|
import { SHIFT_CYCLE } from './constants';
|
||||||
import {
|
import {
|
||||||
|
buildDayShiftByType,
|
||||||
buildWeekEditorRows,
|
buildWeekEditorRows,
|
||||||
cloneScheduleMap,
|
cloneScheduleMap,
|
||||||
cloneShifts,
|
cloneShifts,
|
||||||
@@ -74,16 +75,9 @@ export function createScheduleActions(options: CreateScheduleActionsOptions) {
|
|||||||
|
|
||||||
/** 更新个人排班班次类型。 */
|
/** 更新个人排班班次类型。 */
|
||||||
function setPersonalShiftType(dayOfWeek: number, shiftType: ShiftType) {
|
function setPersonalShiftType(dayOfWeek: number, shiftType: ShiftType) {
|
||||||
const currentShift = options.personalForm.shifts.find(
|
const nextShift = buildDayShiftByType({
|
||||||
(item) => item.dayOfWeek === dayOfWeek,
|
|
||||||
);
|
|
||||||
const nextShift = normalizeDayShift({
|
|
||||||
dayOfWeek,
|
|
||||||
shift: {
|
|
||||||
dayOfWeek,
|
dayOfWeek,
|
||||||
shiftType,
|
shiftType,
|
||||||
},
|
|
||||||
fallback: currentShift,
|
|
||||||
templates: options.templates.value,
|
templates: options.templates.value,
|
||||||
});
|
});
|
||||||
upsertPersonalShift(dayOfWeek, nextShift);
|
upsertPersonalShift(dayOfWeek, nextShift);
|
||||||
|
|||||||
@@ -39,11 +39,19 @@
|
|||||||
color: #667085;
|
color: #667085;
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
th:first-child {
|
||||||
|
width: 140px;
|
||||||
|
padding-left: 16px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.schedule-staff-cell,
|
.schedule-staff-cell,
|
||||||
.staff-week-name-cell {
|
.staff-week-name-cell {
|
||||||
min-width: 96px;
|
width: 140px;
|
||||||
|
min-width: 140px;
|
||||||
|
padding-left: 16px !important;
|
||||||
text-align: left !important;
|
text-align: left !important;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user