refactor(project): remove store mock fallback flows and unify real-data states
This commit is contained in:
@@ -8,6 +8,7 @@ import { Button, Card, InputNumber } from 'ant-design-vue';
|
||||
|
||||
interface Props {
|
||||
baseDeliveryFee: number;
|
||||
canOperate: boolean;
|
||||
freeDeliveryThreshold: null | number;
|
||||
isSaving: boolean;
|
||||
minimumOrderAmount: number;
|
||||
@@ -46,6 +47,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
||||
:precision="2"
|
||||
:step="1"
|
||||
:controls="false"
|
||||
:disabled="!props.canOperate"
|
||||
class="fees-input"
|
||||
placeholder="如:15.00"
|
||||
@update:value="
|
||||
@@ -69,6 +71,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
||||
:precision="2"
|
||||
:step="1"
|
||||
:controls="false"
|
||||
:disabled="!props.canOperate"
|
||||
class="fees-input"
|
||||
placeholder="如:3.00"
|
||||
@update:value="
|
||||
@@ -92,6 +95,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
||||
:precision="2"
|
||||
:step="1"
|
||||
:controls="false"
|
||||
:disabled="!props.canOperate"
|
||||
class="fees-input"
|
||||
placeholder="如:30.00"
|
||||
@update:value="
|
||||
@@ -109,8 +113,18 @@ function toNumber(value: null | number | string, fallback = 0) {
|
||||
</div>
|
||||
|
||||
<div class="fees-actions">
|
||||
<Button :disabled="props.isSaving" @click="emit('reset')">重置</Button>
|
||||
<Button type="primary" :loading="props.isSaving" @click="emit('save')">
|
||||
<Button
|
||||
:disabled="props.isSaving || !props.canOperate"
|
||||
@click="emit('reset')"
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
:loading="props.isSaving"
|
||||
:disabled="!props.canOperate"
|
||||
@click="emit('save')"
|
||||
>
|
||||
保存设置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { Button, Card, InputNumber, Switch } from 'ant-design-vue';
|
||||
|
||||
interface Props {
|
||||
canOperate: boolean;
|
||||
cutleryAmount: number;
|
||||
cutleryEnabled: boolean;
|
||||
isSaving: boolean;
|
||||
@@ -41,6 +42,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
||||
<div class="other-fee-row">
|
||||
<Switch
|
||||
:checked="props.cutleryEnabled"
|
||||
:disabled="!props.canOperate"
|
||||
@update:checked="(value) => props.onSetCutleryEnabled(Boolean(value))"
|
||||
/>
|
||||
<div class="other-fee-meta">
|
||||
@@ -57,7 +59,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
||||
:precision="2"
|
||||
:step="0.5"
|
||||
:controls="false"
|
||||
:disabled="!props.cutleryEnabled"
|
||||
:disabled="!props.cutleryEnabled || !props.canOperate"
|
||||
class="other-fee-input"
|
||||
placeholder="如:1.00"
|
||||
@update:value="
|
||||
@@ -71,6 +73,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
||||
<div class="other-fee-row">
|
||||
<Switch
|
||||
:checked="props.rushEnabled"
|
||||
:disabled="!props.canOperate"
|
||||
@update:checked="(value) => props.onSetRushEnabled(Boolean(value))"
|
||||
/>
|
||||
<div class="other-fee-meta">
|
||||
@@ -85,7 +88,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
||||
:precision="2"
|
||||
:step="0.5"
|
||||
:controls="false"
|
||||
:disabled="!props.rushEnabled"
|
||||
:disabled="!props.rushEnabled || !props.canOperate"
|
||||
class="other-fee-input"
|
||||
placeholder="如:3.00"
|
||||
@update:value="
|
||||
@@ -96,8 +99,18 @@ function toNumber(value: null | number | string, fallback = 0) {
|
||||
</div>
|
||||
|
||||
<div class="fees-actions">
|
||||
<Button :disabled="props.isSaving" @click="emit('reset')">重置</Button>
|
||||
<Button type="primary" :loading="props.isSaving" @click="emit('save')">
|
||||
<Button
|
||||
:disabled="props.isSaving || !props.canOperate"
|
||||
@click="emit('reset')"
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
:loading="props.isSaving"
|
||||
:disabled="!props.canOperate"
|
||||
@click="emit('save')"
|
||||
>
|
||||
保存设置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { PackagingFeeTierDto } from '#/api/store-fees';
|
||||
import { Button, Card, InputNumber, Switch } from 'ant-design-vue';
|
||||
|
||||
interface Props {
|
||||
canOperate: boolean;
|
||||
fixedPackagingFee: number;
|
||||
formatCurrency: (value: number) => string;
|
||||
formatTierRange: (tier: PackagingFeeTierDto) => string;
|
||||
@@ -31,11 +32,6 @@ const emit = defineEmits<{
|
||||
(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) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : fallback;
|
||||
@@ -48,17 +44,51 @@ function toNumber(value: null | number | string, fallback = 0) {
|
||||
<span class="section-title">包装费设置</span>
|
||||
</template>
|
||||
|
||||
<div class="packaging-mode-switch">
|
||||
<div class="packaging-mode-toggle-row">
|
||||
<button
|
||||
v-for="mode in packagingModes"
|
||||
:key="mode.value"
|
||||
type="button"
|
||||
class="mode-switch-item"
|
||||
:class="{ active: props.packagingMode === mode.value }"
|
||||
@click="props.onSetPackagingMode(mode.value)"
|
||||
class="mode-toggle-label"
|
||||
:class="{ active: props.packagingMode === 'order' }"
|
||||
:disabled="!props.canOperate"
|
||||
@click="props.onSetPackagingMode('order')"
|
||||
>
|
||||
{{ mode.label }}
|
||||
按订单收取
|
||||
</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>
|
||||
|
||||
<template v-if="props.packagingMode === 'order'">
|
||||
@@ -73,6 +103,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
||||
:precision="2"
|
||||
:step="0.5"
|
||||
:controls="false"
|
||||
:disabled="!props.canOperate"
|
||||
class="fees-input"
|
||||
placeholder="如:2.00"
|
||||
@update:value="
|
||||
@@ -91,6 +122,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
||||
<div class="packaging-tier-toggle-row">
|
||||
<Switch
|
||||
:checked="props.tieredEnabled"
|
||||
:disabled="!props.canOperate"
|
||||
@update:checked="
|
||||
(value) => props.onSetTieredEnabled(Boolean(value))
|
||||
"
|
||||
@@ -117,12 +149,15 @@ function toNumber(value: null | number | string, fallback = 0) {
|
||||
<td>{{ props.formatTierRange(tier) }}</td>
|
||||
<td>{{ props.formatCurrency(tier.fee) }}</td>
|
||||
<td>
|
||||
<a class="fees-table-link" @click="emit('editTier', tier)">
|
||||
<a
|
||||
class="fees-table-link"
|
||||
@click="props.canOperate && emit('editTier', tier)"
|
||||
>
|
||||
编辑
|
||||
</a>
|
||||
<a
|
||||
class="fees-table-link danger"
|
||||
@click="emit('deleteTier', tier)"
|
||||
@click="props.canOperate && emit('deleteTier', tier)"
|
||||
>
|
||||
删除
|
||||
</a>
|
||||
@@ -133,7 +168,9 @@ function toNumber(value: null | number | string, fallback = 0) {
|
||||
</div>
|
||||
|
||||
<div class="packaging-tier-add-row">
|
||||
<Button @click="emit('addTier')">+ 添加阶梯</Button>
|
||||
<Button :disabled="!props.canOperate" @click="emit('addTier')">
|
||||
+ 添加阶梯
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -149,8 +186,18 @@ function toNumber(value: null | number | string, fallback = 0) {
|
||||
</template>
|
||||
|
||||
<div class="fees-actions">
|
||||
<Button :disabled="props.isSaving" @click="emit('reset')">重置</Button>
|
||||
<Button type="primary" :loading="props.isSaving" @click="emit('save')">
|
||||
<Button
|
||||
:disabled="props.isSaving || !props.canOperate"
|
||||
@click="emit('reset')"
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
:loading="props.isSaving"
|
||||
:disabled="!props.canOperate"
|
||||
@click="emit('save')"
|
||||
>
|
||||
保存设置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { PackagingFeeTierFormState } from '#/views/store/fees/types';
|
||||
import { Button, Drawer, InputNumber } from 'ant-design-vue';
|
||||
|
||||
interface Props {
|
||||
canOperate: boolean;
|
||||
form: PackagingFeeTierFormState;
|
||||
isSaving: boolean;
|
||||
onSetFee: (value: number) => void;
|
||||
@@ -49,6 +50,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
||||
:precision="2"
|
||||
:step="1"
|
||||
:controls="false"
|
||||
:disabled="!props.canOperate"
|
||||
class="drawer-input"
|
||||
placeholder="起始金额"
|
||||
@update:value="
|
||||
@@ -63,6 +65,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
||||
:precision="2"
|
||||
:step="1"
|
||||
:controls="false"
|
||||
:disabled="!props.canOperate"
|
||||
class="drawer-input"
|
||||
placeholder="结束金额(留空表示无上限)"
|
||||
@update:value="
|
||||
@@ -90,6 +93,7 @@ function toNumber(value: null | number | string, fallback = 0) {
|
||||
:precision="2"
|
||||
:step="0.5"
|
||||
:controls="false"
|
||||
:disabled="!props.canOperate"
|
||||
class="drawer-input"
|
||||
placeholder="如:2.00"
|
||||
@update:value="
|
||||
@@ -102,12 +106,16 @@ function toNumber(value: null | number | string, fallback = 0) {
|
||||
|
||||
<template #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
|
||||
type="primary"
|
||||
:loading="props.isSaving"
|
||||
:disabled="!props.canOperate"
|
||||
@click="emit('submit')"
|
||||
>
|
||||
{{ props.form.id ? '保存修改' : '新增并保存' }}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import type {
|
||||
PackagingFeeMode,
|
||||
PackagingFeeTierDto,
|
||||
StoreFeesSettingsDto,
|
||||
} from '#/api/store-fees';
|
||||
import type { PackagingFeeMode } from '#/api/store-fees';
|
||||
|
||||
/** 文件职责:费用设置页面常量定义。 */
|
||||
|
||||
@@ -15,47 +11,3 @@ export const PACKAGING_MODE_OPTIONS: Array<{
|
||||
{ label: '按订单收取', value: 'order' },
|
||||
{ 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. 加载门店列表与门店费用配置。
|
||||
* 2. 保存费用配置并维护快照。
|
||||
*/
|
||||
import type { StoreFeesSettingsDto } from '#/api/store-fees';
|
||||
import type {
|
||||
SaveStoreFeesSettingsParams,
|
||||
StoreFeesSettingsDto,
|
||||
} from '#/api/store-fees';
|
||||
import type {
|
||||
StoreFeesFormState,
|
||||
StoreFeesSettingsSnapshot,
|
||||
@@ -20,7 +23,6 @@ import {
|
||||
saveStoreFeesSettingsApi,
|
||||
} from '#/api/store-fees';
|
||||
|
||||
import { DEFAULT_FEES_SETTINGS, DEFAULT_PACKAGING_TIERS } from './constants';
|
||||
import {
|
||||
cloneOtherFees,
|
||||
cloneTiers,
|
||||
@@ -30,14 +32,28 @@ import {
|
||||
} from './helpers';
|
||||
|
||||
interface CreateDataActionsOptions {
|
||||
clearSettings: () => void;
|
||||
form: StoreFeesFormState;
|
||||
isConfigured: Ref<boolean>;
|
||||
isPageLoading: Ref<boolean>;
|
||||
isStoreLoading: Ref<boolean>;
|
||||
loadedStoreId: Ref<string>;
|
||||
selectedStoreId: Ref<string>;
|
||||
snapshot: Ref<null | StoreFeesSettingsSnapshot>;
|
||||
stores: Ref<StoreListItemDto[]>;
|
||||
}
|
||||
|
||||
const EMPTY_OTHER_FEES = {
|
||||
cutlery: {
|
||||
enabled: false,
|
||||
amount: 0,
|
||||
},
|
||||
rush: {
|
||||
enabled: false,
|
||||
amount: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export function createDataActions(options: CreateDataActionsOptions) {
|
||||
/** 同步页面表单,保持 reactive 引用不变。 */
|
||||
function syncForm(next: StoreFeesFormState) {
|
||||
@@ -64,65 +80,52 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
||||
return createSettingsSnapshot(options.form);
|
||||
}
|
||||
|
||||
/** 应用默认配置。 */
|
||||
function applyDefaultSettings() {
|
||||
syncForm({
|
||||
...DEFAULT_FEES_SETTINGS,
|
||||
packagingFeeTiers: cloneTiers(DEFAULT_PACKAGING_TIERS),
|
||||
otherFees: cloneOtherFees(DEFAULT_FEES_SETTINGS.otherFees),
|
||||
});
|
||||
}
|
||||
|
||||
/** 将接口返回值转为页面表单态。 */
|
||||
function normalizeSettings(
|
||||
source: null | Partial<StoreFeesSettingsDto> | undefined,
|
||||
): 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 {
|
||||
minimumOrderAmount: normalizeMoney(
|
||||
source?.minimumOrderAmount ?? DEFAULT_FEES_SETTINGS.minimumOrderAmount,
|
||||
DEFAULT_FEES_SETTINGS.minimumOrderAmount,
|
||||
),
|
||||
baseDeliveryFee: normalizeMoney(
|
||||
source?.baseDeliveryFee ?? DEFAULT_FEES_SETTINGS.baseDeliveryFee,
|
||||
DEFAULT_FEES_SETTINGS.baseDeliveryFee,
|
||||
),
|
||||
minimumOrderAmount: normalizeMoney(source?.minimumOrderAmount ?? 0, 0),
|
||||
baseDeliveryFee: normalizeMoney(source?.baseDeliveryFee ?? 0, 0),
|
||||
freeDeliveryThreshold:
|
||||
source?.freeDeliveryThreshold === null ||
|
||||
source?.freeDeliveryThreshold === undefined
|
||||
? null
|
||||
: normalizeMoney(
|
||||
source.freeDeliveryThreshold,
|
||||
DEFAULT_FEES_SETTINGS.freeDeliveryThreshold ?? 0,
|
||||
),
|
||||
packagingFeeMode:
|
||||
source?.packagingFeeMode === 'item' ||
|
||||
source?.packagingFeeMode === 'order'
|
||||
? source.packagingFeeMode
|
||||
: 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,
|
||||
),
|
||||
: normalizeMoney(source.freeDeliveryThreshold, 0),
|
||||
packagingFeeMode,
|
||||
orderPackagingFeeMode,
|
||||
fixedPackagingFee: normalizeMoney(source?.fixedPackagingFee ?? 0, 0),
|
||||
packagingFeeTiers: sortTiers(cloneTiers(source?.packagingFeeTiers ?? [])),
|
||||
otherFees: source?.otherFees
|
||||
? otherFees
|
||||
: cloneOtherFees(EMPTY_OTHER_FEES),
|
||||
};
|
||||
}
|
||||
|
||||
/** 按当前门店构建保存参数。 */
|
||||
function buildSavePayload(storeId: string): StoreFeesSettingsDto {
|
||||
function buildSavePayload(storeId: string): SaveStoreFeesSettingsParams {
|
||||
return {
|
||||
storeId,
|
||||
minimumOrderAmount: options.form.minimumOrderAmount,
|
||||
@@ -144,12 +147,25 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
||||
const result = await getStoreFeesSettingsApi(storeId);
|
||||
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));
|
||||
options.isConfigured.value = true;
|
||||
options.loadedStoreId.value = currentStoreId;
|
||||
options.snapshot.value = buildCurrentSnapshot();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
applyDefaultSettings();
|
||||
options.snapshot.value = buildCurrentSnapshot();
|
||||
options.isConfigured.value = false;
|
||||
options.loadedStoreId.value = '';
|
||||
options.snapshot.value = null;
|
||||
options.clearSettings();
|
||||
message.error('加载费用设置失败,请稍后重试');
|
||||
} finally {
|
||||
options.isPageLoading.value = false;
|
||||
}
|
||||
@@ -171,8 +187,9 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
||||
|
||||
if (options.stores.value.length === 0) {
|
||||
options.selectedStoreId.value = '';
|
||||
options.isConfigured.value = false;
|
||||
options.snapshot.value = null;
|
||||
applyDefaultSettings();
|
||||
options.clearSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -189,10 +206,12 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error('加载门店失败,请稍后重试');
|
||||
options.stores.value = [];
|
||||
options.selectedStoreId.value = '';
|
||||
options.isConfigured.value = false;
|
||||
options.snapshot.value = null;
|
||||
applyDefaultSettings();
|
||||
options.clearSettings();
|
||||
} finally {
|
||||
options.isStoreLoading.value = false;
|
||||
}
|
||||
@@ -205,6 +224,7 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
||||
const payload = buildSavePayload(options.selectedStoreId.value);
|
||||
const result = await saveStoreFeesSettingsApi(payload);
|
||||
syncForm(normalizeSettings(result ?? payload));
|
||||
options.isConfigured.value = true;
|
||||
options.snapshot.value = buildCurrentSnapshot();
|
||||
message.success(successText);
|
||||
return true;
|
||||
@@ -218,7 +238,7 @@ export function createDataActions(options: CreateDataActionsOptions) {
|
||||
/** 重置到最近一次快照。 */
|
||||
function resetFromSnapshot() {
|
||||
if (!options.snapshot.value) {
|
||||
applyDefaultSettings();
|
||||
message.warning('暂无可恢复的已保存配置');
|
||||
return;
|
||||
}
|
||||
syncForm(options.snapshot.value);
|
||||
|
||||
@@ -9,7 +9,6 @@ import type {
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { DEFAULT_PACKAGING_TIERS } from './constants';
|
||||
import {
|
||||
cloneTiers,
|
||||
createTierId,
|
||||
@@ -51,7 +50,7 @@ export function createPackagingActions(options: CreatePackagingActionsOptions) {
|
||||
options.tierForm.id = '';
|
||||
options.tierForm.minAmount = defaultMin;
|
||||
options.tierForm.maxAmount = null;
|
||||
options.tierForm.fee = normalizeMoney(lastTier?.fee ?? 2, 2);
|
||||
options.tierForm.fee = normalizeMoney(lastTier?.fee ?? 0, 0);
|
||||
options.isTierDrawerOpen.value = true;
|
||||
}
|
||||
|
||||
@@ -96,9 +95,6 @@ export function createPackagingActions(options: CreatePackagingActionsOptions) {
|
||||
/** 切换是否启用阶梯包装费。 */
|
||||
function toggleTiered(checked: boolean) {
|
||||
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 { message } from 'ant-design-vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
DEFAULT_FEES_SETTINGS,
|
||||
PACKAGING_MODE_OPTIONS,
|
||||
} from './fees-page/constants';
|
||||
import { PACKAGING_MODE_OPTIONS } from './fees-page/constants';
|
||||
import { createCopyActions } from './fees-page/copy-actions';
|
||||
import { createDataActions } from './fees-page/data-actions';
|
||||
import {
|
||||
@@ -33,6 +30,28 @@ import {
|
||||
} from './fees-page/helpers';
|
||||
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() {
|
||||
// 1. 页面 loading / submitting 状态。
|
||||
const isStoreLoading = ref(false);
|
||||
@@ -41,13 +60,13 @@ export function useStoreFeesPage() {
|
||||
const isSavingPackaging = ref(false);
|
||||
const isSavingOther = ref(false);
|
||||
const isCopySubmitting = ref(false);
|
||||
const isConfigured = ref(false);
|
||||
|
||||
// 2. 页面核心业务数据。
|
||||
const stores = ref<StoreListItemDto[]>([]);
|
||||
const selectedStoreId = ref('');
|
||||
const form = reactive<StoreFeesFormState>(
|
||||
cloneFeesForm(DEFAULT_FEES_SETTINGS),
|
||||
);
|
||||
const loadedStoreId = ref('');
|
||||
const form = reactive<StoreFeesFormState>(cloneFeesForm(EMPTY_FEES_SETTINGS));
|
||||
const snapshot = ref<null | StoreFeesSettingsSnapshot>(null);
|
||||
|
||||
// 3. 复制弹窗状态。
|
||||
@@ -61,7 +80,7 @@ export function useStoreFeesPage() {
|
||||
id: '',
|
||||
minAmount: 0,
|
||||
maxAmount: null,
|
||||
fee: 2,
|
||||
fee: 0,
|
||||
});
|
||||
|
||||
// 5. 页面衍生视图数据。
|
||||
@@ -74,6 +93,19 @@ export function useStoreFeesPage() {
|
||||
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(() =>
|
||||
stores.value.filter((store) => store.id !== selectedStoreId.value),
|
||||
@@ -97,6 +129,37 @@ export function useStoreFeesPage() {
|
||||
|
||||
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. 动作装配。
|
||||
const {
|
||||
loadStoreSettings,
|
||||
@@ -104,9 +167,12 @@ export function useStoreFeesPage() {
|
||||
resetFromSnapshot,
|
||||
saveCurrentSettings,
|
||||
} = createDataActions({
|
||||
clearSettings,
|
||||
form,
|
||||
isConfigured,
|
||||
isPageLoading,
|
||||
isStoreLoading,
|
||||
loadedStoreId,
|
||||
selectedStoreId,
|
||||
snapshot,
|
||||
stores,
|
||||
@@ -169,7 +235,25 @@ export function useStoreFeesPage() {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -196,7 +280,11 @@ export function useStoreFeesPage() {
|
||||
|
||||
/** 重置“起送与配送费”分区。 */
|
||||
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.baseDeliveryFee = source.baseDeliveryFee;
|
||||
form.freeDeliveryThreshold = source.freeDeliveryThreshold;
|
||||
@@ -205,7 +293,11 @@ export function useStoreFeesPage() {
|
||||
|
||||
/** 重置“包装费设置”分区。 */
|
||||
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.orderPackagingFeeMode = source.orderPackagingFeeMode;
|
||||
form.fixedPackagingFee = source.fixedPackagingFee;
|
||||
@@ -215,14 +307,18 @@ export function useStoreFeesPage() {
|
||||
|
||||
/** 重置“其他费用”分区。 */
|
||||
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);
|
||||
message.success('已重置其他费用');
|
||||
}
|
||||
|
||||
/** 保存“起送与配送费”分区。 */
|
||||
async function saveDeliverySection() {
|
||||
if (!selectedStoreId.value) return;
|
||||
if (!canOperate.value) return;
|
||||
isSavingDelivery.value = true;
|
||||
try {
|
||||
await saveCurrentSettings('起送与配送费已保存');
|
||||
@@ -233,7 +329,7 @@ export function useStoreFeesPage() {
|
||||
|
||||
/** 保存“包装费设置”分区。 */
|
||||
async function savePackagingSection() {
|
||||
if (!selectedStoreId.value) return;
|
||||
if (!canOperate.value) return;
|
||||
if (!validateCurrentPackaging()) return;
|
||||
isSavingPackaging.value = true;
|
||||
try {
|
||||
@@ -245,7 +341,7 @@ export function useStoreFeesPage() {
|
||||
|
||||
/** 保存“其他费用”分区。 */
|
||||
async function saveOtherSection() {
|
||||
if (!selectedStoreId.value) return;
|
||||
if (!canOperate.value) return;
|
||||
isSavingOther.value = true;
|
||||
try {
|
||||
await saveCurrentSettings('其他费用已保存');
|
||||
@@ -274,11 +370,14 @@ export function useStoreFeesPage() {
|
||||
/** 切换门店时同步拉取配置。 */
|
||||
watch(selectedStoreId, async (storeId) => {
|
||||
if (!storeId) {
|
||||
Object.assign(form, cloneFeesForm(DEFAULT_FEES_SETTINGS));
|
||||
clearSettings();
|
||||
isConfigured.value = false;
|
||||
snapshot.value = null;
|
||||
isTierDrawerOpen.value = false;
|
||||
return;
|
||||
}
|
||||
loadedStoreId.value = '';
|
||||
isConfigured.value = false;
|
||||
isTierDrawerOpen.value = false;
|
||||
await loadStoreSettings(storeId);
|
||||
});
|
||||
|
||||
@@ -291,6 +390,7 @@ export function useStoreFeesPage() {
|
||||
PACKAGING_MODE_OPTIONS,
|
||||
copyCandidates,
|
||||
copyTargetStoreIds,
|
||||
canOperate,
|
||||
form,
|
||||
formatCurrency,
|
||||
formatTierRange,
|
||||
@@ -301,6 +401,7 @@ export function useStoreFeesPage() {
|
||||
isCopyIndeterminate,
|
||||
isCopyModalOpen,
|
||||
isCopySubmitting,
|
||||
isConfigured,
|
||||
isOrderMode,
|
||||
isPageLoading,
|
||||
isSavingDelivery,
|
||||
@@ -308,6 +409,7 @@ export function useStoreFeesPage() {
|
||||
isSavingPackaging,
|
||||
isStoreLoading,
|
||||
isTierDrawerOpen,
|
||||
loadedStoreId,
|
||||
onDeleteTier,
|
||||
openCopyModal,
|
||||
openTierDrawer,
|
||||
@@ -318,6 +420,8 @@ export function useStoreFeesPage() {
|
||||
saveDeliverySection,
|
||||
saveOtherSection,
|
||||
savePackagingSection,
|
||||
hasLoadedStoreSettings,
|
||||
hasSelectedStore,
|
||||
selectedStoreId,
|
||||
selectedStoreName,
|
||||
setBaseDeliveryFee,
|
||||
|
||||
@@ -19,6 +19,7 @@ import FeesTierDrawer from './components/FeesTierDrawer.vue';
|
||||
import { useStoreFeesPage } from './composables/useStoreFeesPage';
|
||||
|
||||
const {
|
||||
canOperate,
|
||||
copyCandidates,
|
||||
copyTargetStoreIds,
|
||||
form,
|
||||
@@ -27,10 +28,12 @@ const {
|
||||
handleCopyCheckAll,
|
||||
handleCopySubmit,
|
||||
handleSubmitTier,
|
||||
hasLoadedStoreSettings,
|
||||
isCopyAllChecked,
|
||||
isCopyIndeterminate,
|
||||
isCopyModalOpen,
|
||||
isCopySubmitting,
|
||||
isConfigured,
|
||||
isPageLoading,
|
||||
isSavingDelivery,
|
||||
isSavingOther,
|
||||
@@ -81,7 +84,9 @@ function onEditTier(tier: PackagingFeeTierDto) {
|
||||
:selected-store-id="selectedStoreId"
|
||||
:store-options="storeOptions"
|
||||
:is-store-loading="isStoreLoading"
|
||||
:copy-disabled="!selectedStoreId || copyCandidates.length === 0"
|
||||
:copy-disabled="
|
||||
!canOperate || !isConfigured || copyCandidates.length === 0
|
||||
"
|
||||
copy-button-text="复制费用设置到其他门店"
|
||||
@update:selected-store-id="setSelectedStoreId"
|
||||
@copy="openCopyModal"
|
||||
@@ -95,7 +100,14 @@ function onEditTier(tier: PackagingFeeTierDto) {
|
||||
|
||||
<template v-else>
|
||||
<Spin :spinning="isPageLoading">
|
||||
<Card v-if="hasLoadedStoreSettings && !isConfigured" :bordered="false">
|
||||
<Empty
|
||||
description="当前门店尚未配置费用规则。请先填写并保存,之后可执行复制。"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<FeesDeliveryCard
|
||||
:can-operate="canOperate"
|
||||
:minimum-order-amount="form.minimumOrderAmount"
|
||||
:base-delivery-fee="form.baseDeliveryFee"
|
||||
:free-delivery-threshold="form.freeDeliveryThreshold"
|
||||
@@ -108,6 +120,7 @@ function onEditTier(tier: PackagingFeeTierDto) {
|
||||
/>
|
||||
|
||||
<FeesPackagingCard
|
||||
:can-operate="canOperate"
|
||||
:packaging-mode="form.packagingFeeMode"
|
||||
:tiered-enabled="form.orderPackagingFeeMode === 'tiered'"
|
||||
:fixed-packaging-fee="form.fixedPackagingFee"
|
||||
@@ -126,6 +139,7 @@ function onEditTier(tier: PackagingFeeTierDto) {
|
||||
/>
|
||||
|
||||
<FeesOtherCard
|
||||
:can-operate="canOperate"
|
||||
:cutlery-enabled="form.otherFees.cutlery.enabled"
|
||||
:cutlery-amount="form.otherFees.cutlery.amount"
|
||||
:rush-enabled="form.otherFees.rush.enabled"
|
||||
@@ -142,6 +156,7 @@ function onEditTier(tier: PackagingFeeTierDto) {
|
||||
</template>
|
||||
|
||||
<FeesTierDrawer
|
||||
:can-operate="canOperate"
|
||||
:open="isTierDrawerOpen"
|
||||
:title="tierDrawerTitle"
|
||||
:form="tierForm"
|
||||
|
||||
@@ -1,30 +1,58 @@
|
||||
/* 文件职责:包装费卡片样式。 */
|
||||
.page-store-fees {
|
||||
.packaging-mode-switch {
|
||||
.packaging-mode-toggle-row {
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
padding: 3px;
|
||||
margin-bottom: 16px;
|
||||
background: #f8f9fb;
|
||||
border-radius: 8px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mode-switch-item {
|
||||
padding: 6px 18px;
|
||||
.mode-toggle-label {
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.mode-switch-item.active {
|
||||
.mode-toggle-label.active {
|
||||
font-weight: 600;
|
||||
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 {
|
||||
|
||||
@@ -23,13 +23,13 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.packaging-mode-switch {
|
||||
.packaging-mode-toggle-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mode-switch-item {
|
||||
flex: 1;
|
||||
.mode-toggle-label {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user