feat(warehouse): implement bulk delete and multi-level rule-based batch generation for locations

This commit is contained in:
DXC
2026-04-02 10:55:17 +08:00
parent 84e615baf6
commit e28326b2e4
3 changed files with 331 additions and 2 deletions

View File

@ -202,3 +202,157 @@ def delete_location(location_id):
'msg': str(e), 'msg': str(e),
'data': None 'data': None
}), 500 }), 500
@warehouse_bp.route('/batch', methods=['DELETE'])
@jwt_required()
@audit_log(
module='库位管理',
action='批量删除'
)
def batch_delete_locations():
"""
批量删除库位
"""
try:
ids = request.get_json()
if not ids or not isinstance(ids, list):
return jsonify({'code': 400, 'msg': '请提供要删除的库位ID列表', 'data': None})
deleted_count = 0
deleted_names = []
for loc_id in ids:
location = SysWarehouseLocation.query.get(loc_id)
if not location:
continue
# 在删除前提取属性
deleted_names.append(location.name)
# 递归删除
def delete_recursive(loc):
children = SysWarehouseLocation.query.filter_by(parent_id=loc.id).all()
for child in children:
delete_recursive(child)
db.session.delete(loc)
delete_recursive(location)
deleted_count += 1
db.session.commit()
return jsonify({
'code': 200,
'msg': f'删除成功,共删除 {deleted_count} 个库位',
'data': {'deleted_count': deleted_count, 'deleted_names': deleted_names}
})
except Exception as e:
db.session.rollback()
return jsonify({
'code': 500,
'msg': str(e),
'data': None
}), 500
@warehouse_bp.route('/batch-generate', methods=['POST'])
@jwt_required()
@audit_log(
module='库位管理',
action='批量生成'
)
def batch_generate_locations():
"""
规则化批量新增库位
"""
MAX_TOTAL = 3000 # 单次最多生成数量限制
try:
data = request.get_json()
parent_id = data.get('parent_id')
rules = data.get('rules', [])
if not rules:
return jsonify({'code': 400, 'msg': '请提供生成规则', 'data': None})
# 验证规则并计算总数
total_count = 1
for rule in rules:
start = rule.get('start', 1)
end = rule.get('end', 1)
total_count *= max(0, end - start + 1)
if total_count > MAX_TOTAL:
return jsonify({'code': 400, 'msg': f'单次生成数量不能超过 {MAX_TOTAL} 个,当前计划生成 {total_count}', 'data': None})
# 初始化父级列表
if parent_id:
parent = SysWarehouseLocation.query.get(parent_id)
if not parent:
return jsonify({'code': 404, 'msg': '父级库位不存在', 'data': None})
parent_level = parent.level
parent_full_path = parent.full_path or ''
current_parents = [parent_id]
else:
parent_level = -1 # 顶级的话,第一层.level = 0
parent_full_path = ''
current_parents = [None]
# 逐层处理规则
generated_ids = []
for rule in rules:
prefix = rule.get('prefix', '')
start = rule.get('start', 1)
end = rule.get('end', 1)
pad = rule.get('pad', 1)
next_parents = []
new_locations = []
for parent_id in current_parents:
# 计算该父级下的 level
if parent_id is None:
level = 0
else:
p = SysWarehouseLocation.query.get(parent_id)
level = p.level + 1 if p else 0
for num in range(start, end + 1):
name = f"{prefix}{str(num).zfill(pad)}"
full_path = f"{parent_full_path}/{name}" if parent_full_path else name
location = SysWarehouseLocation(
name=name,
parent_id=parent_id,
full_path=full_path,
level=level,
is_enabled=True
)
db.session.add(location)
new_locations.append(location)
# 立即刷新以获取 ID
db.session.flush()
generated_ids.extend([loc.id for loc in new_locations])
# 下一层的父级列表
current_parents = [loc.id for loc in new_locations]
# 更新 full_path 的基准路径(为下一层准备)
if new_locations:
parent_full_path = new_locations[0].full_path
db.session.commit()
return jsonify({
'code': 200,
'msg': f'生成成功,共生成 {len(generated_ids)} 个库位',
'data': {'generated_count': len(generated_ids), 'generated_ids': generated_ids}
})
except Exception as e:
db.session.rollback()
return jsonify({
'code': 500,
'msg': str(e),
'data': None
}), 500

