重构盘点范围选择交互,将单一抽屉改为分步选择模式,新增盘点范围类型字段,支持仓库/库区/库位分别选择并可移除,优化选择流程和用户体验

This commit is contained in:
WUSIJIAN
2026-02-24 14:20:18 +08:00
parent dcfceb2d0e
commit ddae4f9173
2 changed files with 456 additions and 37 deletions

View File

@@ -22,23 +22,59 @@
</el-select>
</el-form-item>
<el-form-item label="盘点范围" prop="warehouseIds">
<el-form-item label="盘点范围类型" prop="scope">
<el-select v-model="form.scope" placeholder="请选择盘点范围类型" style="width: 100%" @change="onScopeTypeChange">
<el-option label="按仓库盘点" :value="1" />
<el-option label="按库区盘点" :value="2" />
<el-option label="按库位盘点" :value="3" />
<el-option label="按SKU盘点" :value="4" />
<el-option label="按资产盘点" :value="5" />
</el-select>
</el-form-item>
<!-- 选择仓库按仓库/库区/库位盘点都需要 -->
<el-form-item label="选择仓库" prop="warehouseIds" v-if="form.scope >= 1 && form.scope <= 3">
<div class="scope-select-box">
<div class="scope-tags" v-if="scopeDisplayText">
<el-tag type="info" size="small">{{ getScopeLabel(form.scope) }}</el-tag>
<el-tag v-for="name in selectedWarehouseNames" :key="name" size="small" style="margin-left: 4px;">{{ name }}</el-tag>
<el-tag v-for="name in selectedZoneNames" :key="name" size="small" type="success" style="margin-left: 4px;">{{ name }}</el-tag>
<el-tag v-for="name in selectedLocationNames" :key="name" size="small" type="warning" style="margin-left: 4px;">{{ name }}</el-tag>
<div class="scope-tags" v-if="form.warehouseIds.length > 0">
<el-tag v-for="name in selectedWarehouseNames" :key="name" size="small" closable @close="removeWarehouse(name)">{{ name }}</el-tag>
</div>
<el-button type="primary" link @click="openScopeDrawer">
<el-icon><ele-Setting /></el-icon>
{{ scopeDisplayText ? '修改' : '选择盘点范围' }}
<el-button type="primary" @click="openSelectDialog(1)">
<el-icon><ele-Plus /></el-icon>
{{ form.warehouseIds.length > 0 ? '继续选择' : '选择仓库' }}
</el-button>
</div>
</el-form-item>
<!-- 盘点范围选择抽屉 -->
<ScopeSelectDrawer ref="scopeDrawerRef" @confirm="onScopeConfirm" />
<!-- 选择库区按库区/库位盘点需要 -->
<el-form-item label="选择库区" prop="zoneIds" v-if="form.scope >= 2 && form.scope <= 3">
<div class="scope-select-box">
<div class="scope-tags" v-if="form.zoneIds.length > 0">
<el-tag v-for="name in selectedZoneNames" :key="name" size="small" type="success" closable @close="removeZone(name)">{{ name }}</el-tag>
</div>
<el-button type="primary" @click="openSelectDialog(2)" :disabled="form.warehouseIds.length === 0">
<el-icon><ele-Plus /></el-icon>
{{ form.zoneIds.length > 0 ? '继续选择' : '选择库区' }}
</el-button>
<span v-if="form.warehouseIds.length === 0" class="tip-text">请先选择仓库</span>
</div>
</el-form-item>
<!-- 选择库位按库位盘点需要 -->
<el-form-item label="选择库位" prop="locationIds" v-if="form.scope === 3">
<div class="scope-select-box">
<div class="scope-tags" v-if="form.locationIds.length > 0">
<el-tag v-for="name in selectedLocationNames" :key="name" size="small" type="warning" closable @close="removeLocation(name)">{{ name }}</el-tag>
</div>
<el-button type="primary" @click="openSelectDialog(3)" :disabled="form.zoneIds.length === 0">
<el-icon><ele-Plus /></el-icon>
{{ form.locationIds.length > 0 ? '继续选择' : '选择库位' }}
</el-button>
<span v-if="form.zoneIds.length === 0" class="tip-text">请先选择库区</span>
</div>
</el-form-item>
<!-- 选择弹窗 -->
<ScopeSelectDialog ref="scopeSelectDialogRef" @confirm="onSelectConfirm" />
<el-form-item label="负责人" prop="assigneeId">
@@ -66,14 +102,14 @@ import { ref, reactive, computed } from 'vue';
import { ElMessage } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
import { createInventoryCount, updateInventoryCount, getInventoryCount } from '/@/api/assets/operation/count';
import ScopeSelectDrawer from './scopeSelectDrawer.vue';
import ScopeSelectDialog from './scopeSelectDialog.vue';
const emit = defineEmits(['refresh']);
const visible = ref(false);
const loading = ref(false);
const formRef = ref<FormInstance>();
const scopeDrawerRef = ref();
const scopeSelectDialogRef = ref();
// 表单数据
const form = reactive({
@@ -98,15 +134,53 @@ const selectedLocationNames = ref<string[]>([]);
// 参与人员输入框
const participantsInput = ref('');
// 盘点范围显示文本
const scopeDisplayText = computed(() => {
return form.warehouseIds.length > 0;
});
// 当前选择的类型1-仓库 2-库区 3-库位)
const currentSelectType = ref(1);
// 获取盘点范围标签
const getScopeLabel = (scope: number) => {
const labels: Record<number, string> = { 1: '按仓库盘点', 2: '按库区盘点', 3: '按库位盘点', 4: '按SKU盘点', 5: '按资产盘点' };
return labels[scope] || '';
// 移除仓库
const removeWarehouse = (name: string) => {
const idx = selectedWarehouseNames.value.indexOf(name);
if (idx > -1) {
selectedWarehouseNames.value.splice(idx, 1);
form.warehouseIds.splice(idx, 1);
// 清空下级选择
form.zoneIds = [];
form.locationIds = [];
selectedZoneNames.value = [];
selectedLocationNames.value = [];
}
};
// 移除库区
const removeZone = (name: string) => {
const idx = selectedZoneNames.value.indexOf(name);
if (idx > -1) {
selectedZoneNames.value.splice(idx, 1);
form.zoneIds.splice(idx, 1);
// 清空下级选择
form.locationIds = [];
selectedLocationNames.value = [];
}
};
// 移除库位
const removeLocation = (name: string) => {
const idx = selectedLocationNames.value.indexOf(name);
if (idx > -1) {
selectedLocationNames.value.splice(idx, 1);
form.locationIds.splice(idx, 1);
}
};
// 盘点范围类型变化
const onScopeTypeChange = () => {
// 清空已选项
form.warehouseIds = [];
form.zoneIds = [];
form.locationIds = [];
selectedWarehouseNames.value = [];
selectedZoneNames.value = [];
selectedLocationNames.value = [];
};
// 表单校验规则
@@ -186,25 +260,52 @@ const resetForm = () => {
formRef.value?.resetFields();
};
// 打开盘点范围选择抽屉
const openScopeDrawer = () => {
scopeDrawerRef.value?.openDrawer({
scope: form.scope,
// 打开选择弹窗
const openSelectDialog = (type: number) => {
currentSelectType.value = type;
let selectedIds: string[] = [];
let selectedNames: string[] = [];
if (type === 1) {
selectedIds = form.warehouseIds;
selectedNames = selectedWarehouseNames.value;
} else if (type === 2) {
selectedIds = form.zoneIds;
selectedNames = selectedZoneNames.value;
} else if (type === 3) {
selectedIds = form.locationIds;
selectedNames = selectedLocationNames.value;
}
scopeSelectDialogRef.value?.open({
scope: type,
selectedIds,
selectedNames,
warehouseIds: form.warehouseIds,
zoneIds: form.zoneIds,
locationIds: form.locationIds,
});
};
// 盘点范围选择确认
const onScopeConfirm = (data: any) => {
form.scope = data.scope;
form.warehouseIds = data.warehouseIds;
form.zoneIds = data.zoneIds;
form.locationIds = data.locationIds;
selectedWarehouseNames.value = data.warehouseNames;
selectedZoneNames.value = data.zoneNames;
selectedLocationNames.value = data.locationNames;
// 选择确认
const onSelectConfirm = (data: any) => {
if (currentSelectType.value === 1) {
form.warehouseIds = data.selectedIds;
selectedWarehouseNames.value = data.selectedNames;
// 清空下级选择
form.zoneIds = [];
form.locationIds = [];
selectedZoneNames.value = [];
selectedLocationNames.value = [];
} else if (currentSelectType.value === 2) {
form.zoneIds = data.selectedIds;
selectedZoneNames.value = data.selectedNames;
// 清空下级选择
form.locationIds = [];
selectedLocationNames.value = [];
} else if (currentSelectType.value === 3) {
form.locationIds = data.selectedIds;
selectedLocationNames.value = data.selectedNames;
}
};
// 关闭弹窗
@@ -269,7 +370,7 @@ defineExpose({
<style scoped lang="scss">
.scope-select-box {
display: flex;
align-items: flex-start;
align-items: center;
flex-wrap: wrap;
gap: 8px;
@@ -277,7 +378,12 @@ defineExpose({
display: flex;
flex-wrap: wrap;
gap: 4px;
flex: 1;
}
.tip-text {
font-size: 12px;
color: #909399;
margin-left: 8px;
}
}
</style>

View File

@@ -0,0 +1,313 @@
<template>
<el-dialog
v-model="visible"
:title="dialogTitle"
width="800px"
:close-on-click-modal="false"
@close="handleClose"
>
<!-- 搜索栏 -->
<div class="search-bar">
<el-input
v-model="searchKeyword"
placeholder="请输入关键词搜索"
clearable
style="width: 300px"
@keyup.enter="handleSearch"
>
<template #append>
<el-button @click="handleSearch">
<el-icon><ele-Search /></el-icon>
</el-button>
</template>
</el-input>
</div>
<!-- 数据表格 -->
<el-table
ref="tableRef"
:data="tableData"
v-loading="loading"
border
style="width: 100%; margin-top: 15px"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="name" :label="getColumnLabel()" min-width="200" show-overflow-tooltip />
<el-table-column prop="code" label="编码" width="150" show-overflow-tooltip v-if="scope <= 2" />
</el-table>
<!-- 分页 -->
<div class="pagination-box">
<el-pagination
v-model:current-page="pageNum"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 已选项展示 -->
<div class="selected-box" v-if="selectedItems.length > 0">
<span class="selected-label">已选择 {{ selectedItems.length }} </span>
<el-tag
v-for="item in selectedItems"
:key="item.id"
size="small"
closable
style="margin-right: 4px; margin-bottom: 4px"
@close="removeSelected(item)"
>
{{ item.name }}
</el-tag>
</div>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleConfirm">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { listWarehouses } from '/@/api/assets/warehouse';
import { listZones } from '/@/api/assets/zone';
import { listLocations } from '/@/api/assets/location';
const emit = defineEmits(['confirm']);
const visible = ref(false);
const loading = ref(false);
const scope = ref(1); // 1-仓库 2-库区 3-库位
const searchKeyword = ref('');
const tableData = ref<any[]>([]);
const total = ref(0);
const pageNum = ref(1);
const pageSize = ref(10);
const tableRef = ref();
// 已选项
const selectedItems = ref<any[]>([]);
// 父级ID库区需要仓库ID库位需要库区ID
const parentWarehouseIds = ref<string[]>([]);
const parentZoneIds = ref<string[]>([]);
// 弹窗标题
const dialogTitle = computed(() => {
const titles: Record<number, string> = {
1: '选择仓库',
2: '选择库区',
3: '选择库位',
};
return titles[scope.value] || '选择';
});
// 获取列标题
const getColumnLabel = () => {
const labels: Record<number, string> = {
1: '仓库名称',
2: '库区名称',
3: '库位名称',
};
return labels[scope.value] || '名称';
};
// 打开弹窗
const open = async (data: {
scope: number;
selectedIds: string[];
selectedNames: string[];
warehouseIds?: string[];
zoneIds?: string[];
}) => {
scope.value = data.scope;
parentWarehouseIds.value = data.warehouseIds || [];
parentZoneIds.value = data.zoneIds || [];
// 恢复已选项
selectedItems.value = data.selectedIds.map((id, idx) => ({
id,
name: data.selectedNames[idx] || id,
}));
searchKeyword.value = '';
pageNum.value = 1;
visible.value = true;
await loadData();
};
// 加载数据
const loadData = async () => {
loading.value = true;
try {
let res: any;
const params = {
keyword: searchKeyword.value,
pageNum: pageNum.value,
pageSize: pageSize.value,
};
if (scope.value === 1) {
// 加载仓库
res = await listWarehouses(params);
const list = res.data?.list || [];
tableData.value = list.map((item: any) => ({
id: item.id,
name: item.warehouseName || item.name,
code: item.warehouseCode || item.code,
}));
total.value = res.data?.total || 0;
} else if (scope.value === 2) {
// 加载库区
if (parentWarehouseIds.value.length === 0) {
tableData.value = [];
total.value = 0;
return;
}
const allZones: any[] = [];
let totalCount = 0;
for (const warehouseId of parentWarehouseIds.value) {
res = await listZones({ ...params, warehouseId });
const list = res.data?.list || res.data?.items || res.data || [];
const zones = (Array.isArray(list) ? list : []).map((item: any) => ({
id: item.id || item._id,
name: item.zoneName || item.name,
code: item.zoneCode || item.code,
}));
allZones.push(...zones);
totalCount += res.data?.total || zones.length;
}
tableData.value = allZones;
total.value = totalCount;
} else if (scope.value === 3) {
// 加载库位
if (parentZoneIds.value.length === 0) {
tableData.value = [];
total.value = 0;
return;
}
const allLocations: any[] = [];
let totalCount = 0;
for (const zoneId of parentZoneIds.value) {
res = await listLocations({ ...params, zoneId });
const list = res.data?.list || res.data?.items || res.data || [];
const locations = (Array.isArray(list) ? list : []).map((item: any) => ({
id: item.id || item._id,
name: item.locationName || item.name,
code: item.locationCode || item.code,
}));
allLocations.push(...locations);
totalCount += res.data?.total || locations.length;
}
tableData.value = allLocations;
total.value = totalCount;
}
// 恢复选中状态
setTimeout(() => {
tableData.value.forEach((row: any) => {
if (selectedItems.value.some(item => item.id === row.id)) {
tableRef.value?.toggleRowSelection(row, true);
}
});
}, 0);
} catch (error) {
console.error('加载数据失败:', error);
} finally {
loading.value = false;
}
};
// 搜索
const handleSearch = () => {
pageNum.value = 1;
loadData();
};
// 分页大小变化
const handleSizeChange = () => {
pageNum.value = 1;
loadData();
};
// 页码变化
const handleCurrentChange = () => {
loadData();
};
// 选择变化
const handleSelectionChange = (selection: any[]) => {
// 合并当前页选中和之前选中的(去除当前页取消选中的)
const currentPageIds = tableData.value.map(item => item.id);
// 移除当前页的旧选中
const otherSelected = selectedItems.value.filter(item => !currentPageIds.includes(item.id));
// 添加当前页新选中
selectedItems.value = [...otherSelected, ...selection];
};
// 移除已选
const removeSelected = (item: any) => {
const idx = selectedItems.value.findIndex(i => i.id === item.id);
if (idx > -1) {
selectedItems.value.splice(idx, 1);
// 取消表格选中
const row = tableData.value.find(r => r.id === item.id);
if (row) {
tableRef.value?.toggleRowSelection(row, false);
}
}
};
// 关闭弹窗
const handleClose = () => {
visible.value = false;
};
// 确认选择
const handleConfirm = () => {
emit('confirm', {
selectedIds: selectedItems.value.map(item => item.id),
selectedNames: selectedItems.value.map(item => item.name),
});
handleClose();
};
defineExpose({
open,
});
</script>
<style scoped lang="scss">
.search-bar {
display: flex;
align-items: center;
}
.pagination-box {
display: flex;
justify-content: flex-end;
margin-top: 15px;
}
.selected-box {
margin-top: 15px;
padding: 10px;
background: #f5f7fa;
border-radius: 4px;
.selected-label {
font-size: 14px;
color: #606266;
margin-right: 8px;
}
}
</style>