feat(@vben/web-antd): split store delivery active and config modes

This commit is contained in:
2026-02-19 08:02:49 +08:00
parent f7854bb925
commit b4a63cb0b8
5 changed files with 134 additions and 13 deletions

View File

@@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
/** /**
* 文件职责:配送模式区块。 * 文件职责:配送模式区块。
* 1. 展示配送模式切换按钮 * 1. 展示当前启用配送模式,并提供独立切换入口
* 2. 展示地图占位与半径/区域两种预览。 * 2. 展示配置编辑视图切换与半径/区域两种预览。
*/ */
import type { DeliveryMode, RadiusTierDto } from '#/api/store-delivery'; import type { DeliveryMode, RadiusTierDto } from '#/api/store-delivery';
@@ -11,7 +11,8 @@ import { computed } from 'vue';
import { Card } from 'ant-design-vue'; import { Card } from 'ant-design-vue';
interface Props { interface Props {
mode: DeliveryMode; activeMode: DeliveryMode;
configMode: DeliveryMode;
modeOptions: Array<{ label: string; value: DeliveryMode }>; modeOptions: Array<{ label: string; value: DeliveryMode }>;
radiusTiers: RadiusTierDto[]; radiusTiers: RadiusTierDto[];
} }
@@ -19,9 +20,17 @@ interface Props {
const props = defineProps<Props>(); const props = defineProps<Props>();
const emit = defineEmits<{ const emit = defineEmits<{
(event: 'changeMode', mode: DeliveryMode): void; (event: 'changeActiveMode', mode: DeliveryMode): void;
(event: 'changeConfigMode', mode: DeliveryMode): void;
}>(); }>();
const activeModeLabel = computed(() => {
return (
props.modeOptions.find((item) => item.value === props.activeMode)?.label ??
'--'
);
});
const radiusLabels = computed(() => { const radiusLabels = computed(() => {
const fallback = ['1km', '3km', '5km']; const fallback = ['1km', '3km', '5km'];
const sorted = props.radiusTiers const sorted = props.radiusTiers
@@ -43,14 +52,36 @@ const radiusLabels = computed(() => {
<span class="section-title">配送模式</span> <span class="section-title">配送模式</span>
</template> </template>
<div class="delivery-active-mode">
<div class="delivery-active-title">当前已启用模式</div>
<div class="delivery-active-value">{{ activeModeLabel }}</div>
<div class="delivery-active-tip">
切换后将影响顾客下单时的配送范围与配送费计算
</div>
<div class="delivery-active-switch">
<button
v-for="item in props.modeOptions"
:key="item.value"
type="button"
class="mode-switch-item"
:class="{ active: props.activeMode === item.value }"
@click="emit('changeActiveMode', item.value)"
>
{{ item.label }}
</button>
</div>
</div>
<div class="delivery-config-title">配送配置编辑视图</div>
<div class="delivery-mode-switch"> <div class="delivery-mode-switch">
<button <button
v-for="item in props.modeOptions" v-for="item in props.modeOptions"
:key="item.value" :key="item.value"
type="button" type="button"
class="mode-switch-item" class="mode-switch-item"
:class="{ active: props.mode === item.value }" :class="{ active: props.configMode === item.value }"
@click="emit('changeMode', item.value)" @click="emit('changeConfigMode', item.value)"
> >
{{ item.label }} {{ item.label }}
</button> </button>
@@ -66,7 +97,7 @@ const radiusLabels = computed(() => {
<span class="map-pin"></span> <span class="map-pin"></span>
<template v-if="props.mode === 'radius'"> <template v-if="props.configMode === 'radius'">
<span class="radius-circle radius-3"> <span class="radius-circle radius-3">
<span class="radius-label">{{ radiusLabels[2] }}</span> <span class="radius-label">{{ radiusLabels[2] }}</span>
</span> </span>

View File

@@ -38,6 +38,7 @@ import {
} from './helpers'; } from './helpers';
interface CreateDataActionsOptions { interface CreateDataActionsOptions {
editingMode: Ref<DeliveryMode>;
generalSettings: DeliveryGeneralSettingsDto; generalSettings: DeliveryGeneralSettingsDto;
isSaving: Ref<boolean>; isSaving: Ref<boolean>;
isSettingsLoading: Ref<boolean>; isSettingsLoading: Ref<boolean>;
@@ -62,6 +63,7 @@ export function createDataActions(options: CreateDataActionsOptions) {
/** 将快照应用到页面状态。 */ /** 将快照应用到页面状态。 */
function applySnapshot(snapshot: DeliverySettingsSnapshot) { function applySnapshot(snapshot: DeliverySettingsSnapshot) {
options.mode.value = snapshot.mode; options.mode.value = snapshot.mode;
options.editingMode.value = snapshot.mode;
options.radiusTiers.value = sortRadiusTiers(snapshot.radiusTiers); options.radiusTiers.value = sortRadiusTiers(snapshot.radiusTiers);
options.polygonZones.value = sortPolygonZones(snapshot.polygonZones); options.polygonZones.value = sortPolygonZones(snapshot.polygonZones);
syncGeneralSettings(snapshot.generalSettings); syncGeneralSettings(snapshot.generalSettings);
@@ -80,6 +82,7 @@ export function createDataActions(options: CreateDataActionsOptions) {
/** 回填默认配置,作为接口异常时的兜底展示。 */ /** 回填默认配置,作为接口异常时的兜底展示。 */
function applyDefaultSettings() { function applyDefaultSettings() {
options.mode.value = DEFAULT_DELIVERY_MODE; options.mode.value = DEFAULT_DELIVERY_MODE;
options.editingMode.value = DEFAULT_DELIVERY_MODE;
options.radiusTiers.value = sortRadiusTiers(DEFAULT_RADIUS_TIERS); options.radiusTiers.value = sortRadiusTiers(DEFAULT_RADIUS_TIERS);
options.polygonZones.value = sortPolygonZones(DEFAULT_POLYGON_ZONES); options.polygonZones.value = sortPolygonZones(DEFAULT_POLYGON_ZONES);
syncGeneralSettings(cloneGeneralSettings(DEFAULT_GENERAL_SETTINGS)); syncGeneralSettings(cloneGeneralSettings(DEFAULT_GENERAL_SETTINGS));
@@ -94,6 +97,7 @@ export function createDataActions(options: CreateDataActionsOptions) {
if (options.selectedStoreId.value !== currentStoreId) return; if (options.selectedStoreId.value !== currentStoreId) return;
options.mode.value = result.mode ?? DEFAULT_DELIVERY_MODE; options.mode.value = result.mode ?? DEFAULT_DELIVERY_MODE;
options.editingMode.value = options.mode.value;
options.radiusTiers.value = sortRadiusTiers( options.radiusTiers.value = sortRadiusTiers(
result.radiusTiers?.length ? result.radiusTiers : DEFAULT_RADIUS_TIERS, result.radiusTiers?.length ? result.radiusTiers : DEFAULT_RADIUS_TIERS,
); );
@@ -166,7 +170,7 @@ export function createDataActions(options: CreateDataActionsOptions) {
/** 保存当前门店配送设置。 */ /** 保存当前门店配送设置。 */
async function saveCurrentSettings() { async function saveCurrentSettings() {
if (!options.selectedStoreId.value) return; if (!options.selectedStoreId.value) return false;
options.isSaving.value = true; options.isSaving.value = true;
try { try {
@@ -179,8 +183,11 @@ export function createDataActions(options: CreateDataActionsOptions) {
}); });
options.snapshot.value = buildCurrentSnapshot(); options.snapshot.value = buildCurrentSnapshot();
message.success('配送设置已保存'); message.success('配送设置已保存');
return true;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
message.error('配送设置保存失败,请稍后重试');
return false;
} finally { } finally {
options.isSaving.value = false; options.isSaving.value = false;
} }

View File

@@ -19,7 +19,9 @@ import type {
RadiusTierDto, RadiusTierDto,
} from '#/api/store-delivery'; } from '#/api/store-delivery';
import { computed, onMounted, reactive, ref, watch } from 'vue'; import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue';
import { Modal } from 'ant-design-vue';
import { import {
DEFAULT_DELIVERY_MODE, DEFAULT_DELIVERY_MODE,
@@ -56,7 +58,10 @@ export function useStoreDeliveryPage() {
// 2. 页面主业务数据。 // 2. 页面主业务数据。
const stores = ref<StoreListItemDto[]>([]); const stores = ref<StoreListItemDto[]>([]);
const selectedStoreId = ref(''); const selectedStoreId = ref('');
// 当前生效配送模式(会落库)。
const deliveryMode = ref<DeliveryMode>(DEFAULT_DELIVERY_MODE); const deliveryMode = ref<DeliveryMode>(DEFAULT_DELIVERY_MODE);
// 当前编辑视图模式(仅影响页面展示,不直接落库)。
const editingMode = ref<DeliveryMode>(DEFAULT_DELIVERY_MODE);
const radiusTiers = ref<RadiusTierDto[]>( const radiusTiers = ref<RadiusTierDto[]>(
cloneRadiusTiers(DEFAULT_RADIUS_TIERS), cloneRadiusTiers(DEFAULT_RADIUS_TIERS),
); );
@@ -123,7 +128,7 @@ export function useStoreDeliveryPage() {
copyTargetStoreIds.value.length < copyCandidates.value.length, copyTargetStoreIds.value.length < copyCandidates.value.length,
); );
const isRadiusMode = computed(() => deliveryMode.value === 'radius'); const isRadiusMode = computed(() => editingMode.value === 'radius');
const isPageLoading = computed(() => isSettingsLoading.value); const isPageLoading = computed(() => isSettingsLoading.value);
const tierDrawerTitle = computed(() => const tierDrawerTitle = computed(() =>
@@ -140,6 +145,7 @@ export function useStoreDeliveryPage() {
resetFromSnapshot, resetFromSnapshot,
saveCurrentSettings, saveCurrentSettings,
} = createDataActions({ } = createDataActions({
editingMode,
generalSettings, generalSettings,
isSaving, isSaving,
isSettingsLoading, isSettingsLoading,
@@ -212,8 +218,34 @@ export function useStoreDeliveryPage() {
selectedStoreId.value = value; selectedStoreId.value = value;
} }
// 切换“配置编辑视图”,不直接修改生效模式。
function setEditingMode(value: DeliveryMode) {
editingMode.value = value;
}
// 切换“当前生效模式”,二次确认后保存,防止误操作。
function setDeliveryMode(value: DeliveryMode) { function setDeliveryMode(value: DeliveryMode) {
if (value === deliveryMode.value) return;
Modal.confirm({
title: '确认切换当前启用的配送模式?',
content:
value === 'radius'
? '切换后将使用按半径配送规则进行计费与可达性判断。'
: '切换后将使用按区域配送(多边形)规则进行计费与可达性判断。',
okText: '确认切换',
cancelText: '取消',
async onOk() {
const previousMode = deliveryMode.value;
const previousEditingMode = editingMode.value;
deliveryMode.value = value; deliveryMode.value = value;
editingMode.value = value;
const saved = await saveCurrentSettings();
if (!saved) {
deliveryMode.value = previousMode;
editingMode.value = previousEditingMode;
}
},
});
} }
function setFreeDeliveryThreshold(value: null | number) { function setFreeDeliveryThreshold(value: null | number) {
@@ -246,6 +278,7 @@ export function useStoreDeliveryPage() {
watch(selectedStoreId, async (storeId) => { watch(selectedStoreId, async (storeId) => {
if (!storeId) { if (!storeId) {
deliveryMode.value = DEFAULT_DELIVERY_MODE; deliveryMode.value = DEFAULT_DELIVERY_MODE;
editingMode.value = DEFAULT_DELIVERY_MODE;
radiusTiers.value = cloneRadiusTiers(DEFAULT_RADIUS_TIERS); radiusTiers.value = cloneRadiusTiers(DEFAULT_RADIUS_TIERS);
polygonZones.value = clonePolygonZones(DEFAULT_POLYGON_ZONES); polygonZones.value = clonePolygonZones(DEFAULT_POLYGON_ZONES);
Object.assign( Object.assign(
@@ -260,6 +293,8 @@ export function useStoreDeliveryPage() {
// 8. 页面首屏初始化。 // 8. 页面首屏初始化。
onMounted(loadStores); onMounted(loadStores);
// 9. 路由回到当前页时刷新门店列表,避免使用旧缓存。
onActivated(loadStores);
return { return {
DELIVERY_MODE_OPTIONS, DELIVERY_MODE_OPTIONS,
@@ -295,6 +330,8 @@ export function useStoreDeliveryPage() {
selectedStoreId, selectedStoreId,
selectedStoreName, selectedStoreName,
setDeliveryMode, setDeliveryMode,
editingMode,
setEditingMode,
setEtaAdjustmentMinutes, setEtaAdjustmentMinutes,
setFreeDeliveryThreshold, setFreeDeliveryThreshold,
setHourlyCapacityLimit, setHourlyCapacityLimit,

View File

@@ -23,6 +23,7 @@ const {
copyCandidates, copyCandidates,
copyTargetStoreIds, copyTargetStoreIds,
deliveryMode, deliveryMode,
editingMode,
formatCurrency, formatCurrency,
formatDistanceRange, formatDistanceRange,
generalSettings, generalSettings,
@@ -52,6 +53,7 @@ const {
selectedStoreId, selectedStoreId,
selectedStoreName, selectedStoreName,
setDeliveryMode, setDeliveryMode,
setEditingMode,
setEtaAdjustmentMinutes, setEtaAdjustmentMinutes,
setFreeDeliveryThreshold, setFreeDeliveryThreshold,
setHourlyCapacityLimit, setHourlyCapacityLimit,
@@ -101,10 +103,12 @@ const {
<template v-else> <template v-else>
<Spin :spinning="isPageLoading"> <Spin :spinning="isPageLoading">
<DeliveryModeCard <DeliveryModeCard
:mode="deliveryMode" :active-mode="deliveryMode"
:config-mode="editingMode"
:mode-options="DELIVERY_MODE_OPTIONS" :mode-options="DELIVERY_MODE_OPTIONS"
:radius-tiers="radiusTiers" :radius-tiers="radiusTiers"
@change-mode="setDeliveryMode" @change-active-mode="setDeliveryMode"
@change-config-mode="setEditingMode"
/> />
<RadiusTierSection <RadiusTierSection

View File

@@ -1,5 +1,47 @@
/* 文件职责:配送模式切换与地图占位样式。 */ /* 文件职责:配送模式切换与地图占位样式。 */
.page-store-delivery { .page-store-delivery {
.delivery-active-mode {
padding: 14px 16px;
margin-bottom: 16px;
background: #f8fbff;
border: 1px solid #d6e4ff;
border-radius: 10px;
}
.delivery-active-title {
margin-bottom: 4px;
font-size: 13px;
color: #475569;
}
.delivery-active-value {
margin-bottom: 6px;
font-size: 16px;
font-weight: 600;
color: #1d4ed8;
}
.delivery-active-tip {
margin-bottom: 10px;
font-size: 12px;
color: #64748b;
}
.delivery-active-switch {
display: flex;
gap: 2px;
width: fit-content;
padding: 3px;
background: #f1f5ff;
border-radius: 8px;
}
.delivery-config-title {
margin-bottom: 8px;
font-size: 13px;
color: #475569;
}
.delivery-mode-switch { .delivery-mode-switch {
display: flex; display: flex;
gap: 2px; gap: 2px;