View File

@ -33,3 +33,21 @@ export function deleteWarehouse(id: number) {
method: 'delete' method: 'delete'
}) })
} }
// 批量删除库位
export function batchDeleteWarehouse(ids: number[]) {
return request({
url: '/v1/warehouse/batch',
method: 'delete',
data: ids
})
}
// 规则化批量生成库位
export function batchGenerateWarehouse(data: { parent_id: number | null, rules: any[] }) {
return request({
url: '/v1/warehouse/batch-generate',
method: 'post',
data
})
}

View File

@ -75,6 +75,12 @@
<el-button type="primary" @click="handleAddTopLevel" :icon="Plus"> <el-button type="primary" @click="handleAddTopLevel" :icon="Plus">
新增顶级区域 新增顶级区域
</el-button> </el-button>
<el-button type="success" @click="openBatchGenerate" :icon="Plus">
批量生成
</el-button>
<el-button type="danger" @click="handleBatchDelete" :disabled="selectedIds.length === 0" :icon="Delete">
批量删除 {{ selectedIds.length > 0 ? `(${selectedIds.length})` : '' }}
</el-button>
</div> </div>
<el-scrollbar height="450px"> <el-scrollbar height="450px">
<el-tree <el-tree
@ -84,6 +90,8 @@
:props="treeProps" :props="treeProps"
:expand-on-click-node="false" :expand-on-click-node="false"
:default-expand-all="false" :default-expand-all="false"
show-checkbox
@check="handleTreeCheck"
> >
<template #default="{ node, data }"> <template #default="{ node, data }">
<div class="tree-node"> <div class="tree-node">
@ -143,11 +151,54 @@
</template> </template>
</el-dialog> </el-dialog>
<!-- 批量生成库位弹窗 -->
<el-dialog v-model="batchGenerateVisible" title="批量生成库位" width="600px">
<el-form :model="batchGenerateForm" label-width="100px">
<el-form-item label="父级库位">
<el-input :value="batchGenerateForm.parentName" disabled />
<el-button link size="small" @click="batchGenerateForm.parent_id = null; batchGenerateForm.parentName = '无(顶级)'">清空选择</el-button>
</el-form-item>
<el-divider>层级规则按顺序生成</el-divider>
<div v-for="(rule, index) in batchGenerateForm.rules" :key="index" class="rule-item">
<el-row :gutter="10">
<el-col :span="6">
<el-input v-model="rule.prefix" placeholder="前缀" />
</el-col>
<el-col :span="5">
<el-input-number v-model="rule.start" :min="1" placeholder="起始" style="width: 100%" />
</el-col>
<el-col :span="5">
<el-input-number v-model="rule.end" :min="1" placeholder="结束" style="width: 100%" />
</el-col>
<el-col :span="4">
<el-input-number v-model="rule.pad" :min="1" :max="5" placeholder="补零" style="width: 100%" />
</el-col>
<el-col :span="4">
<el-button type="danger" :icon="Delete" @click="batchGenerateForm.rules.splice(index, 1)" />
</el-col>
</el-row>
</div>
<el-button type="primary" plain @click="batchGenerateForm.rules.push({ prefix: '', start: 1, end: 10, pad: 1 })" :icon="Plus">
添加层级
</el-button>
<div class="batch-preview">
<span v-if="previewCount > 0">即将生成 <strong>{{ previewCount }}</strong> 个库位</span>
<span v-else>请完善规则</span>
</div>
</el-form>
<template #footer>
<el-button @click="batchGenerateVisible = false">取消</el-button>
<el-button type="primary" @click="handleBatchGenerate" :loading="batchLoading" :disabled="previewCount === 0 || previewCount > 3000">
生成 {{ previewCount > 3000 ? '(超限)' : '' }}
</el-button>
</template>
</el-dialog>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive } from 'vue' import { ref, reactive, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
// 1. 引入 User Store // 1. 引入 User Store
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
@ -155,7 +206,7 @@ import { useUserStore } from '@/stores/user'
import { Box, TrendCharts, ShoppingCart, Operation, Setting, Location, Plus, Edit, Delete } from '@element-plus/icons-vue' import { Box, TrendCharts, ShoppingCart, Operation, Setting, Location, Plus, Edit, Delete } from '@element-plus/icons-vue'
import { getPrinterConfig, updatePrinterConfig } from '@/api/common/print' import { getPrinterConfig, updatePrinterConfig } from '@/api/common/print'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { getWarehouseTree, createWarehouse, updateWarehouse, deleteWarehouse } from '@/api/common/warehouse' import { getWarehouseTree, createWarehouse, updateWarehouse, deleteWarehouse, batchDeleteWarehouse, batchGenerateWarehouse } from '@/api/common/warehouse'
const router = useRouter() const router = useRouter()
// 2. 实例化 store // 2. 实例化 store
@ -249,6 +300,93 @@ const locationForm = reactive({
}) })
const locationLoading = ref(false) const locationLoading = ref(false)
// 批量删除相关
const selectedIds = ref<number[]>([])
const handleTreeCheck = (data: any, checked: any) => {
selectedIds.value = checked.checkedKeys
}
const handleBatchDelete = async () => {
if (selectedIds.value.length === 0) {
ElMessage.warning('请先选择要删除的库位')
return
}
try {
await ElMessageBox.confirm(
`确定要删除选中的 ${selectedIds.value.length} 个库位吗?该操作将同时删除所有子库位。`,
'警告',
{ type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }
)
const res = await batchDeleteWarehouse(selectedIds.value)
if (res.code === 200) {
ElMessage.success('批量删除成功')
selectedIds.value = []
await loadWarehouseTree()
} else {
ElMessage.error(res.msg || '删除失败')
}
} catch (e) {
// 用户取消
}
}
// 批量生成相关
const batchGenerateVisible = ref(false)
const batchGenerateForm = reactive({
parent_id: null as number | null,
parentName: '无(顶级)',
rules: [{ prefix: '', start: 1, end: 10, pad: 1 }] as { prefix: string, start: number, end: number, pad: number }[]
})
const batchLoading = ref(false)
const previewCount = computed(() => {
let count = 1
for (const rule of batchGenerateForm.rules) {
if (rule.start && rule.end && rule.start <= rule.end) {
count *= (rule.end - rule.start + 1)
} else {
return 0
}
}
return count
})
const openBatchGenerate = () => {
batchGenerateForm.parent_id = null
batchGenerateForm.parentName = '无(顶级)'
batchGenerateForm.rules = [{ prefix: '', start: 1, end: 10, pad: 1 }]
batchGenerateVisible.value = true
}
const handleBatchGenerate = async () => {
if (previewCount.value === 0 || previewCount.value > 3000) {
ElMessage.warning('生成数量无效或超过限制')
return
}
try {
batchLoading.value = true
const res = await batchGenerateWarehouse({
parent_id: batchGenerateForm.parent_id,
rules: batchGenerateForm.rules
})
if (res.code === 200) {
ElMessage.success(`生成成功,共生成 ${res.data.generated_count} 个库位`)
batchGenerateVisible.value = false
await loadWarehouseTree()
} else {
ElMessage.error(res.msg || '生成失败')
}
} catch (e) {
ElMessage.error('请求异常')
} finally {
batchLoading.value = false
}
}
// 打开库位管理弹窗 // 打开库位管理弹窗
const openWarehouseDialog = async () => { const openWarehouseDialog = async () => {
await loadWarehouseTree() await loadWarehouseTree()
@ -466,4 +604,23 @@ const handleNav = (path: string) => {
display: flex; display: flex;
gap: 5px; gap: 5px;
} }
/* 批量生成规则样式 */
.rule-item {
margin-bottom: 10px;
padding: 10px;
background: #f5f7fa;
border-radius: 4px;
}
.batch-preview {
margin-top: 15px;
padding: 10px;
text-align: center;
background: #ecf5ff;
border-radius: 4px;
}
.batch-preview strong {
color: #409eff;
font-size: 16px;
}
</style> </style>