feat(project): 优化费用与自提模式切换交互
This commit is contained in:
@@ -88,6 +88,14 @@ export interface SaveStoreFeesSettingsParams {
|
|||||||
storeId: string;
|
storeId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 保存包装费收取方式参数 */
|
||||||
|
export interface SaveStoreFeesModeParams {
|
||||||
|
/** 包装费模式 */
|
||||||
|
packagingFeeMode: PackagingFeeMode;
|
||||||
|
/** 门店 ID */
|
||||||
|
storeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** 复制费用设置参数 */
|
/** 复制费用设置参数 */
|
||||||
export interface CopyStoreFeesSettingsParams {
|
export interface CopyStoreFeesSettingsParams {
|
||||||
sourceStoreId: string;
|
sourceStoreId: string;
|
||||||
@@ -108,6 +116,11 @@ export async function saveStoreFeesSettingsApi(
|
|||||||
return requestClient.post<StoreFeesSettingsDto>('/store/fees/save', data);
|
return requestClient.post<StoreFeesSettingsDto>('/store/fees/save', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 保存包装费收取方式 */
|
||||||
|
export async function saveStoreFeesModeApi(data: SaveStoreFeesModeParams) {
|
||||||
|
return requestClient.post('/store/fees/mode/save', data);
|
||||||
|
}
|
||||||
|
|
||||||
/** 复制费用设置到其他门店 */
|
/** 复制费用设置到其他门店 */
|
||||||
export async function copyStoreFeesSettingsApi(
|
export async function copyStoreFeesSettingsApi(
|
||||||
data: CopyStoreFeesSettingsParams,
|
data: CopyStoreFeesSettingsParams,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface Props {
|
|||||||
formatCurrency: (value: number) => string;
|
formatCurrency: (value: number) => string;
|
||||||
formatTierRange: (tier: PackagingFeeTierDto) => string;
|
formatTierRange: (tier: PackagingFeeTierDto) => string;
|
||||||
isSaving: boolean;
|
isSaving: boolean;
|
||||||
|
isSwitchingMode: boolean;
|
||||||
onSetFixedPackagingFee: (value: number) => void;
|
onSetFixedPackagingFee: (value: number) => void;
|
||||||
onSetPackagingMode: (value: 'item' | 'order') => void;
|
onSetPackagingMode: (value: 'item' | 'order') => void;
|
||||||
onSetTieredEnabled: (value: boolean) => void;
|
onSetTieredEnabled: (value: boolean) => void;
|
||||||
@@ -36,6 +37,20 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getModeLabel(mode: 'item' | 'order') {
|
||||||
|
return mode === 'order' ? '按订单收取' : '按商品收取';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModeHint(mode: 'item' | 'order') {
|
||||||
|
return mode === 'order' ? '按订单金额统一计算包装费' : '按商品维度汇总包装费';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModeEffect(mode: 'item' | 'order') {
|
||||||
|
return mode === 'order'
|
||||||
|
? '固定包装费/阶梯包装费生效,商品包装费暂不生效。'
|
||||||
|
: '商品包装费生效,本页固定包装费/阶梯包装费暂不生效。';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -44,50 +59,50 @@ function toNumber(value: null | number | string, fallback = 0) {
|
|||||||
<span class="section-title">包装费设置</span>
|
<span class="section-title">包装费设置</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="packaging-mode-toggle-row">
|
<div class="packaging-mode-panel">
|
||||||
|
<div class="packaging-mode-header">
|
||||||
|
<div class="packaging-mode-title">包装费收取方式</div>
|
||||||
|
<div class="packaging-mode-badge">
|
||||||
|
{{
|
||||||
|
props.isSwitchingMode
|
||||||
|
? '切换中...'
|
||||||
|
: `当前:${getModeLabel(props.packagingMode)}`
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="packaging-mode-grid">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="mode-toggle-label"
|
class="packaging-mode-card"
|
||||||
:class="{ active: props.packagingMode === 'order' }"
|
:class="{ active: props.packagingMode === 'order' }"
|
||||||
:disabled="!props.canOperate"
|
:disabled="!props.canOperate || props.isSwitchingMode"
|
||||||
@click="props.onSetPackagingMode('order')"
|
@click="props.onSetPackagingMode('order')"
|
||||||
>
|
>
|
||||||
按订单收取
|
<div class="packaging-mode-card-title">
|
||||||
|
<span class="packaging-mode-dot"></span>
|
||||||
|
{{ getModeLabel('order') }}
|
||||||
|
</div>
|
||||||
|
<div class="packaging-mode-card-hint">{{ getModeHint('order') }}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Switch
|
|
||||||
:checked="props.packagingMode === 'item'"
|
|
||||||
:disabled="!props.canOperate"
|
|
||||||
@update:checked="
|
|
||||||
(checked) => props.onSetPackagingMode(checked ? 'item' : 'order')
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="mode-toggle-label"
|
class="packaging-mode-card"
|
||||||
:class="{ active: props.packagingMode === 'item' }"
|
:class="{ active: props.packagingMode === 'item' }"
|
||||||
:disabled="!props.canOperate"
|
:disabled="!props.canOperate || props.isSwitchingMode"
|
||||||
@click="props.onSetPackagingMode('item')"
|
@click="props.onSetPackagingMode('item')"
|
||||||
>
|
>
|
||||||
按商品收取
|
<div class="packaging-mode-card-title">
|
||||||
|
<span class="packaging-mode-dot"></span>
|
||||||
|
{{ getModeLabel('item') }}
|
||||||
|
</div>
|
||||||
|
<div class="packaging-mode-card-hint">{{ getModeHint('item') }}</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="packaging-mode-effect">
|
||||||
class="packaging-mode-guide"
|
切换后效果:{{ getModeEffect(props.packagingMode) }}
|
||||||
: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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue';
|
|||||||
|
|
||||||
import { message, Modal } from 'ant-design-vue';
|
import { message, Modal } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { saveStoreFeesModeApi } from '#/api/store-fees';
|
||||||
|
|
||||||
import { PACKAGING_MODE_OPTIONS } from './fees-page/constants';
|
import { 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';
|
||||||
@@ -24,6 +26,7 @@ import {
|
|||||||
cloneFeesForm,
|
cloneFeesForm,
|
||||||
cloneOtherFees,
|
cloneOtherFees,
|
||||||
cloneTiers,
|
cloneTiers,
|
||||||
|
createSettingsSnapshot,
|
||||||
formatCurrency,
|
formatCurrency,
|
||||||
formatTierRange,
|
formatTierRange,
|
||||||
normalizeMoney,
|
normalizeMoney,
|
||||||
@@ -49,8 +52,6 @@ const EMPTY_FEES_SETTINGS: StoreFeesFormState = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const PACKAGING_MODE_SWITCH_CONFIRM_KEY =
|
|
||||||
'store-fees-packaging-mode-switch-confirmed';
|
|
||||||
|
|
||||||
export function useStoreFeesPage() {
|
export function useStoreFeesPage() {
|
||||||
// 1. 页面 loading / submitting 状态。
|
// 1. 页面 loading / submitting 状态。
|
||||||
@@ -59,6 +60,7 @@ export function useStoreFeesPage() {
|
|||||||
const isSavingDelivery = ref(false);
|
const isSavingDelivery = ref(false);
|
||||||
const isSavingPackaging = ref(false);
|
const isSavingPackaging = ref(false);
|
||||||
const isSavingOther = ref(false);
|
const isSavingOther = ref(false);
|
||||||
|
const isSwitchingPackagingMode = ref(false);
|
||||||
const isCopySubmitting = ref(false);
|
const isCopySubmitting = ref(false);
|
||||||
const isConfigured = ref(false);
|
const isConfigured = ref(false);
|
||||||
|
|
||||||
@@ -104,7 +106,8 @@ export function useStoreFeesPage() {
|
|||||||
!isPageLoading.value &&
|
!isPageLoading.value &&
|
||||||
!isSavingDelivery.value &&
|
!isSavingDelivery.value &&
|
||||||
!isSavingPackaging.value &&
|
!isSavingPackaging.value &&
|
||||||
!isSavingOther.value,
|
!isSavingOther.value &&
|
||||||
|
!isSwitchingPackagingMode.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
const copyCandidates = computed(() =>
|
const copyCandidates = computed(() =>
|
||||||
@@ -129,29 +132,11 @@ 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) {
|
function getPackagingModeConfirmContent(value: PackagingFeeMode) {
|
||||||
if (value === 'order') {
|
if (value === 'order') {
|
||||||
return '切换后订单将按本页固定/阶梯包装费计算,商品包装费配置将暂不生效。';
|
return '切换后将按订单收取包装费,固定/阶梯包装费立即生效;商品维度包装费将暂不生效。';
|
||||||
}
|
}
|
||||||
return '切换后订单包装费将汇总商品维度配置,本页固定/阶梯包装费将暂不生效。';
|
return '切换后将按商品收取包装费,商品配置的包装费立即生效;本页固定/阶梯包装费将暂不生效。';
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSettings() {
|
function clearSettings() {
|
||||||
@@ -237,21 +222,52 @@ export function useStoreFeesPage() {
|
|||||||
function setPackagingMode(value: PackagingFeeMode) {
|
function setPackagingMode(value: PackagingFeeMode) {
|
||||||
if (!canOperate.value) return;
|
if (!canOperate.value) return;
|
||||||
if (value === form.packagingFeeMode) return;
|
if (value === form.packagingFeeMode) return;
|
||||||
|
if (!selectedStoreId.value) return;
|
||||||
|
|
||||||
const applyMode = () => setPackagingFeeMode(value);
|
const currentStoreId = selectedStoreId.value;
|
||||||
if (hasConfirmedPackagingModeSwitch()) {
|
const previousMode = form.packagingFeeMode;
|
||||||
applyMode();
|
const previousOrderMode = form.orderPackagingFeeMode;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '确认切换包装费收取方式?',
|
title: '确认切换包装费收取方式?',
|
||||||
content: getPackagingModeConfirmContent(value),
|
content: getPackagingModeConfirmContent(value),
|
||||||
okText: '确认切换',
|
okText: '确认切换',
|
||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
onOk() {
|
async onOk() {
|
||||||
applyMode();
|
if (isSwitchingPackagingMode.value) return;
|
||||||
markPackagingModeSwitchConfirmed();
|
isSwitchingPackagingMode.value = true;
|
||||||
|
setPackagingFeeMode(value);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await saveStoreFeesModeApi({
|
||||||
|
storeId: currentStoreId,
|
||||||
|
packagingFeeMode: value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selectedStoreId.value === currentStoreId) {
|
||||||
|
isConfigured.value = true;
|
||||||
|
loadedStoreId.value = currentStoreId;
|
||||||
|
if (snapshot.value) {
|
||||||
|
snapshot.value = {
|
||||||
|
...snapshot.value,
|
||||||
|
packagingFeeMode: form.packagingFeeMode,
|
||||||
|
orderPackagingFeeMode: form.orderPackagingFeeMode,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
snapshot.value = createSettingsSnapshot(form);
|
||||||
|
}
|
||||||
|
message.success('包装费收取方式已切换');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
if (selectedStoreId.value === currentStoreId) {
|
||||||
|
form.packagingFeeMode = previousMode;
|
||||||
|
form.orderPackagingFeeMode = previousOrderMode;
|
||||||
|
}
|
||||||
|
message.error('包装费收取方式切换失败,请稍后重试');
|
||||||
|
} finally {
|
||||||
|
isSwitchingPackagingMode.value = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -369,6 +385,7 @@ export function useStoreFeesPage() {
|
|||||||
|
|
||||||
/** 切换门店时同步拉取配置。 */
|
/** 切换门店时同步拉取配置。 */
|
||||||
watch(selectedStoreId, async (storeId) => {
|
watch(selectedStoreId, async (storeId) => {
|
||||||
|
isSwitchingPackagingMode.value = false;
|
||||||
if (!storeId) {
|
if (!storeId) {
|
||||||
clearSettings();
|
clearSettings();
|
||||||
isConfigured.value = false;
|
isConfigured.value = false;
|
||||||
@@ -407,6 +424,7 @@ export function useStoreFeesPage() {
|
|||||||
isSavingDelivery,
|
isSavingDelivery,
|
||||||
isSavingOther,
|
isSavingOther,
|
||||||
isSavingPackaging,
|
isSavingPackaging,
|
||||||
|
isSwitchingPackagingMode,
|
||||||
isStoreLoading,
|
isStoreLoading,
|
||||||
isTierDrawerOpen,
|
isTierDrawerOpen,
|
||||||
loadedStoreId,
|
loadedStoreId,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const {
|
|||||||
isSavingDelivery,
|
isSavingDelivery,
|
||||||
isSavingOther,
|
isSavingOther,
|
||||||
isSavingPackaging,
|
isSavingPackaging,
|
||||||
|
isSwitchingPackagingMode,
|
||||||
isStoreLoading,
|
isStoreLoading,
|
||||||
isTierDrawerOpen,
|
isTierDrawerOpen,
|
||||||
onDeleteTier,
|
onDeleteTier,
|
||||||
@@ -126,6 +127,7 @@ function onEditTier(tier: PackagingFeeTierDto) {
|
|||||||
:fixed-packaging-fee="form.fixedPackagingFee"
|
:fixed-packaging-fee="form.fixedPackagingFee"
|
||||||
:tiers="form.packagingFeeTiers"
|
:tiers="form.packagingFeeTiers"
|
||||||
:is-saving="isSavingPackaging"
|
:is-saving="isSavingPackaging"
|
||||||
|
:is-switching-mode="isSwitchingPackagingMode"
|
||||||
:format-currency="formatCurrency"
|
:format-currency="formatCurrency"
|
||||||
:format-tier-range="formatTierRange"
|
:format-tier-range="formatTierRange"
|
||||||
:on-set-packaging-mode="setPackagingMode"
|
:on-set-packaging-mode="setPackagingMode"
|
||||||
|
|||||||
@@ -1,58 +1,110 @@
|
|||||||
/* 文件职责:包装费卡片样式。 */
|
/* 文件职责:包装费卡片样式。 */
|
||||||
.page-store-fees {
|
.page-store-fees {
|
||||||
.packaging-mode-toggle-row {
|
.packaging-mode-panel {
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e9f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.packaging-mode-header {
|
||||||
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 12px;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-toggle-label {
|
.packaging-mode-title {
|
||||||
padding: 0;
|
font-size: 14px;
|
||||||
font-size: 13px;
|
|
||||||
color: #4b5563;
|
|
||||||
cursor: pointer;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
transition: color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mode-toggle-label.active {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1677ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
font-weight: 600;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
.packaging-mode-guide .guide-desc {
|
.packaging-mode-badge {
|
||||||
|
padding: 3px 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #4b5563;
|
line-height: 1.5;
|
||||||
|
color: #155eef;
|
||||||
|
background: #eff4ff;
|
||||||
|
border: 1px solid #c7d7fe;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.packaging-mode-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.packaging-mode-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 92px;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #d8dde6;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.packaging-mode-card:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.packaging-mode-card:hover:not(:disabled) {
|
||||||
|
border-color: #86a6ff;
|
||||||
|
box-shadow: 0 4px 12px rgb(17 24 39 / 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.packaging-mode-card.active {
|
||||||
|
background: linear-gradient(160deg, #ecf3ff 0%, #f8fbff 100%);
|
||||||
|
border-color: #5b8bff;
|
||||||
|
box-shadow: 0 6px 18px rgb(32 94 255 / 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.packaging-mode-card-title {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.packaging-mode-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: #94a3b8;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.packaging-mode-card.active .packaging-mode-dot {
|
||||||
|
background: #2563eb;
|
||||||
|
box-shadow: 0 0 0 4px rgb(37 99 235 / 16%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.packaging-mode-card-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: #556070;
|
||||||
|
}
|
||||||
|
|
||||||
|
.packaging-mode-effect {
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #1f3a8a;
|
||||||
|
background: #f5f8ff;
|
||||||
|
border: 1px solid #dbe7ff;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.packaging-tier-block {
|
.packaging-tier-block {
|
||||||
|
|||||||
@@ -23,14 +23,25 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.packaging-mode-toggle-row {
|
.packaging-mode-panel {
|
||||||
display: flex;
|
gap: 10px;
|
||||||
justify-content: space-between;
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.packaging-mode-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.packaging-mode-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-toggle-label {
|
.packaging-mode-card {
|
||||||
text-align: center;
|
min-height: 84px;
|
||||||
|
padding: 10px 11px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
import type { PickupMode } from '#/api/store-pickup';
|
import type { PickupMode } from '#/api/store-pickup';
|
||||||
|
|
||||||
import { Switch } from 'ant-design-vue';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
isSwitching?: boolean;
|
isSwitching?: boolean;
|
||||||
@@ -24,54 +22,53 @@ const emit = defineEmits<{
|
|||||||
function getModeLabel(mode: PickupMode) {
|
function getModeLabel(mode: PickupMode) {
|
||||||
return props.options.find((item) => item.value === mode)?.label ?? mode;
|
return props.options.find((item) => item.value === mode)?.label ?? mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getModeHint(mode: PickupMode) {
|
||||||
|
return mode === 'big'
|
||||||
|
? '按时段统一预约,适合固定营业节奏'
|
||||||
|
: '按精细时间点预约,适合高峰分流';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="pickup-mode-switch-wrap">
|
<div class="pickup-mode-panel" :class="{ switching: props.isSwitching }">
|
||||||
<div class="pickup-mode-switch">
|
<div class="pickup-mode-header">
|
||||||
|
<div class="pickup-mode-title">自提预约模式</div>
|
||||||
|
<div class="pickup-mode-badge">
|
||||||
|
{{
|
||||||
|
props.isSwitching ? '切换中...' : `当前:${getModeLabel(props.mode)}`
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pickup-mode-grid">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="pickup-mode-item"
|
class="pickup-mode-card"
|
||||||
:class="{ active: props.mode === 'big' }"
|
:class="{ active: props.mode === 'big' }"
|
||||||
:disabled="props.disabled || props.isSwitching"
|
:disabled="props.disabled || props.isSwitching"
|
||||||
@click="emit('change', 'big')"
|
@click="emit('change', 'big')"
|
||||||
>
|
>
|
||||||
|
<div class="pickup-mode-card-title">
|
||||||
|
<span class="pickup-mode-dot"></span>
|
||||||
{{ getModeLabel('big') }}
|
{{ getModeLabel('big') }}
|
||||||
|
</div>
|
||||||
|
<div class="pickup-mode-card-hint">{{ getModeHint('big') }}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Switch
|
|
||||||
:checked="props.mode === 'fine'"
|
|
||||||
:loading="props.isSwitching"
|
|
||||||
:disabled="props.disabled || props.isSwitching"
|
|
||||||
@update:checked="(checked) => emit('change', checked ? 'fine' : 'big')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="pickup-mode-item"
|
class="pickup-mode-card"
|
||||||
:class="{ active: props.mode === 'fine' }"
|
:class="{ active: props.mode === 'fine' }"
|
||||||
:disabled="props.disabled || props.isSwitching"
|
:disabled="props.disabled || props.isSwitching"
|
||||||
@click="emit('change', 'fine')"
|
@click="emit('change', 'fine')"
|
||||||
>
|
>
|
||||||
|
<div class="pickup-mode-card-title">
|
||||||
|
<span class="pickup-mode-dot"></span>
|
||||||
{{ getModeLabel('fine') }}
|
{{ getModeLabel('fine') }}
|
||||||
|
</div>
|
||||||
|
<div class="pickup-mode-card-hint">{{ getModeHint('fine') }}</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
class="pickup-mode-guide"
|
|
||||||
:class="
|
|
||||||
props.mode === 'fine'
|
|
||||||
? 'pickup-mode-guide-fine'
|
|
||||||
: 'pickup-mode-guide-big'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div class="guide-title">当前生效规则</div>
|
|
||||||
<div v-if="props.mode === 'big'" class="guide-desc">
|
|
||||||
顾客按大时段进行预约,系统按已配置时段控制可约时间与容量。
|
|
||||||
</div>
|
|
||||||
<div v-else class="guide-desc">
|
|
||||||
顾客按精细时间窗口预约,系统按精细规则自动生成可约时间点。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ 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 { savePickupModeApi } from '#/api/store-pickup';
|
import { savePickupModeApi } from '#/api/store-pickup';
|
||||||
|
|
||||||
@@ -243,15 +243,30 @@ export function useStorePickupPage() {
|
|||||||
selectedStoreId.value = value;
|
selectedStoreId.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setPickupMode(value: 'big' | 'fine') {
|
function getPickupModeConfirmContent(value: 'big' | 'fine') {
|
||||||
|
if (value === 'big') {
|
||||||
|
return '切换后将按大时段统一预约,精细时段规则与预览将暂不生效。';
|
||||||
|
}
|
||||||
|
return '切换后将按精细时段预约,大时段配置将暂不生效。';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPickupMode(value: 'big' | 'fine') {
|
||||||
if (!canOperate.value) return;
|
if (!canOperate.value) return;
|
||||||
if (value === pickupMode.value) return;
|
if (value === pickupMode.value) return;
|
||||||
if (!selectedStoreId.value) return;
|
if (!selectedStoreId.value) return;
|
||||||
|
|
||||||
const currentStoreId = selectedStoreId.value;
|
const currentStoreId = selectedStoreId.value;
|
||||||
const previousMode = pickupMode.value;
|
const previousMode = pickupMode.value;
|
||||||
pickupMode.value = value;
|
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认切换自提预约模式?',
|
||||||
|
content: getPickupModeConfirmContent(value),
|
||||||
|
okText: '确认切换',
|
||||||
|
cancelText: '取消',
|
||||||
|
async onOk() {
|
||||||
|
if (isModeSwitching.value) return;
|
||||||
isModeSwitching.value = true;
|
isModeSwitching.value = true;
|
||||||
|
pickupMode.value = value;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await savePickupModeApi({
|
await savePickupModeApi({
|
||||||
@@ -262,6 +277,12 @@ export function useStorePickupPage() {
|
|||||||
if (selectedStoreId.value === currentStoreId) {
|
if (selectedStoreId.value === currentStoreId) {
|
||||||
isConfigured.value = true;
|
isConfigured.value = true;
|
||||||
loadedStoreId.value = currentStoreId;
|
loadedStoreId.value = currentStoreId;
|
||||||
|
if (snapshot.value) {
|
||||||
|
snapshot.value = {
|
||||||
|
...snapshot.value,
|
||||||
|
mode: value,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
snapshot.value = createSettingsSnapshot({
|
snapshot.value = createSettingsSnapshot({
|
||||||
mode: value,
|
mode: value,
|
||||||
basicSettings,
|
basicSettings,
|
||||||
@@ -269,17 +290,20 @@ export function useStorePickupPage() {
|
|||||||
fineRule,
|
fineRule,
|
||||||
previewDays: previewDays.value,
|
previewDays: previewDays.value,
|
||||||
});
|
});
|
||||||
message.success('自提模式已切换');
|
}
|
||||||
|
message.success('自提预约模式已切换');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (selectedStoreId.value === currentStoreId) {
|
if (selectedStoreId.value === currentStoreId) {
|
||||||
pickupMode.value = previousMode;
|
pickupMode.value = previousMode;
|
||||||
message.error('自提模式切换失败,请稍后重试');
|
|
||||||
}
|
}
|
||||||
|
message.error('自提预约模式切换失败,请稍后重试');
|
||||||
} finally {
|
} finally {
|
||||||
isModeSwitching.value = false;
|
isModeSwitching.value = false;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setAllowSameDayPickup(value: boolean) {
|
function setAllowSameDayPickup(value: boolean) {
|
||||||
|
|||||||
@@ -1,71 +1,105 @@
|
|||||||
/* 文件职责:自提设置页面模式切换样式。 */
|
/* 文件职责:自提设置页面模式切换样式。 */
|
||||||
.page-store-pickup {
|
.page-store-pickup {
|
||||||
.pickup-mode-switch-wrap {
|
.pickup-mode-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
|
padding: 14px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e9f0;
|
||||||
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pickup-mode-switch {
|
.pickup-mode-header {
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 3px;
|
justify-content: space-between;
|
||||||
background: #f8f9fb;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pickup-mode-item {
|
.pickup-mode-title {
|
||||||
min-width: 118px;
|
font-size: 14px;
|
||||||
padding: 6px 18px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #4b5563;
|
|
||||||
cursor: pointer;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pickup-mode-item:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pickup-mode-item.active {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1677ff;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 1px 2px rgb(15 23 42 / 10%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pickup-mode-guide {
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pickup-mode-guide-big {
|
|
||||||
background: #f5f9ff;
|
|
||||||
border-color: #bfdbfe;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pickup-mode-guide-fine {
|
|
||||||
background: #effff7;
|
|
||||||
border-color: #bbf7d0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pickup-mode-guide .guide-title {
|
|
||||||
margin-bottom: 2px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pickup-mode-guide .guide-desc {
|
.pickup-mode-badge {
|
||||||
|
padding: 3px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #155eef;
|
||||||
|
background: #eff4ff;
|
||||||
|
border: 1px solid #c7d7fe;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-mode-panel.switching .pickup-mode-badge {
|
||||||
|
color: #9a3412;
|
||||||
|
background: #fff7ed;
|
||||||
|
border-color: #fdba74;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-mode-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-mode-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 92px;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #d8dde6;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-mode-card:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-mode-card:hover:not(:disabled) {
|
||||||
|
border-color: #86a6ff;
|
||||||
|
box-shadow: 0 4px 12px rgb(17 24 39 / 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-mode-card.active {
|
||||||
|
background: linear-gradient(160deg, #ecf3ff 0%, #f8fbff 100%);
|
||||||
|
border-color: #5b8bff;
|
||||||
|
box-shadow: 0 6px 18px rgb(32 94 255 / 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-mode-card-title {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-mode-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: #94a3b8;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-mode-card.active .pickup-mode-dot {
|
||||||
|
background: #2563eb;
|
||||||
|
box-shadow: 0 0 0 4px rgb(37 99 235 / 16%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-mode-card-hint {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
color: #4b5563;
|
color: #556070;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,20 +7,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.pickup-mode-switch-wrap {
|
.pickup-mode-panel {
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pickup-mode-switch {
|
.pickup-mode-header {
|
||||||
display: flex;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
gap: 6px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pickup-mode-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pickup-mode-item {
|
.pickup-mode-card {
|
||||||
flex: 1;
|
min-height: 84px;
|
||||||
min-width: 0;
|
padding: 10px 11px;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pickup-form-row {
|
.pickup-form-row {
|
||||||
|
|||||||
Reference in New Issue
Block a user