diff --git a/inventory-backend/app/api/v1/warehouse.py b/inventory-backend/app/api/v1/warehouse.py new file mode 100644 index 0000000..8e24d09 --- /dev/null +++ b/inventory-backend/app/api/v1/warehouse.py @@ -0,0 +1,180 @@ +# inventory-backend/app/api/v1/warehouse.py +from flask import Blueprint, request, jsonify +from app.extensions import db +from app.models.system import SysWarehouseLocation + +warehouse_bp = Blueprint('warehouse', __name__, url_prefix='/api/v1/warehouse') + + +def build_tree(nodes, parent_id=None): + """ + 将平铺的数据构建为树形结构 + """ + tree = [] + for node in nodes: + if node.parent_id == parent_id: + children = build_tree(nodes, node.id) + node_dict = node.to_dict() + if children: + node_dict['children'] = children + else: + node_dict['children'] = [] + tree.append(node_dict) + return tree + + +@warehouse_bp.route('/tree', methods=['GET']) +def get_tree(): + """ + 获取库位树形结构 + """ + try: + # 查询所有库位 + all_locations = SysWarehouseLocation.query.order_by(SysWarehouseLocation.level, SysWarehouseLocation.id).all() + + # 构建树形结构 + tree_data = build_tree(all_locations, parent_id=None) + + return jsonify({ + 'code': 200, + 'msg': 'success', + 'data': tree_data + }) + except Exception as e: + return jsonify({ + 'code': 500, + 'msg': str(e), + 'data': None + }), 500 + + +@warehouse_bp.route('', methods=['POST']) +def create_location(): + """ + 创建库位 + """ + try: + data = request.get_json() + name = data.get('name', '').strip() + parent_id = data.get('parent_id') # None 表示顶级 + is_enabled = data.get('is_enabled', True) + + if not name: + return jsonify({'code': 400, 'msg': '库位名称不能为空', 'data': None}) + + # 计算 level 和 full_path + if parent_id is None: + level = 0 + full_path = name + parent_full_path = '' + else: + parent = SysWarehouseLocation.query.get(parent_id) + if not parent: + return jsonify({'code': 400, 'msg': '父级库位不存在', 'data': None}) + level = parent.level + 1 + parent_full_path = parent.full_path or '' + 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=is_enabled + ) + db.session.add(location) + db.session.commit() + + return jsonify({ + 'code': 200, + 'msg': '创建成功', + 'data': location.to_dict() + }) + except Exception as e: + db.session.rollback() + return jsonify({ + 'code': 500, + 'msg': str(e), + 'data': None + }), 500 + + +@warehouse_bp.route('/', methods=['PUT']) +def update_location(location_id): + """ + 更新库位 + """ + try: + data = request.get_json() + location = SysWarehouseLocation.query.get(location_id) + + if not location: + return jsonify({'code': 404, 'msg': '库位不存在', 'data': None}) + + # 更新名称 + if 'name' in data and data['name']: + new_name = data['name'].strip() + if new_name != location.name: + # 需要更新 full_path + parent = location.parent + if parent: + location.full_path = f"{parent.full_path}/{new_name}" if parent.full_path else new_name + else: + location.full_path = new_name + location.name = new_name + + # 更新启用状态 + if 'is_enabled' in data: + location.is_enabled = data['is_enabled'] + + db.session.commit() + + return jsonify({ + 'code': 200, + 'msg': '更新成功', + 'data': location.to_dict() + }) + except Exception as e: + db.session.rollback() + return jsonify({ + 'code': 500, + 'msg': str(e), + 'data': None + }), 500 + + +@warehouse_bp.route('/', methods=['DELETE']) +def delete_location(location_id): + """ + 删除库位(级联删除子库位) + """ + try: + location = SysWarehouseLocation.query.get(location_id) + + if not location: + return jsonify({'code': 404, 'msg': '库位不存在', 'data': None}) + + # 递归删除所有子库位 + 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) + db.session.commit() + + return jsonify({ + 'code': 200, + 'msg': '删除成功', + 'data': None + }) + except Exception as e: + db.session.rollback() + return jsonify({ + 'code': 500, + 'msg': str(e), + 'data': None + }), 500 diff --git a/inventory-backend/app/models/system.py b/inventory-backend/app/models/system.py index f3ecbb4..3daa395 100644 --- a/inventory-backend/app/models/system.py +++ b/inventory-backend/app/models/system.py @@ -168,10 +168,14 @@ class SysWarehouseLocation(db.Model): level = db.Column(db.Integer, default=0) # 层级深度,顶级为0 is_enabled = db.Column(db.Boolean, default=True) created_at = db.Column(db.DateTime, default=datetime.now) - updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now) + # 注意:数据库表中没有 updated_at 字段,不要添加! - # 自关联 - children = db.relationship('SysWarehouseLocation', backref=db.backref('parent', remote_side=[id]), lazy='dynamic') + # 自关联 - 使用 backref 定义父节点的反向引用 + children = db.relationship( + 'SysWarehouseLocation', + backref=db.backref('parent', remote_side=[id]), + lazy='dynamic' + ) def to_dict(self): return { @@ -181,6 +185,5 @@ class SysWarehouseLocation(db.Model): 'full_path': self.full_path, 'level': self.level, 'is_enabled': self.is_enabled, - 'created_at': self.created_at.isoformat() if self.created_at else None, - 'updated_at': self.updated_at.isoformat() if self.updated_at else None + 'created_at': self.created_at.isoformat() if self.created_at else None } \ No newline at end of file diff --git a/inventory-web/src/api/common/warehouse.ts b/inventory-web/src/api/common/warehouse.ts new file mode 100644 index 0000000..c2554e1 --- /dev/null +++ b/inventory-web/src/api/common/warehouse.ts @@ -0,0 +1,35 @@ +import request from '@/utils/request' + +// 获取库位树形结构 +export function getWarehouseTree() { + return request({ + url: '/v1/warehouse/tree', + method: 'get' + }) +} + +// 创建库位 +export function createWarehouse(data: any) { + return request({ + url: '/v1/warehouse', + method: 'post', + data + }) +} + +// 更新库位 +export function updateWarehouse(data: any) { + return request({ + url: `/v1/warehouse/${data.id}`, + method: 'put', + data + }) +} + +// 删除库位 +export function deleteWarehouse(id: number) { + return request({ + url: `/v1/warehouse/${id}`, + method: 'delete' + }) +} diff --git a/inventory-web/src/views/dashboard/index.vue b/inventory-web/src/views/dashboard/index.vue index ef2c4d0..76f908e 100644 --- a/inventory-web/src/views/dashboard/index.vue +++ b/inventory-web/src/views/dashboard/index.vue @@ -84,13 +84,31 @@
{{ node.label }} - + 新增下级 - + 编辑 - + 删除 @@ -195,6 +213,21 @@ const savePrinterConfig = async () => { } // ==================== 库位管理相关 ==================== +// 根据层级返回按钮颜色类型 +const getLevelButtonType = (level: number, action: string) => { + // 层级颜色映射 + const levelColors: Record> = { + 0: { add: 'primary', edit: 'info', delete: 'danger' }, // 顶级: 蓝/灰/红 + 1: { add: 'success', edit: 'warning', delete: 'danger' }, // 二级: 绿/橙/红 + 2: { add: 'warning', edit: 'info', delete: 'danger' }, // 三级: 橙/灰/红 + } + // 4级及以上统一使用危险色 + if (level >= 3) { + return 'danger' + } + return levelColors[level]?.[action] || 'primary' +} + const warehouseDialogVisible = ref(false) const treeRef = ref() const treeData = ref([]) diff --git a/inventory-web/src/views/stock/inbound/buy.vue b/inventory-web/src/views/stock/inbound/buy.vue index 06b1f6f..e0af247 100644 --- a/inventory-web/src/views/stock/inbound/buy.vue +++ b/inventory-web/src/views/stock/inbound/buy.vue @@ -327,16 +327,15 @@ - - - + @@ -653,6 +652,7 @@ import { getFilterOptions } from '@/api/inbound/buy' import {getLabelPreview, executePrint} from '@/api/common/print' +import { getWarehouseTree } from '@/api/common/warehouse' import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue' import { useUserStore } from '@/stores/user' @@ -782,6 +782,9 @@ const cameraRef = ref | null>(null) const currentCameraField = ref<'arrival_photo' | 'inspection_report'>('arrival_photo') const inspection_report_url = ref('') +// 库位级联选择器数据 +const warehouseOptions = ref([]) + const advancedFilterVisible = ref(false) const advancedConditions = ref([{ field: '', operator: '', value: '' }]) const fieldOptions = computed(() => { @@ -1227,6 +1230,18 @@ const fetchOptions = async () => { } } +// 加载库位树数据 +const loadWarehouseTree = async () => { + try { + const res = await getWarehouseTree() + if (res.code === 200) { + warehouseOptions.value = res.data || [] + } + } catch (e) { + console.error('加载库位树失败', e) + } +} + const resetQuery = () => { queryParams.keyword = '' queryParams.category = '' @@ -1524,6 +1539,7 @@ onMounted(() => { initColumnPermissions() fetchData() fetchOptions() + loadWarehouseTree() }) diff --git a/inventory-web/src/views/stock/inbound/product.vue b/inventory-web/src/views/stock/inbound/product.vue index ab16788..f05834c 100644 --- a/inventory-web/src/views/stock/inbound/product.vue +++ b/inventory-web/src/views/stock/inbound/product.vue @@ -286,7 +286,17 @@
- + + + @@ -513,6 +523,7 @@ import { import { uploadFile, deleteFile } from '@/api/inbound/buy' import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue' import { getLabelPreview, executePrint } from '@/api/common/print' +import { getWarehouseTree } from '@/api/common/warehouse' import { useUserStore } from '@/stores/user' // ------------------------------------ @@ -628,6 +639,9 @@ const currentCameraField = ref<'product_photo' | 'quality_report_link' | 'inspec const quality_url = ref('') const inspection_url = ref('') +// 库位级联选择器数据 +const warehouseOptions = ref([]) + // [核心优化] 所有列定义 const allColumns = [ { prop: 'company_name', label: '所属公司', minWidth: '100', sortable: true }, // [新增] @@ -958,6 +972,18 @@ const fetchOptions = async () => { } } +// 加载库位树数据 +const loadWarehouseTree = async () => { + try { + const res = await getWarehouseTree() + if (res.code === 200) { + warehouseOptions.value = res.data || [] + } + } catch (e) { + console.error('加载库位树失败', e) + } +} + const resetQuery = () => { queryParams.keyword = '' queryParams.category = '' @@ -1178,6 +1204,7 @@ onMounted(() => { initColumnPermissions() fetchData() fetchOptions() + loadWarehouseTree() }) // 成本计算监听 diff --git a/inventory-web/src/views/stock/inbound/semi.vue b/inventory-web/src/views/stock/inbound/semi.vue index 23275e2..762648f 100644 --- a/inventory-web/src/views/stock/inbound/semi.vue +++ b/inventory-web/src/views/stock/inbound/semi.vue @@ -345,7 +345,17 @@ - + + +
@@ -577,6 +587,7 @@ import { import { uploadFile, deleteFile } from '@/api/inbound/buy' import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue' import {getLabelPreview, executePrint} from '@/api/common/print' +import { getWarehouseTree } from '@/api/common/warehouse' import { useUserStore } from '@/stores/user' // ------------------------------------ @@ -691,6 +702,9 @@ const cameraRef = ref | null>(null) const currentCameraField = ref<'arrival_photo' | 'quality_report_link'>('arrival_photo') const quality_report_url = ref('') +// 库位级联选择器数据 +const warehouseOptions = ref([]) + const entryMode = ref('batch') const modeLocked = ref(false) @@ -1092,6 +1106,18 @@ const fetchOptions = async () => { } } +// 加载库位树数据 +const loadWarehouseTree = async () => { + try { + const res = await getWarehouseTree() + if (res.code === 200) { + warehouseOptions.value = res.data || [] + } + } catch (e) { + console.error('加载库位树失败', e) + } +} + const resetQuery = () => { queryParams.keyword = '' queryParams.category = '' @@ -1306,6 +1332,7 @@ onMounted(() => { initColumnPermissions() fetchData() fetchOptions() + loadWarehouseTree() })