Compare commits

5 Commits

8 changed files with 369 additions and 13 deletions

View File

@ -4,5 +4,6 @@
"Bash(git add *)", "Bash(git add *)",
"Bash(git commit *)" "Bash(git commit *)"
] ]
} },
"$version": 3
} }

8
.qwen/settings.json.orig Normal file
View File

@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(git add *)",
"Bash(git commit *)"
]
}
}

Binary file not shown.

View File

@ -175,6 +175,9 @@ def delete_location(location_id):
if not location: if not location:
return jsonify({'code': 404, 'msg': '库位不存在', 'data': None}) return jsonify({'code': 404, 'msg': '库位不存在', 'data': None})
# 在删除前提取属性,避免 commit 后访问已删除对象
deleted_loc_name = location.name
# 递归删除所有子库位 # 递归删除所有子库位
def delete_recursive(loc): def delete_recursive(loc):
# 先删除所有子节点 # 先删除所有子节点
@ -190,7 +193,155 @@ def delete_location(location_id):
return jsonify({ return jsonify({
'code': 200, 'code': 200,
'msg': '删除成功', 'msg': '删除成功',
'location_code': location.location_code 'deleted_location': deleted_loc_name
})
except Exception as e:
db.session.rollback()
return jsonify({
'code': 500,
'msg': str(e),
'data': None
}), 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})
current_parents = [parent_id]
else:
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)
new_locations = []
for parent_id in current_parents:
# 1. 动态获取当前特定父节点的信息(严禁放循环外面共享!)
if parent_id is None:
current_level = 0
current_parent_path = ''
else:
p = SysWarehouseLocation.query.get(parent_id)
current_level = (p.level + 1) if p else 0
current_parent_path = p.full_path if p and p.full_path else ''
# 2. 生成当前父节点下的专属子节点
for num in range(start, end + 1):
name = f"{prefix}{str(num).zfill(pad)}"
# 路径由当前特定的 current_parent_path 决定
full_path = f"{current_parent_path}/{name}" if current_parent_path else name
location = SysWarehouseLocation(
name=name,
parent_id=parent_id,
full_path=full_path,
level=current_level,
is_enabled=True
)
db.session.add(location)
new_locations.append(location)
# 单层循环结束后再 flush 和获取新 ID 列表
db.session.flush()
current_parents = [loc.id for loc in new_locations]
generated_ids.extend(current_parents)
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: except Exception as e:
db.session.rollback() db.session.rollback()

View File

@ -1,6 +1,6 @@
{ {
"label_printer": { "label_printer": {
"ip": "172.16.0.119", "ip": "192.168.9.14",
"port": 9100 "port": 9100
}, },
"network_printer": { "network_printer": {

View File

@ -1,7 +1,7 @@
import socket import socket
# ================= 配置区域 ================= # ================= 配置区域 =================
PRINTER_IP = '192.168.9.89' PRINTER_IP = '192.168.9.101'
PRINTER_PORT = 9100 PRINTER_PORT = 9100

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

@ -72,9 +72,27 @@
<el-dialog v-model="warehouseDialogVisible" title="库位管理" width="700px" :close-on-click-modal="false"> <el-dialog v-model="warehouseDialogVisible" title="库位管理" width="700px" :close-on-click-modal="false">
<div class="warehouse-dialog"> <div class="warehouse-dialog">
<div class="warehouse-header"> <div class="warehouse-header">
<el-button type="primary" @click="handleAddTopLevel" :icon="Plus"> <!-- 非批量模式 -->
新增顶级区域 <template v-if="!isBulkDeleteMode">
</el-button> <el-button type="primary" @click="handleAddTopLevel" :icon="Plus">
新增顶级区域
</el-button>
<el-button type="success" @click="openBatchGenerate" :icon="Plus">
批量生成
</el-button>
<el-button type="danger" @click="isBulkDeleteMode = true" :icon="Delete">
批量删除
</el-button>
</template>
<!-- 批量模式 -->
<template v-else>
<el-button @click="cancelBatchMode" :icon="Close">
取消
</el-button>
<el-button type="danger" @click="handleBatchDelete" :disabled="selectedIds.length === 0" :icon="Delete">
确认删除 {{ selectedIds.length > 0 ? `(${selectedIds.length}项)` : '' }}
</el-button>
</template>
</div> </div>
<el-scrollbar height="450px"> <el-scrollbar height="450px">
<el-tree <el-tree
@ -84,6 +102,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="isBulkDeleteMode"
@check="handleTreeCheck"
> >
<template #default="{ node, data }"> <template #default="{ node, data }">
<div class="tree-node"> <div class="tree-node">
@ -143,19 +163,62 @@
</template> </template>
</el-dialog> </el-dialog>
<!-- 批量生成库位弹窗 -->
<el-dialog v-model="batchGenerateVisible" title="批量生成库位" width="800px">
<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="5">
<el-input v-model="rule.prefix" placeholder="前缀" />
</el-col>
<el-col :span="6">
<el-input-number v-model="rule.start" :min="1" placeholder="起始" style="width: 100%" controls-position="right" />
</el-col>
<el-col :span="6">
<el-input-number v-model="rule.end" :min="1" placeholder="结束" style="width: 100%" controls-position="right" />
</el-col>
<el-col :span="5">
<el-input-number v-model="rule.pad" :min="1" :max="5" placeholder="补零" style="width: 100%" controls-position="right" />
</el-col>
<el-col :span="2">
<el-button type="danger" :icon="Delete" @click="batchGenerateForm.rules.splice(index, 1)" style="width: 100%" />
</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'
// 引入需要的图标 // 引入需要的图标
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, Close } 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 +312,102 @@ const locationForm = reactive({
}) })
const locationLoading = ref(false) const locationLoading = ref(false)
// 批量删除相关
const selectedIds = ref<number[]>([])
const isBulkDeleteMode = ref(false)
const handleTreeCheck = (data: any, checked: any) => {
selectedIds.value = checked.checkedKeys
}
const cancelBatchMode = () => {
isBulkDeleteMode.value = false
selectedIds.value = []
if (treeRef.value) {
treeRef.value.setCheckedKeys([])
}
}
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('批量删除成功')
cancelBatchMode()
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 +625,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>