feat: 批量工具面板改为抽屉式交互
All checks were successful
Build and Deploy TenantUI / build-and-deploy (push) Successful in 51s

This commit is contained in:
2026-02-26 13:50:52 +08:00
parent 97e833d8d8
commit 2617234d9e
8 changed files with 210 additions and 109 deletions

View File

@@ -4,6 +4,7 @@ import { Button, Select, Space, Upload } from 'ant-design-vue';
interface Props { interface Props {
categoryOptions: Array<{ label: string; value: string }>; categoryOptions: Array<{ label: string; value: string }>;
embedded?: boolean;
exportCategoryIds: string[]; exportCategoryIds: string[];
exportScopeType: 'all' | 'category'; exportScopeType: 'all' | 'category';
open: boolean; open: boolean;
@@ -21,7 +22,9 @@ interface Emits {
(event: 'update:exportScopeType', value: 'all' | 'category'): void; (event: 'update:exportScopeType', value: 'all' | 'category'): void;
} }
const props = defineProps<Props>(); const props = withDefaults(defineProps<Props>(), {
embedded: false,
});
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
const beforeUpload: UploadProps['beforeUpload'] = (file) => { const beforeUpload: UploadProps['beforeUpload'] = (file) => {
@@ -40,8 +43,12 @@ function setExportCategoryIds(value: unknown) {
</script> </script>
<template> <template>
<section v-if="props.open" class="pbt-panel"> <section
<header class="pbt-panel-hd"> v-if="props.open"
class="pbt-panel"
:class="{ 'pbt-panel-embedded': props.embedded }"
>
<header v-if="!props.embedded" class="pbt-panel-hd">
<h3>导入导出</h3> <h3>导入导出</h3>
<button type="button" class="pbt-close" @click="emit('close')">×</button> <button type="button" class="pbt-close" @click="emit('close')">×</button>
</header> </header>

View File

@@ -3,6 +3,7 @@ import { Button, Select } from 'ant-design-vue';
interface Props { interface Props {
categoryOptions: Array<{ label: string; value: string }>; categoryOptions: Array<{ label: string; value: string }>;
embedded?: boolean;
estimatedCount: number; estimatedCount: number;
open: boolean; open: boolean;
sourceCategoryId: string; sourceCategoryId: string;
@@ -17,7 +18,9 @@ interface Emits {
(event: 'update:targetCategoryId', value: string): void; (event: 'update:targetCategoryId', value: string): void;
} }
const props = defineProps<Props>(); const props = withDefaults(defineProps<Props>(), {
embedded: false,
});
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
function setSourceCategory(value: unknown) { function setSourceCategory(value: unknown) {
@@ -30,8 +33,12 @@ function setTargetCategory(value: unknown) {
</script> </script>
<template> <template>
<section v-if="props.open" class="pbt-panel"> <section
<header class="pbt-panel-hd"> v-if="props.open"
class="pbt-panel"
:class="{ 'pbt-panel-embedded': props.embedded }"
>
<header v-if="!props.embedded" class="pbt-panel-hd">
<h3>批量移动分类</h3> <h3>批量移动分类</h3>
<button type="button" class="pbt-close" @click="emit('close')">×</button> <button type="button" class="pbt-close" @click="emit('close')">×</button>
</header> </header>

View File

@@ -9,6 +9,7 @@ interface Props {
amountType: 'fixed' | 'percent'; amountType: 'fixed' | 'percent';
categoryOptions: Array<{ label: string; value: string }>; categoryOptions: Array<{ label: string; value: string }>;
direction: 'down' | 'up'; direction: 'down' | 'up';
embedded?: boolean;
open: boolean; open: boolean;
previewItems: BatchPricePreviewRow[]; previewItems: BatchPricePreviewRow[];
previewLoading: boolean; previewLoading: boolean;
@@ -32,7 +33,9 @@ interface Emits {
(event: 'update:scopeType', value: BatchScopeType): void; (event: 'update:scopeType', value: BatchScopeType): void;
} }
const props = defineProps<Props>(); const props = withDefaults(defineProps<Props>(), {
embedded: false,
});
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
const scopePills = [ const scopePills = [
@@ -93,8 +96,12 @@ function setAmount(value: null | number | string) {
</script> </script>
<template> <template>
<section v-if="props.open" class="pbt-panel"> <section
<header class="pbt-panel-hd"> v-if="props.open"
class="pbt-panel"
:class="{ 'pbt-panel-embedded': props.embedded }"
>
<header v-if="!props.embedded" class="pbt-panel-hd">
<h3>批量调价</h3> <h3>批量调价</h3>
<button type="button" class="pbt-close" @click="emit('close')">×</button> <button type="button" class="pbt-close" @click="emit('close')">×</button>
</header> </header>

View File

@@ -5,6 +5,7 @@ import { Button, Select } from 'ant-design-vue';
interface Props { interface Props {
action: 'off' | 'on'; action: 'off' | 'on';
categoryOptions: Array<{ label: string; value: string }>; categoryOptions: Array<{ label: string; value: string }>;
embedded?: boolean;
estimatedCount: number; estimatedCount: number;
open: boolean; open: boolean;
productOptions: Array<{ label: string; value: string }>; productOptions: Array<{ label: string; value: string }>;
@@ -23,7 +24,9 @@ interface Emits {
(event: 'update:scopeType', value: BatchScopeType): void; (event: 'update:scopeType', value: BatchScopeType): void;
} }
const props = defineProps<Props>(); const props = withDefaults(defineProps<Props>(), {
embedded: false,
});
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
const scopePills = [ const scopePills = [
@@ -54,8 +57,12 @@ function setScopeProductIds(value: unknown) {
</script> </script>
<template> <template>
<section v-if="props.open" class="pbt-panel"> <section
<header class="pbt-panel-hd"> v-if="props.open"
class="pbt-panel"
:class="{ 'pbt-panel-embedded': props.embedded }"
>
<header v-if="!props.embedded" class="pbt-panel-hd">
<h3>批量上下架</h3> <h3>批量上下架</h3>
<button type="button" class="pbt-close" @click="emit('close')">×</button> <button type="button" class="pbt-close" @click="emit('close')">×</button>
</header> </header>

View File

@@ -2,6 +2,7 @@
import { Button, Checkbox, Input, Select, Space } from 'ant-design-vue'; import { Button, Checkbox, Input, Select, Space } from 'ant-design-vue';
interface Props { interface Props {
embedded?: boolean;
open: boolean; open: boolean;
productIds: string[]; productIds: string[];
productOptions: Array<{ label: string; value: string }>; productOptions: Array<{ label: string; value: string }>;
@@ -24,7 +25,9 @@ interface Emits {
(event: 'update:targetStoreIds', value: string[]): void; (event: 'update:targetStoreIds', value: string[]): void;
} }
const props = defineProps<Props>(); const props = withDefaults(defineProps<Props>(), {
embedded: false,
});
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
function normalizeArray(value: unknown) { function normalizeArray(value: unknown) {
@@ -56,8 +59,12 @@ function setSyncStatus(value: boolean | string | undefined) {
</script> </script>
<template> <template>
<section v-if="props.open" class="pbt-panel"> <section
<header class="pbt-panel-hd"> v-if="props.open"
class="pbt-panel"
:class="{ 'pbt-panel-embedded': props.embedded }"
>
<header v-if="!props.embedded" class="pbt-panel-hd">
<h3>批量同步门店</h3> <h3>批量同步门店</h3>
<button type="button" class="pbt-close" @click="emit('close')">×</button> <button type="button" class="pbt-close" @click="emit('close')">×</button>
</header> </header>

View File

@@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { Page } from '@vben/common-ui'; import { Page } from '@vben/common-ui';
import { computed } from 'vue';
import { Alert, Empty, Spin } from 'ant-design-vue'; import { Alert, Drawer, Empty, Spin } from 'ant-design-vue';
import BatchImportExportPanel from './components/BatchImportExportPanel.vue'; import BatchImportExportPanel from './components/BatchImportExportPanel.vue';
import BatchMoveCategoryPanel from './components/BatchMoveCategoryPanel.vue'; import BatchMoveCategoryPanel from './components/BatchMoveCategoryPanel.vue';
@@ -11,6 +12,7 @@ import BatchStoreSyncPanel from './components/BatchStoreSyncPanel.vue';
import StoreScopeToolbar from '../../shared/components/StoreScopeToolbar.vue'; import StoreScopeToolbar from '../../shared/components/StoreScopeToolbar.vue';
import BatchToolCards from './components/BatchToolCards.vue'; import BatchToolCards from './components/BatchToolCards.vue';
import { useProductBatchPage } from './composables/useProductBatchPage'; import { useProductBatchPage } from './composables/useProductBatchPage';
import type { BatchToolKey } from './types';
const { const {
activeTool, activeTool,
@@ -119,6 +121,21 @@ function updateExportScopeType(value: 'all' | 'category') {
function updateExportCategoryIds(value: string[]) { function updateExportCategoryIds(value: string[]) {
exportCategoryIds.value = value; exportCategoryIds.value = value;
} }
const toolTitleMap: Record<BatchToolKey, string> = {
price: '批量调价',
sale: '批量上下架',
category: '批量移动分类',
sync: '批量同步门店',
excel: '导入导出',
};
const activeToolTitle = computed(() => {
if (!activeTool.value) {
return '';
}
return toolTitleMap[activeTool.value];
});
</script> </script>
<template> <template>
@@ -151,98 +168,6 @@ function updateExportCategoryIds(value: string[]) {
:active-tool="activeTool" :active-tool="activeTool"
@toggle="toggleTool" @toggle="toggleTool"
/> />
<BatchPriceAdjustPanel
:open="activeTool === 'price'"
:scope-type="scopeType"
:scope-category-ids="scopeCategoryIds"
:scope-product-ids="scopeProductIds"
:category-options="categoryOptions"
:product-options="productOptions"
:direction="priceDirection"
:amount-type="priceAmountType"
:amount="priceAmount"
:preview-items="pricePreview.items"
:preview-total="pricePreview.totalCount"
:preview-loading="pricePreviewLoading"
:submitting="isSubmitting"
@update:scope-type="setScopeType"
@update:scope-category-ids="updateScopeCategoryIds"
@update:scope-product-ids="updateScopeProductIds"
@update:direction="updatePriceDirection"
@update:amount-type="updatePriceAmountType"
@update:amount="updatePriceAmount"
@preview="previewPriceAdjust"
@submit="submitPriceAdjust"
@close="closeTool"
/>
<BatchSaleSwitchPanel
:open="activeTool === 'sale'"
:scope-type="scopeType"
:scope-category-ids="scopeCategoryIds"
:scope-product-ids="scopeProductIds"
:category-options="categoryOptions"
:product-options="productOptions"
:estimated-count="scopeEstimatedCount"
:action="saleAction"
:submitting="isSubmitting"
@update:scope-type="setScopeType"
@update:scope-category-ids="updateScopeCategoryIds"
@update:scope-product-ids="updateScopeProductIds"
@update:action="updateSaleAction"
@submit="submitSaleSwitch"
@close="closeTool"
/>
<BatchMoveCategoryPanel
:open="activeTool === 'category'"
:category-options="categoryOptions"
:source-category-id="moveSourceCategoryId"
:target-category-id="moveTargetCategoryId"
:estimated-count="moveEstimatedCount"
:submitting="isSubmitting"
@update:source-category-id="updateMoveSourceCategoryId"
@update:target-category-id="updateMoveTargetCategoryId"
@submit="submitMoveCategory"
@close="closeTool"
/>
<BatchStoreSyncPanel
:open="activeTool === 'sync'"
:source-store-name="selectedStoreLabel"
:target-store-ids="syncTargetStoreIds"
:target-store-options="targetStoreOptions"
:product-ids="syncProductIds"
:product-options="productOptions"
:sync-price="syncPrice"
:sync-stock="syncStock"
:sync-status="syncStatus"
:submitting="isSubmitting"
@update:target-store-ids="updateSyncTargetStoreIds"
@update:product-ids="updateSyncProductIds"
@update:sync-price="updateSyncPrice"
@update:sync-stock="updateSyncStock"
@update:sync-status="updateSyncStatus"
@submit="submitStoreSync"
@close="closeTool"
/>
<BatchImportExportPanel
:open="activeTool === 'excel'"
:selected-file-name="importFile?.name || ''"
:export-scope-type="exportScopeType"
:export-category-ids="exportCategoryIds"
:category-options="categoryOptions"
:submitting="isSubmitting"
@select-file="setImportFile"
@update:export-scope-type="updateExportScopeType"
@update:export-category-ids="updateExportCategoryIds"
@download-template="downloadImportTemplate"
@submit-import="submitImport"
@submit-export="submitExport"
@close="closeTool"
/>
</template> </template>
<Alert <Alert
@@ -253,6 +178,116 @@ function updateExportCategoryIds(value: string[]) {
:message="latestResultText" :message="latestResultText"
/> />
</div> </div>
<Drawer
class="pbt-drawer"
:title="activeToolTitle"
:open="Boolean(activeTool)"
:width="920"
@close="closeTool"
>
<BatchPriceAdjustPanel
v-if="activeTool === 'price'"
embedded
:open="true"
:scope-type="scopeType"
:scope-category-ids="scopeCategoryIds"
:scope-product-ids="scopeProductIds"
:category-options="categoryOptions"
:product-options="productOptions"
:direction="priceDirection"
:amount-type="priceAmountType"
:amount="priceAmount"
:preview-items="pricePreview.items"
:preview-total="pricePreview.totalCount"
:preview-loading="pricePreviewLoading"
:submitting="isSubmitting"
@update:scope-type="setScopeType"
@update:scope-category-ids="updateScopeCategoryIds"
@update:scope-product-ids="updateScopeProductIds"
@update:direction="updatePriceDirection"
@update:amount-type="updatePriceAmountType"
@update:amount="updatePriceAmount"
@preview="previewPriceAdjust"
@submit="submitPriceAdjust"
@close="closeTool"
/>
<BatchSaleSwitchPanel
v-if="activeTool === 'sale'"
embedded
:open="true"
:scope-type="scopeType"
:scope-category-ids="scopeCategoryIds"
:scope-product-ids="scopeProductIds"
:category-options="categoryOptions"
:product-options="productOptions"
:estimated-count="scopeEstimatedCount"
:action="saleAction"
:submitting="isSubmitting"
@update:scope-type="setScopeType"
@update:scope-category-ids="updateScopeCategoryIds"
@update:scope-product-ids="updateScopeProductIds"
@update:action="updateSaleAction"
@submit="submitSaleSwitch"
@close="closeTool"
/>
<BatchMoveCategoryPanel
v-if="activeTool === 'category'"
embedded
:open="true"
:category-options="categoryOptions"
:source-category-id="moveSourceCategoryId"
:target-category-id="moveTargetCategoryId"
:estimated-count="moveEstimatedCount"
:submitting="isSubmitting"
@update:source-category-id="updateMoveSourceCategoryId"
@update:target-category-id="updateMoveTargetCategoryId"
@submit="submitMoveCategory"
@close="closeTool"
/>
<BatchStoreSyncPanel
v-if="activeTool === 'sync'"
embedded
:open="true"
:source-store-name="selectedStoreLabel"
:target-store-ids="syncTargetStoreIds"
:target-store-options="targetStoreOptions"
:product-ids="syncProductIds"
:product-options="productOptions"
:sync-price="syncPrice"
:sync-stock="syncStock"
:sync-status="syncStatus"
:submitting="isSubmitting"
@update:target-store-ids="updateSyncTargetStoreIds"
@update:product-ids="updateSyncProductIds"
@update:sync-price="updateSyncPrice"
@update:sync-stock="updateSyncStock"
@update:sync-status="updateSyncStatus"
@submit="submitStoreSync"
@close="closeTool"
/>
<BatchImportExportPanel
v-if="activeTool === 'excel'"
embedded
:open="true"
:selected-file-name="importFile?.name || ''"
:export-scope-type="exportScopeType"
:export-category-ids="exportCategoryIds"
:category-options="categoryOptions"
:submitting="isSubmitting"
@select-file="setImportFile"
@update:export-scope-type="updateExportScopeType"
@update:export-category-ids="updateExportCategoryIds"
@download-template="downloadImportTemplate"
@submit-import="submitImport"
@submit-export="submitExport"
@close="closeTool"
/>
</Drawer>
</Page> </Page>
</template> </template>

View File

@@ -1,4 +1,22 @@
.page-product-batch { .pbt-drawer .ant-drawer-header {
padding: 14px 20px;
border-bottom: 1px solid #f0f0f0;
}
.pbt-drawer .ant-drawer-title {
font-size: 15px;
font-weight: 600;
color: #1a1a2e;
}
.pbt-drawer .ant-drawer-body {
padding: 16px 20px 20px;
background: #f8fafc;
}
.page-product-batch,
.pbt-drawer {
.pbt-panel { .pbt-panel {
padding: 20px; padding: 20px;
background: #fff; background: #fff;
@@ -6,6 +24,13 @@
box-shadow: var(--pbt-shadow-sm); box-shadow: var(--pbt-shadow-sm);
} }
.pbt-panel.pbt-panel-embedded {
padding: 0;
background: transparent;
border-radius: 0;
box-shadow: none;
}
.pbt-panel-hd { .pbt-panel-hd {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -21,3 +21,9 @@
} }
} }
} }
@media (width <= 1200px) {
.pbt-drawer .ant-drawer-content-wrapper {
width: min(920px, 100vw) !important;
}
}