feat(warehouse): implement bulk delete and multi-level rule-based batch generation for locations
This commit is contained in:
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user