diff --git a/inventory-backend/app/__init__.py b/inventory-backend/app/__init__.py
index 4a273cd..fd45320 100644
--- a/inventory-backend/app/__init__.py
+++ b/inventory-backend/app/__init__.py
@@ -130,6 +130,19 @@ def create_app():
except ImportError as e:
print(f"❌ 错误: Permission 模块导入失败 (请检查 app/api/v1/permission.py 是否存在): {e}")
+ # -----------------------------------------------------
+ # 2.8 注册库位管理模块 (Warehouse)
+ # -----------------------------------------------------
+ try:
+ from app.api.v1.warehouse import warehouse_bp
+ # 标准: /api/v1/warehouse/tree
+ app.register_blueprint(warehouse_bp, url_prefix='/api/v1/warehouse')
+ # 兼容: /api/warehouse/tree
+ app.register_blueprint(warehouse_bp, url_prefix='/api/warehouse', name='warehouse_legacy')
+ print("✅ Warehouse 模块注册成功")
+ except ImportError as e:
+ print(f"❌ 错误: Warehouse 模块导入失败: {e}")
+
# =========================================================
# 3. 预加载数据模型
# =========================================================
@@ -145,7 +158,7 @@ def create_app():
from app.models.outbound import TransOutbound
# 系统与业务模型 (SysRolePermission 等在 models.system 中)
- from app.models.system import SysUser, SysLog, SysMenu, SysElement, SysRolePermission
+ from app.models.system import SysUser, SysLog, SysMenu, SysElement, SysRolePermission, SysWarehouseLocation
# 确保借还模型被加载
from app.models.transaction import TransBorrow, TransRepair, TransScrap
diff --git a/inventory-backend/app/models/system.py b/inventory-backend/app/models/system.py
index f0b77e5..f3ecbb4 100644
--- a/inventory-backend/app/models/system.py
+++ b/inventory-backend/app/models/system.py
@@ -148,4 +148,39 @@ class SysRolePermission(db.Model):
id = db.Column(db.Integer, primary_key=True)
role_code = db.Column(db.String(50), nullable=False)
target_code = db.Column(db.String(100), nullable=False) # menu_code 或 element_code
- type = db.Column(db.String(20), nullable=False) # 'menu' 或 'element'
\ No newline at end of file
+ type = db.Column(db.String(20), nullable=False) # 'menu' 或 'element'
+
+
+# ==========================================
+# 4. 库位管理模型
+# ==========================================
+class SysWarehouseLocation(db.Model):
+ """
+ 库位字典表(支持无限层级树形结构)
+ 对应数据库: sys_warehouse_location
+ """
+ __tablename__ = 'sys_warehouse_location'
+
+ id = db.Column(db.Integer, primary_key=True)
+ parent_id = db.Column(db.Integer, db.ForeignKey('sys_warehouse_location.id'), nullable=True)
+ name = db.Column(db.String(100), nullable=False)
+ full_path = db.Column(db.String(500)) # 完整路径,如 "A区/货架1/第3层"
+ 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)
+
+ # 自关联
+ children = db.relationship('SysWarehouseLocation', backref=db.backref('parent', remote_side=[id]), lazy='dynamic')
+
+ def to_dict(self):
+ return {
+ 'id': self.id,
+ 'parent_id': self.parent_id,
+ 'name': self.name,
+ '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
+ }
\ No newline at end of file
diff --git a/inventory-web/src/views/dashboard/index.vue b/inventory-web/src/views/dashboard/index.vue
index f4eacfa..ef2c4d0 100644
--- a/inventory-web/src/views/dashboard/index.vue
+++ b/inventory-web/src/views/dashboard/index.vue
@@ -5,6 +5,9 @@
@@ -68,9 +129,10 @@ import { useRouter } from 'vue-router'
// 1. 引入 User Store
import { useUserStore } from '@/stores/user'
// 引入需要的图标
-import { Box, TrendCharts, ShoppingCart, Operation, Setting } 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 { ElMessage } from 'element-plus'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { getWarehouseTree, createWarehouse, updateWarehouse, deleteWarehouse } from '@/api/common/warehouse'
const router = useRouter()
// 2. 实例化 store
@@ -132,6 +194,141 @@ const savePrinterConfig = async () => {
}
}
+// ==================== 库位管理相关 ====================
+const warehouseDialogVisible = ref(false)
+const treeRef = ref()
+const treeData = ref([])
+const treeProps = {
+ label: 'name',
+ children: 'children'
+}
+
+// 库位表单
+const locationFormVisible = ref(false)
+const locationFormTitle = ref('')
+const locationForm = reactive({
+ id: undefined as number | undefined,
+ parent_id: null as number | null,
+ parentName: '无(顶级)',
+ name: '',
+ is_enabled: true
+})
+const locationLoading = ref(false)
+
+// 打开库位管理弹窗
+const openWarehouseDialog = async () => {
+ await loadWarehouseTree()
+ warehouseDialogVisible.value = true
+}
+
+// 加载库位树
+const loadWarehouseTree = async () => {
+ try {
+ const res = await getWarehouseTree()
+ if (res.code === 200) {
+ treeData.value = res.data || []
+ }
+ } catch (e) {
+ console.error(e)
+ }
+}
+
+// 新增顶级区域
+const handleAddTopLevel = () => {
+ locationForm.id = undefined
+ locationForm.parent_id = null
+ locationForm.parentName = '无(顶级)'
+ locationForm.name = ''
+ locationForm.is_enabled = true
+ locationFormTitle.value = '新增顶级库位'
+ locationFormVisible.value = true
+}
+
+// 新增下级
+const handleAddChild = (data: any) => {
+ locationForm.id = undefined
+ locationForm.parent_id = data.id
+ locationForm.parentName = data.full_path || data.name
+ locationForm.name = ''
+ locationForm.is_enabled = true
+ locationFormTitle.value = `新增下级库位:${data.name}`
+ locationFormVisible.value = true
+}
+
+// 编辑
+const handleEdit = (data: any) => {
+ locationForm.id = data.id
+ locationForm.parent_id = data.parent_id
+ locationForm.parentName = data.parent_id ? data.parent_full_path || '无(顶级)' : '无(顶级)'
+ locationForm.name = data.name
+ locationForm.is_enabled = data.is_enabled
+ locationFormTitle.value = `编辑库位:${data.name}`
+ locationFormVisible.value = true
+}
+
+// 删除
+const handleDelete = async (data: any) => {
+ try {
+ await ElMessageBox.confirm(
+ `确定要删除库位「${data.name}」吗?${data.children && data.children.length > 0 ? '该操作将同时删除所有子库位。' : ''}`,
+ '警告',
+ { type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }
+ )
+
+ const res = await deleteWarehouse(data.id)
+ if (res.code === 200) {
+ ElMessage.success('删除成功')
+ await loadWarehouseTree()
+ } else {
+ ElMessage.error(res.msg || '删除失败')
+ }
+ } catch (e) {
+ // 用户取消
+ }
+}
+
+// 保存库位
+const saveLocation = async () => {
+ if (!locationForm.name.trim()) {
+ ElMessage.warning('请输入库位名称')
+ return
+ }
+
+ try {
+ locationLoading.value = true
+ const payload: any = {
+ name: locationForm.name.trim(),
+ is_enabled: locationForm.is_enabled
+ }
+
+ if (locationForm.parent_id) {
+ payload.parent_id = locationForm.parent_id
+ }
+
+ let res
+ if (locationForm.id) {
+ // 编辑
+ payload.id = locationForm.id
+ res = await updateWarehouse(payload)
+ } else {
+ // 新增
+ res = await createWarehouse(payload)
+ }
+
+ if (res.code === 200) {
+ ElMessage.success('保存成功')
+ locationFormVisible.value = false
+ await loadWarehouseTree()
+ } else {
+ ElMessage.error(res.msg || '保存失败')
+ }
+ } catch (e) {
+ ElMessage.error('请求异常')
+ } finally {
+ locationLoading.value = false
+ }
+}
+
// 统一跳转函数
const handleNav = (path: string) => {
router.push(path)
@@ -197,4 +394,26 @@ const handleNav = (path: string) => {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
+
+/* 库位管理弹窗样式 */
+.warehouse-dialog {
+ padding: 10px 0;
+}
+.warehouse-header {
+ margin-bottom: 15px;
+}
+.tree-node {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding-right: 10px;
+}
+.node-label {
+ font-size: 14px;
+}
+.node-actions {
+ display: flex;
+ gap: 5px;
+}
diff --git a/inventory-web/src/views/stock/inbound/product.vue b/inventory-web/src/views/stock/inbound/product.vue
index f6cd06e..ab16788 100644
--- a/inventory-web/src/views/stock/inbound/product.vue
+++ b/inventory-web/src/views/stock/inbound/product.vue
@@ -716,7 +716,7 @@ const initColumnPermissions = () => {
}
// 如果没有保存的配置,使用默认列
- visibleColumnProps.value = defaultColumns
+ visibleColumnProps.value = defaultVisibleCols
}
// 检查列权限(移除权限限制,始终返回 true)