38 Commits

Author SHA1 Message Date
dxc
5bc3dab31c feat: add field-level permission control for inbound modules
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 14:52:12 +08:00
dxc
079987e7f3 feat: enforce field-level permissions for material creation and update
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 14:41:27 +08:00
dxc
00c45c72fb inventory-web/src/views/system/UserCreate.vue
```vue
<<<<<<< SEARCH
          <el-button type="primary" @click="handleCreate">
            + 新增员工
          </el-button>
=======
          <el-button v-if="userStore.hasPermission('system_user:operation')" type="primary" @click="handleCreate">
            + 新增员工
          </el-button>
>>>>>>> REPLACE
```

Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 14:34:48 +08:00
dxc
6fa5233ea6 feat: implement RBAC and field masking for system_user module
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 14:28:48 +08:00
dxc
3f83e8742b fix: remove duplicate error messages in BOM manage page
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 14:20:51 +08:00
dxc
348e4dd024 feat: add RBAC read-write separation and field masking for bom_manage
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 14:13:02 +08:00
dxc
42b0cddd3e feat: add column permission checks to transaction records table
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 14:07:48 +08:00
dxc
a2b1a62132 feat: add RBAC and field masking for borrow/return/records pages
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 14:05:52 +08:00
dxc
5065410662 feat: add RBAC control for outbound list module
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 13:57:59 +08:00
dxc
3714dd180b feat: apply RBAC read/write separation to outbound_create module
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 13:54:06 +08:00
dxc
af41eb1803 feat: add RBAC controls for outbound selection module
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 13:45:49 +08:00
dxc
f79fb53b17 inventory-web/src/views/stock/stocktake/index.vue
```vue
<<<<<<< SEARCH
          <el-button type="primary" size="large" class="action-btn-full" @click="startNewSession" :loading="btnLoading">
            开始新盘点
          </el-button>
=======
          <el-button v-if="userStore.hasPermission('inventory_stocktake:operation')" type="primary" size="large" class="action-btn-full" @click="startNewSession" :loading="btnLoading">
            开始新盘点
          </el-button>
>>>>>>> REPLACE
```

inventory-web/src/views/stock/stocktake/index.vue
```vue
<<<<<<< SEARCH
          <el-button
              v-if="serverDraftCount > 0"
              type="warning"
              plain
              size="large"
              class="action-btn-full"
              @click="resumeSession"
              :loading="btnLoading"
          >
            继续上次盘点 <span class="sub-text">({{ serverDraftCount }}项)</span>
          </el-button>
=======
          <el-button
              v-if="serverDraftCount > 0 && userStore.hasPermission('inventory_stocktake:operation')"
              type="warning"
              plain
              size="large"
              class="action-btn-full"
              @click="resumeSession"
              :loading="btnLoading"
          >
            继续上次盘点 <span class="sub-text">({{ serverDraftCount }}项)</span>
          </el-button>
>>>>>>> REPLACE
```

Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 13:40:55 +08:00
dxc
38f0bbe41d feat: add RBAC for inventory stocktake module
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 13:36:10 +08:00
dxc
1ad477eda8 feat: add permission management to inbound service module
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 13:12:45 +08:00
dxc
1d2e8feced feat: apply RBAC permission control to product module
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 13:03:27 +08:00
dxc
246fb45cde fix: correct try block syntax
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 12:56:54 +08:00
dxc
6e914f1e96 feat: add RBAC permission control for semi inbound module
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 12:08:57 +08:00
dxc
b5b1efdc4e fix: remove duplicate allColumns declaration
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 11:56:15 +08:00
dxc
56bb6a1c84 chore: add user store import to buy inbound view
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 11:51:28 +08:00
dxc
379bc5786f feat: implement RBAC for inbound buy module with field-level permissions
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 11:48:33 +08:00
dxc
a96597da33 基础信息页面权限管理成功,后端返回值隐藏未开放权限内容 2026-02-27 11:10:22 +08:00
dxc
4c1c61065e fix: exclude operation columns from field permission dropdown
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 11:08:00 +08:00
dxc
25487dbede fix: operation permission detection for codes ending with :operation
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 11:00:44 +08:00
dxc
a547d6b164 fix: restore strict column permission control
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 10:57:30 +08:00
dxc
661ce4e5a0 fix: disable column hiding by permissions in material list view
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 10:54:18 +08:00
dxc
d6d9621bf3 fix: resolve global permission code collision with material_list prefix
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 10:43:32 +08:00
dxc
f178b9cd00 fix: correct permission codes in inbound base API
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 10:36:28 +08:00
dxc
11fafde5e3 fix: remove temporary role whitelist and add permission denial logging
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 10:29:15 +08:00
dxc
1f9a363545 chore: add debug logs and temp whitelist to permission decorator
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 10:27:44 +08:00
dxc
b3e1ac6245 feat: implement permission checking and field-level data masking
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 10:20:09 +08:00
dxc
73ee163352 feat: add MaterialBase permission control with field-level filtering
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-27 10:16:43 +08:00
dxc
c86e67b793 feat: refresh user permissions on page reload
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-26 16:21:35 +08:00
dxc
57c2c532ca fix: skip column permission checks for super admin and IRIS
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-26 16:14:01 +08:00
dxc
dad7ffdc66 fix: tie column display to user permissions in material list
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-26 16:07:54 +08:00
dxc
b798c42abf feat: add permission control for material list page
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
2026-02-26 15:58:23 +08:00
dxc
8698b2582c refactor: rebuild permission tree and improve assignment with error handling 2026-02-26 15:57:59 +08:00
dxc
220f50dba6 超级权限管理员权限重复提交报错 2026-02-25 16:54:52 +08:00
dxc
7431f1f41e 权限管理,没有页面修改之前版本 2026-02-25 16:10:12 +08:00
38 changed files with 3038 additions and 240 deletions

View File

@ -80,7 +80,6 @@ def create_app():
# -----------------------------------------------------
# 2.4 注册业务操作模块 (Transactions - 借还/维修/报废)
# ★★★ 关键修改:将前缀改为 /api/v1/transactions 以匹配前端请求 ★★★
# -----------------------------------------------------
try:
from app.api.v1.transactions import trans_bp
@ -90,8 +89,7 @@ def create_app():
app.register_blueprint(trans_bp, url_prefix='/api/transactions', name='trans_legacy')
print("✅ Transactions 模块注册成功")
except ImportError as e:
# 允许模块不存在时不崩溃,但在开发借还功能时这里报错说明 trans_bp 定义有问题
print(f"⚠️ 提示: Transaction 模块导入失败 (请检查 app/api/v1/transactions.py): {e}")
print(f"⚠️ 提示: Transaction 模块导入失败: {e}")
# -----------------------------------------------------
# 2.5 注册出库模块 (Outbound)
@ -119,6 +117,19 @@ def create_app():
except ImportError as e:
print(f"❌ 错误: BOM 模块导入失败: {e}")
# -----------------------------------------------------
# 2.7 注册权限管理模块 (Permission) - [新增]
# -----------------------------------------------------
try:
from app.api.v1.permission import permission_bp
# 标准: /api/v1/permissions/tree
app.register_blueprint(permission_bp, url_prefix='/api/v1/permissions')
# 兼容: /api/permissions/tree
app.register_blueprint(permission_bp, url_prefix='/api/permissions', name='permission_legacy')
print("✅ Permission 模块注册成功")
except ImportError as e:
print(f"❌ 错误: Permission 模块导入失败 (请检查 app/api/v1/permission.py 是否存在): {e}")
# =========================================================
# 3. 预加载数据模型
# =========================================================
@ -133,8 +144,8 @@ def create_app():
# 出库模型
from app.models.outbound import TransOutbound
# 系统与业务模型
from app.models.system import SysUser, SysLog
# 系统与业务模型 (SysRolePermission 等在 models.system 中)
from app.models.system import SysUser, SysLog, SysMenu, SysElement, SysRolePermission
# 确保借还模型被加载
from app.models.transaction import TransBorrow, TransRepair, TransScrap

View File

@ -2,10 +2,56 @@
from flask import Blueprint, request, jsonify, current_app
from flask_jwt_extended import jwt_required, get_jwt
from app.services.auth_service import AuthService
from app.utils.decorators import permission_required
auth_bp = Blueprint('auth', __name__)
# ==============================================================================
# 辅助函数:获取当前用户的完整权限列表(基于角色查询)
# ==============================================================================
def get_current_user_permissions():
"""
返回当前用户拥有的所有权限码列表(包括菜单和元素)
此函数根据角色查询数据库得到权限。
"""
claims = get_jwt()
user_role = claims.get('role')
if not user_role:
return []
# 超级管理员返回所有字段权限
if user_role == 'super_admin':
return ['system_user:*']
perm_dict = AuthService.get_user_permissions(user_role)
# 合并菜单和元素权限
perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
return perms
def filter_item_by_permissions(item_dict, user_permissions):
"""
根据用户权限过滤 item 字典,无权限的字段值置为 None
"""
# 字段名到权限码的映射(与前端 permissionMap 保持一致)
field_to_perm = {
'id': 'system_user:id',
'username': 'system_user:username',
'account_id': 'system_user:account_id',
'email': 'system_user:email',
'department': 'system_user:department',
'role': 'system_user:role',
'status': 'system_user:status',
'created_at': 'system_user:created_at',
}
# 如果用户是超级管理员且有 'system_user:*',则不过滤
if 'system_user:*' in user_permissions:
return item_dict
for field, perm_code in field_to_perm.items():
if field in item_dict and perm_code not in user_permissions:
item_dict[field] = None
return item_dict
@auth_bp.route('/login', methods=['POST'])
def login():
try:
@ -34,6 +80,7 @@ def login():
@auth_bp.route('/user/create', methods=['POST'])
@jwt_required()
@permission_required('system_user:operation')
def create_user():
try:
data = request.get_json()
@ -51,6 +98,7 @@ def create_user():
# [新增] 更新用户
@auth_bp.route('/user/<int:user_id>', methods=['PUT'])
@jwt_required()
@permission_required('system_user:operation')
def update_user(user_id):
try:
data = request.get_json()
@ -67,10 +115,14 @@ def update_user(user_id):
@auth_bp.route('/users', methods=['GET'])
@jwt_required()
@permission_required('system_user')
def get_users():
try:
users = AuthService.get_all_users()
return jsonify({'msg': '获取成功', 'data': users}), 200
# 字段级脱敏
user_permissions = get_current_user_permissions()
filtered_users = [filter_item_by_permissions(user, user_permissions) for user in users]
return jsonify({'msg': '获取成功', 'data': filtered_users}), 200
except Exception as e:
current_app.logger.error(f"Get Users Failed: {str(e)}")
return jsonify({'msg': '获取用户列表失败'}), 500
@ -78,6 +130,7 @@ def get_users():
@auth_bp.route('/user/<int:user_id>', methods=['DELETE'])
@jwt_required()
@permission_required('system_user:operation')
def delete_user(user_id):
try:
claims = get_jwt()
@ -88,3 +141,20 @@ def delete_user(user_id):
except Exception as e:
current_app.logger.error(f"Delete User Failed: {str(e)}")
return jsonify({'msg': str(e)}), 400
@auth_bp.route('/my-permissions', methods=['GET'])
@jwt_required()
def get_my_permissions():
"""获取当前登录用户的权限列表"""
try:
claims = get_jwt()
role = claims.get('role')
# 调用 Service 获取权限
permissions = AuthService.get_user_permissions(role)
return jsonify({'msg': '获取成功', 'data': permissions}), 200
except Exception as e:
current_app.logger.error(f"Get Permissions Failed: {str(e)}")
return jsonify({'msg': '获取权限失败'}), 500

View File

@ -3,15 +3,61 @@ from app.services.bom_service import BomService
from app.models.base import MaterialBase
from app.models.bom import BomTable
from app.extensions import db
from flask_jwt_extended import jwt_required
from flask_jwt_extended import jwt_required, get_jwt
from app.utils.decorators import permission_required
from app.services.auth_service import AuthService
bom_bp = Blueprint('bom', __name__)
# ==============================================================================
# 辅助函数:获取当前用户的完整权限列表(基于角色查询)
# ==============================================================================
def get_current_user_permissions():
"""
返回当前用户拥有的所有权限码列表(包括菜单和元素)
此函数根据角色查询数据库得到权限。
"""
claims = get_jwt()
user_role = claims.get('role')
if not user_role:
return []
# 超级管理员返回所有字段权限
if user_role == 'super_admin':
return ['bom_manage:*']
perm_dict = AuthService.get_user_permissions(user_role)
# 合并菜单和元素权限
perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
return perms
def filter_item_by_permissions(item_dict, user_permissions):
"""
根据用户权限过滤 item 字典,无权限的字段值置为 None
"""
# 字段名到权限码的映射(与前端 permissionMap 保持一致)
field_to_perm = {
'bom_no': 'bom_manage:bom_no',
'parent_name': 'bom_manage:parent_name',
'parent_spec': 'bom_manage:parent_spec',
'version': 'bom_manage:version',
'is_enabled': 'bom_manage:status',
'child_count': 'bom_manage:child_count',
}
# 如果用户是超级管理员且有 'bom_manage:*',则不过滤
if 'bom_manage:*' in user_permissions:
return item_dict
for field, perm_code in field_to_perm.items():
if field in item_dict and perm_code not in user_permissions:
item_dict[field] = None
return item_dict
# ==================== 新版 BOM 接口(基于 bom_no ====================
@bom_bp.route('/list', methods=['GET'])
@jwt_required()
@permission_required('bom_manage')
def get_bom_list():
"""获取所有 BOM 配方列表,支持 keyword 搜索和 active_only 过滤"""
try:
@ -20,6 +66,10 @@ def get_bom_list():
active_only = request.args.get('active_only', 'false').lower() == 'true'
data = BomService.get_bom_list(keyword=keyword, active_only=active_only)
# 字段级脱敏
user_permissions = get_current_user_permissions()
if isinstance(data, list):
data = [filter_item_by_permissions(item, user_permissions) for item in data]
return jsonify({
'code': 200,
'msg': 'success',
@ -32,6 +82,7 @@ def get_bom_list():
@bom_bp.route('/detail/<bom_no>', methods=['GET'])
@jwt_required()
@permission_required('bom_manage')
def get_bom_detail(bom_no):
"""
根据 BOM 编号获取配方详情
@ -42,6 +93,9 @@ def get_bom_detail(bom_no):
data = BomService.get_bom_detail(bom_no, version=version)
if not data:
return jsonify({'code': 404, 'msg': 'BOM 不存在'}), 404
# 字段级脱敏
user_permissions = get_current_user_permissions()
data = filter_item_by_permissions(data, user_permissions)
return jsonify({
'code': 200,
'msg': 'success',
@ -54,6 +108,7 @@ def get_bom_detail(bom_no):
@bom_bp.route('/save', methods=['POST'])
@jwt_required()
@permission_required('bom_manage:operation')
def save_bom():
"""保存或更新 BOM 配方(支持自定义 bom_no 和 多版本)"""
try:
@ -81,12 +136,16 @@ def save_bom():
@bom_bp.route('/stock/<bom_no>', methods=['GET'])
@jwt_required()
@permission_required('bom_manage')
def get_bom_with_stock_by_no(bom_no):
"""根据 BOM 编号获取配方详情及库存信息"""
try:
data = BomService.get_bom_with_stock_by_bom_no(bom_no)
if not data:
return jsonify({'code': 404, 'msg': 'BOM 不存在'}), 404
# 字段级脱敏
user_permissions = get_current_user_permissions()
data = filter_item_by_permissions(data, user_permissions)
return jsonify({
'code': 200,
'msg': 'success',
@ -101,6 +160,7 @@ def get_bom_with_stock_by_no(bom_no):
@bom_bp.route('/<bom_no>', methods=['DELETE'])
@jwt_required()
@permission_required('bom_manage:operation')
def delete_bom(bom_no):
"""
根据 BOM 编号删除
@ -133,9 +193,13 @@ def delete_bom(bom_no):
@bom_bp.route('/<int:parent_id>', methods=['GET'])
@jwt_required()
@permission_required('bom_manage')
def get_bom(parent_id):
try:
data = BomService.get_bom_with_stock(parent_id)
# 字段级脱敏
user_permissions = get_current_user_permissions()
data = filter_item_by_permissions(data, user_permissions)
return jsonify({
'code': 200,
'msg': 'success',
@ -148,6 +212,7 @@ def get_bom(parent_id):
@bom_bp.route('', methods=['POST'])
@jwt_required()
@permission_required('bom_manage:operation')
def save_bom_legacy():
try:
req_data = request.get_json()
@ -169,11 +234,14 @@ def save_bom_legacy():
@bom_bp.route('/base/list', methods=['GET'])
@jwt_required()
@permission_required('bom_manage')
def get_material_base_list():
"""获取所有基础物料列表,用于前端下拉框"""
try:
materials = MaterialBase.query.filter_by(is_enabled=True).order_by(MaterialBase.id.desc()).all()
data = [item.to_dict() for item in materials]
# 字段级脱敏 (如果需要,但此接口通常用于下拉选择,可能不需要脱敏)
# 保持原样
return jsonify({
'code': 200,
'msg': 'success',
@ -186,12 +254,16 @@ def get_material_base_list():
@bom_bp.route('/parents', methods=['GET'])
@jwt_required()
@permission_required('bom_manage')
def get_bom_parents():
"""获取所有已定义BOM的父件物料列表兼容旧版"""
try:
subq = db.session.query(BomTable.parent_id).distinct().subquery()
parents = MaterialBase.query.join(subq, MaterialBase.id == subq.c.parent_id).all()
data = [item.to_dict() for item in parents]
# 字段级脱敏 (如果需要)
user_permissions = get_current_user_permissions()
data = [filter_item_by_permissions(item, user_permissions) for item in data]
return jsonify({
'code': 200,
'msg': 'success',

View File

@ -1,22 +1,77 @@
# 文件路径: app/api/v1/inbound/base.py
from flask import Blueprint, request, jsonify, send_file
from flask import Blueprint, request, jsonify, send_file, g
from app.services.inbound.base_service import MaterialBaseService
from app.utils.decorators import login_required, permission_required
import traceback
import datetime
inbound_base_bp = Blueprint('stock_base', __name__)
# ==============================================================================
# 辅助函数:获取当前用户的完整权限列表(基于角色查询)
# ==============================================================================
def get_current_user_permissions():
"""
返回当前用户拥有的所有权限码列表(包括菜单和元素)
此函数根据角色查询数据库得到权限。
"""
from flask_jwt_extended import get_jwt
from app.services.auth_service import AuthService
claims = get_jwt()
user_role = claims.get('role')
if not user_role:
return []
# 超级管理员返回所有字段权限
if user_role == 'super_admin':
return ['material_list:id', 'material_list:companyName', 'material_list:name', 'material_list:commonName', 'material_list:category', 'material_list:type',
'material_list:spec', 'material_list:unit', 'material_list:inventoryCount', 'material_list:availableCount', 'material_list:files', 'material_list:isEnabled']
perm_dict = AuthService.get_user_permissions(user_role)
# 合并菜单和元素权限
perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
return perms
def filter_item_by_permissions(item_dict, user_permissions):
"""
根据用户权限过滤 item 字典,无权限的字段值置为 None
"""
# 字段名到权限码的映射(与前端 permissionMap 保持一致)
field_to_perm = {
'id': 'material_list:id',
'companyName': 'material_list:companyName',
'name': 'material_list:name',
'commonName': 'material_list:commonName',
'category': 'material_list:category',
'type': 'material_list:type',
'spec': 'material_list:spec',
'unit': 'material_list:unit',
'inventoryCount': 'material_list:inventoryCount',
'availableCount': 'material_list:availableCount',
'generalManual': 'material_list:files',
'generalImage': 'material_list:files',
'isEnabled': 'material_list:isEnabled'
}
for field, perm_code in field_to_perm.items():
if field in item_dict and perm_code not in user_permissions:
item_dict[field] = None
return item_dict
# ==============================================================================
# 1. 搜索接口 (GET /api/v1/inbound/base/search)
# ==============================================================================
@inbound_base_bp.route('/search', methods=['GET'])
@permission_required('material_list')
def search_base():
try:
keyword = request.args.get('keyword', '')
data = MaterialBaseService.search_material(keyword)
return jsonify({"code": 200, "msg": "success", "data": data})
# 字段级脱敏
user_permissions = get_current_user_permissions()
filtered_data = [filter_item_by_permissions(item, user_permissions) for item in data]
return jsonify({"code": 200, "msg": "success", "data": filtered_data})
except Exception as e:
traceback.print_exc()
return jsonify({"code": 500, "msg": str(e)}), 500
@ -26,6 +81,7 @@ def search_base():
# 2. 列表接口 (GET /api/v1/inbound/base/list)
# ==============================================================================
@inbound_base_bp.route('/list', methods=['GET'])
@permission_required('material_list')
def get_list():
try:
page = request.args.get('pageNum', 1, type=int)
@ -41,6 +97,10 @@ def get_list():
}
result = MaterialBaseService.get_list(page, limit, filters)
# 字段级脱敏
user_permissions = get_current_user_permissions()
if result.get('items'):
result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']]
return jsonify({"code": 200, "msg": "success", "data": result})
except Exception as e:
traceback.print_exc()
@ -51,6 +111,7 @@ def get_list():
# 2.1 选项接口 (GET /api/v1/inbound/base/options)
# ==============================================================================
@inbound_base_bp.route('/options', methods=['GET'])
@permission_required('material_list')
def get_options():
try:
data = MaterialBaseService.get_distinct_options()
@ -64,6 +125,7 @@ def get_options():
# 2.2 导出接口 (GET /api/v1/inbound/base/export)
# ==============================================================================
@inbound_base_bp.route('/export', methods=['GET'])
@permission_required('material_list')
def export_data():
try:
# 获取筛选条件
@ -101,13 +163,44 @@ def export_data():
# 3. 新增接口 (POST /api/v1/inbound/base/)
# ==============================================================================
@inbound_base_bp.route('/', methods=['POST'])
@permission_required('material_list:operation')
def create():
try:
data = request.get_json()
if not data:
return jsonify({"code": 400, "msg": "No data provided"}), 400
MaterialBaseService.create_material(data)
# 获取当前用户权限
user_permissions = get_current_user_permissions()
# 字段到权限码的映射(与 filter_item_by_permissions 一致)
field_to_perm = {
'id': 'material_list:id',
'companyName': 'material_list:companyName',
'name': 'material_list:name',
'commonName': 'material_list:commonName',
'category': 'material_list:category',
'type': 'material_list:type',
'spec': 'material_list:spec',
'unit': 'material_list:unit',
'inventoryCount': 'material_list:inventoryCount',
'availableCount': 'material_list:availableCount',
'generalManual': 'material_list:files',
'generalImage': 'material_list:files',
'isEnabled': 'material_list:isEnabled'
}
# 过滤用户没有权限的字段
filtered_data = {}
for key, value in data.items():
if key in field_to_perm:
perm_code = field_to_perm[key]
if perm_code in user_permissions:
filtered_data[key] = value
# 没有权限则跳过,不包含在 filtered_data 中
else:
# 不在映射中的字段,默认允许(例如 visibilityLevel
filtered_data[key] = value
MaterialBaseService.create_material(filtered_data)
return jsonify({"code": 200, "msg": "新增成功"})
except ValueError as e:
# 捕获业务逻辑验证错误 (如名称为空)
@ -122,10 +215,41 @@ def create():
# 4. 修改接口 (PUT /api/v1/inbound/base/<id>)
# ==============================================================================
@inbound_base_bp.route('/<int:id>', methods=['PUT'])
@permission_required('material_list:operation')
def update(id):
try:
data = request.get_json()
MaterialBaseService.update_material(id, data)
# 获取当前用户权限
user_permissions = get_current_user_permissions()
# 字段到权限码的映射(与 filter_item_by_permissions 一致)
field_to_perm = {
'id': 'material_list:id',
'companyName': 'material_list:companyName',
'name': 'material_list:name',
'commonName': 'material_list:commonName',
'category': 'material_list:category',
'type': 'material_list:type',
'spec': 'material_list:spec',
'unit': 'material_list:unit',
'inventoryCount': 'material_list:inventoryCount',
'availableCount': 'material_list:availableCount',
'generalManual': 'material_list:files',
'generalImage': 'material_list:files',
'isEnabled': 'material_list:isEnabled'
}
# 过滤用户没有权限的字段
filtered_data = {}
for key, value in data.items():
if key in field_to_perm:
perm_code = field_to_perm[key]
if perm_code in user_permissions:
filtered_data[key] = value
# 没有权限则跳过,不包含在 filtered_data 中
else:
# 不在映射中的字段,默认允许(例如 visibilityLevel
filtered_data[key] = value
# 使用过滤后的数据调用服务
MaterialBaseService.update_material(id, filtered_data)
return jsonify({"code": 200, "msg": "修改成功"})
except Exception as e:
traceback.print_exc()
@ -136,6 +260,7 @@ def update(id):
# 5. 删除接口 (DELETE /api/v1/inbound/base/<id>)
# ==============================================================================
@inbound_base_bp.route('/<int:id>', methods=['DELETE'])
@permission_required('material_list:operation')
def delete(id):
try:
MaterialBaseService.delete_material(id)

View File

@ -1,14 +1,89 @@
from flask import Blueprint, request, jsonify
from app.services.inbound.buy_service import BuyInboundService
from app.utils.decorators import permission_required
import traceback
inbound_buy_bp = Blueprint('stock_buy', __name__)
# ==============================================================================
# 辅助函数:获取当前用户的完整权限列表(基于角色查询)
# ==============================================================================
def get_current_user_permissions():
"""
返回当前用户拥有的所有权限码列表(包括菜单和元素)
此函数根据角色查询数据库得到权限。
"""
from flask_jwt_extended import get_jwt
from app.services.auth_service import AuthService
claims = get_jwt()
user_role = claims.get('role')
if not user_role:
return []
# 超级管理员返回所有字段权限
if user_role == 'super_admin':
# 返回所有以 inbound_buy: 开头的权限码(这里我们返回一个特殊标记,表示全部)
# 为了简单,我们返回 ['inbound_buy:*'],在过滤函数中特殊处理
return ['inbound_buy:*']
perm_dict = AuthService.get_user_permissions(user_role)
# 合并菜单和元素权限
perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
return perms
def filter_item_by_permissions(item_dict, user_permissions):
"""
根据用户权限过滤 item 字典,无权限的字段值置为 None
"""
# 字段名到权限码的映射(与前端 permissionMap 保持一致)
field_to_perm = {
'id': 'inbound_buy:id',
'base_id': 'inbound_buy:base_id',
'global_print_id': 'inbound_buy:global_print_id',
'sku': 'inbound_buy:sku',
'barcode': 'inbound_buy:barcode',
'in_date': 'inbound_buy:in_date',
'serial_number': 'inbound_buy:serial_number',
'batch_number': 'inbound_buy:batch_number',
'status': 'inbound_buy:status',
'in_quantity': 'inbound_buy:in_quantity',
'stock_quantity': 'inbound_buy:stock_quantity',
'available_quantity': 'inbound_buy:available_quantity',
'inspection_status': 'inbound_buy:inspection_status',
'warehouse_location': 'inbound_buy:warehouse_location',
'unit_price': 'inbound_buy:unit_price',
'tax_rate': 'inbound_buy:tax_rate',
'total_price': 'inbound_buy:total_price',
'currency': 'inbound_buy:currency',
'exchange_rate': 'inbound_buy:exchange_rate',
'supplier_name': 'inbound_buy:supplier_name',
'buyer_name': 'inbound_buy:buyer_name',
'buyer_email': 'inbound_buy:buyer_email',
'original_link': 'inbound_buy:original_link',
'detail_link': 'inbound_buy:detail_link',
'arrival_photo': 'inbound_buy:arrival_photo',
'inspection_report': 'inbound_buy:inspection_report',
'material_name': 'inbound_buy:material_name',
'spec_model': 'inbound_buy:spec_model',
'category': 'inbound_buy:category',
'unit': 'inbound_buy:unit',
'material_type': 'inbound_buy:material_type',
'company_name': 'inbound_buy:company_name',
}
# 如果用户是超级管理员且有 'inbound_buy:*',则不过滤
if 'inbound_buy:*' in user_permissions:
return item_dict
for field, perm_code in field_to_perm.items():
if field in item_dict and perm_code not in user_permissions:
item_dict[field] = None
return item_dict
# ------------------------------------------------------------------
# 0. 基础物料搜索
# ------------------------------------------------------------------
@inbound_buy_bp.route('/search-base', methods=['GET'])
@permission_required('inbound_buy')
def search_base():
try:
keyword = request.args.get('keyword', '')
@ -33,6 +108,7 @@ def search_base():
# 1. 获取列表 (修改:接收 category 和 material_type)
# ------------------------------------------------------------------
@inbound_buy_bp.route('/list', methods=['GET'])
@permission_required('inbound_buy')
def get_list():
try:
page = request.args.get('page', 1, type=int)
@ -48,6 +124,10 @@ def get_list():
statuses = statuses_str.split(',') if statuses_str else []
result = BuyInboundService.get_list(page, limit, keyword, statuses, category, material_type)
# 字段级脱敏
user_permissions = get_current_user_permissions()
if result.get('items'):
result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']]
return jsonify({"code": 200, "msg": "success", "data": result})
except Exception as e:
traceback.print_exc()
@ -58,6 +138,7 @@ def get_list():
# 2. 新增入库
# ------------------------------------------------------------------
@inbound_buy_bp.route('/submit', methods=['POST'])
@permission_required('inbound_buy:operation')
def submit():
try:
data = request.get_json()
@ -80,6 +161,7 @@ def submit():
# 3. 更新入库
# ------------------------------------------------------------------
@inbound_buy_bp.route('/<int:id>', methods=['PUT'])
@permission_required('inbound_buy:operation')
def update_buy(id):
try:
data = request.get_json()
@ -93,6 +175,7 @@ def update_buy(id):
# 4. 删除
# ------------------------------------------------------------------
@inbound_buy_bp.route('/<int:id>', methods=['DELETE'])
@permission_required('inbound_buy:operation')
def delete_buy(id):
try:
BuyInboundService.delete_inbound(id)
@ -105,6 +188,7 @@ def delete_buy(id):
# 5. [新增] 获取筛选下拉选项 (修复404的关键)
# ------------------------------------------------------------------
@inbound_buy_bp.route('/options', methods=['GET'])
@permission_required('inbound_buy')
def get_options():
try:
data = BuyInboundService.get_filter_options()
@ -117,6 +201,7 @@ def get_options():
# 6. 获取关联的出库历史 (如果有)
# ------------------------------------------------------------------
@inbound_buy_bp.route('/<int:id>/history', methods=['GET'])
@permission_required('inbound_buy')
def get_history(id):
# 如果没有出库模块,这个接口可能为空,但为保持兼容性保留
return jsonify({"code": 200, "msg": "success", "data": []})
@ -126,6 +211,7 @@ def get_history(id):
# 7. 供应商建议
# ------------------------------------------------------------------
@inbound_buy_bp.route('/suggestions/suppliers', methods=['GET'])
@permission_required('inbound_buy')
def get_supplier_suggestions():
base_id = request.args.get('base_id', type=int)
if not base_id:
@ -138,6 +224,7 @@ def get_supplier_suggestions():
# 8. 采购人建议 (全局)
# ------------------------------------------------------------------
@inbound_buy_bp.route('/suggestions/users', methods=['GET'])
@permission_required('inbound_buy')
def get_user_suggestions():
keyword = request.args.get('keyword', '')
data = BuyInboundService.get_history_purchasers(keyword)
@ -148,6 +235,7 @@ def get_user_suggestions():
# 9. 链接建议
# ------------------------------------------------------------------
@inbound_buy_bp.route('/suggestions/links', methods=['GET'])
@permission_required('inbound_buy')
def get_link_suggestions():
base_id = request.args.get('base_id', type=int)
link_type = request.args.get('type', 'original') # original or detail
@ -161,6 +249,7 @@ def get_link_suggestions():
# 10. 库位建议
# ------------------------------------------------------------------
@inbound_buy_bp.route('/suggestions/locations', methods=['GET'])
@permission_required('inbound_buy')
def get_location_suggestions():
base_id = request.args.get('base_id', type=int)
if not base_id:

View File

@ -1,15 +1,90 @@
# inventory-backend/app/api/v1/inbound/product.py
from flask import Blueprint, request, jsonify
from app.services.inbound.product_service import ProductInboundService
from app.utils.decorators import permission_required
import traceback
inbound_product_bp = Blueprint('stock_product', __name__)
# ==============================================================================
# 辅助函数:获取当前用户的完整权限列表(基于角色查询)
# ==============================================================================
def get_current_user_permissions():
"""
返回当前用户拥有的所有权限码列表(包括菜单和元素)
此函数根据角色查询数据库得到权限。
"""
from flask_jwt_extended import get_jwt
from app.services.auth_service import AuthService
claims = get_jwt()
user_role = claims.get('role')
if not user_role:
return []
# 超级管理员返回所有字段权限
if user_role == 'super_admin':
# 返回所有以 inbound_product: 开头的权限码(这里我们返回一个特殊标记,表示全部)
# 为了简单,我们返回 ['inbound_product:*'],在过滤函数中特殊处理
return ['inbound_product:*']
perm_dict = AuthService.get_user_permissions(user_role)
# 合并菜单和元素权限
perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
return perms
def filter_item_by_permissions(item_dict, user_permissions):
"""
根据用户权限过滤 item 字典,无权限的字段值置为 None
"""
# 字段名到权限码的映射(与前端 permissionMap 保持一致)
field_to_perm = {
'id': 'inbound_product:id',
'base_id': 'inbound_product:base_id',
'company_name': 'inbound_product:company_name',
'material_name': 'inbound_product:material_name',
'category': 'inbound_product:category',
'material_type': 'inbound_product:material_type',
'spec_model': 'inbound_product:spec_model',
'unit': 'inbound_product:unit',
'sku': 'inbound_product:sku',
'inbound_date': 'inbound_product:inbound_date',
'barcode': 'inbound_product:barcode',
'serial_number': 'inbound_product:serial_number',
'status': 'inbound_product:status',
'quality_status': 'inbound_product:quality_status',
'in_quantity': 'inbound_product:in_quantity',
'stock_quantity': 'inbound_product:stock_quantity',
'available_quantity': 'inbound_product:available_quantity',
'warehouse_location': 'inbound_product:warehouse_location',
'bom_code': 'inbound_product:bom_code',
'bom_version': 'inbound_product:bom_version',
'work_order_code': 'inbound_product:work_order_code',
'order_id': 'inbound_product:order_id',
'production_manager': 'inbound_product:production_manager',
'production_start_time': 'inbound_product:production_start_time',
'production_end_time': 'inbound_product:production_end_time',
'raw_material_cost': 'inbound_product:raw_material_cost',
'manual_cost': 'inbound_product:manual_cost',
'sale_price': 'inbound_product:sale_price',
'product_photo': 'inbound_product:product_photo',
'quality_report_link': 'inbound_product:quality_report_link',
'inspection_report_link': 'inbound_product:inspection_report_link',
'detail_link': 'inbound_product:detail_link',
}
# 如果用户是超级管理员且有 'inbound_product:*',则不过滤
if 'inbound_product:*' in user_permissions:
return item_dict
for field, perm_code in field_to_perm.items():
if field in item_dict and perm_code not in user_permissions:
item_dict[field] = None
return item_dict
# ------------------------------------------------------------------
# 0. 基础物料搜索 (关键接口:配合 Service 实现自动回填)
# ------------------------------------------------------------------
@inbound_product_bp.route('/search-base', methods=['GET'])
@permission_required('inbound_product')
def search_base():
"""
对应前端 API: /inbound/product/search-base
@ -19,7 +94,10 @@ def search_base():
keyword = request.args.get('keyword', '')
# 调用 Service 层已修复的 search_base_material 方法
data = ProductInboundService.search_base_material(keyword)
return jsonify({"code": 200, "msg": "success", "data": data})
# 字段级脱敏
user_permissions = get_current_user_permissions()
filtered_data = [filter_item_by_permissions(item, user_permissions) for item in data]
return jsonify({"code": 200, "msg": "success", "data": filtered_data})
except Exception as e:
# 捕获异常并打印堆栈,方便调试
traceback.print_exc()
@ -29,6 +107,7 @@ def search_base():
# 0.5 [新增] BOM 搜索接口
# ------------------------------------------------------------------
@inbound_product_bp.route('/search-bom', methods=['GET'])
@permission_required('inbound_product')
def search_bom():
"""
供前端下拉框远程搜索使用 (搜索BOM)
@ -50,6 +129,7 @@ def search_bom():
# 1. 获取列表 (支持 status 多选筛选)
# ------------------------------------------------------------------
@inbound_product_bp.route('/list', methods=['GET'])
@permission_required('inbound_product')
def get_list():
try:
page = request.args.get('page', 1, type=int)
@ -61,6 +141,10 @@ def get_list():
statuses = statuses_str.split(',') if statuses_str else []
result = ProductInboundService.get_list(page, limit, keyword, statuses)
# 字段级脱敏
user_permissions = get_current_user_permissions()
if result.get('items'):
result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']]
return jsonify({"code": 200, "msg": "success", "data": result})
except Exception as e:
traceback.print_exc()
@ -71,10 +155,59 @@ def get_list():
# 2. 新增入库
# ------------------------------------------------------------------
@inbound_product_bp.route('/submit', methods=['POST'])
@permission_required('inbound_product:operation')
def submit():
try:
data = request.get_json()
if not data:
return jsonify({"code": 400, "msg": "No data"}), 400
# 数据清洗:移除用户没有权限的字段
user_permissions = get_current_user_permissions()
# 超级管理员不过滤
if 'inbound_product:*' not in user_permissions:
field_to_perm = {
'id': 'inbound_product:id',
'base_id': 'inbound_product:base_id',
'company_name': 'inbound_product:company_name',
'material_name': 'inbound_product:material_name',
'category': 'inbound_product:category',
'material_type': 'inbound_product:material_type',
'spec_model': 'inbound_product:spec_model',
'unit': 'inbound_product:unit',
'sku': 'inbound_product:sku',
'inbound_date': 'inbound_product:inbound_date',
'barcode': 'inbound_product:barcode',
'serial_number': 'inbound_product:serial_number',
'status': 'inbound_product:status',
'quality_status': 'inbound_product:quality_status',
'in_quantity': 'inbound_product:in_quantity',
'stock_quantity': 'inbound_product:stock_quantity',
'available_quantity': 'inbound_product:available_quantity',
'warehouse_location': 'inbound_product:warehouse_location',
'bom_code': 'inbound_product:bom_code',
'bom_version': 'inbound_product:bom_version',
'work_order_code': 'inbound_product:work_order_code',
'order_id': 'inbound_product:order_id',
'production_manager': 'inbound_product:production_manager',
'production_start_time': 'inbound_product:production_start_time',
'production_end_time': 'inbound_product:production_end_time',
'raw_material_cost': 'inbound_product:raw_material_cost',
'manual_cost': 'inbound_product:manual_cost',
'sale_price': 'inbound_product:sale_price',
'product_photo': 'inbound_product:product_photo',
'quality_report_link': 'inbound_product:quality_report_link',
'inspection_report_link': 'inbound_product:inspection_report_link',
'detail_link': 'inbound_product:detail_link',
}
# 复制一份,避免遍历时修改字典
for field in list(data.keys()):
perm_code = field_to_perm.get(field)
if perm_code and perm_code not in user_permissions:
data.pop(field, None)
# 调用 Service 处理入库,获取新创建的对象
new_stock = ProductInboundService.handle_inbound(request.get_json())
new_stock = ProductInboundService.handle_inbound(data)
# 返回成功信息以及新创建的数据包含生成的ID和SKU供前端自动打印使用
return jsonify({
@ -91,9 +224,55 @@ def submit():
# 3. 更新入库
# ------------------------------------------------------------------
@inbound_product_bp.route('/<int:id>', methods=['PUT'])
@permission_required('inbound_product:operation')
def update(id):
try:
ProductInboundService.update_inbound(id, request.get_json())
data = request.get_json()
# 数据清洗:移除用户没有权限的字段
user_permissions = get_current_user_permissions()
# 超级管理员不过滤
if 'inbound_product:*' not in user_permissions:
field_to_perm = {
'id': 'inbound_product:id',
'base_id': 'inbound_product:base_id',
'company_name': 'inbound_product:company_name',
'material_name': 'inbound_product:material_name',
'category': 'inbound_product:category',
'material_type': 'inbound_product:material_type',
'spec_model': 'inbound_product:spec_model',
'unit': 'inbound_product:unit',
'sku': 'inbound_product:sku',
'inbound_date': 'inbound_product:inbound_date',
'barcode': 'inbound_product:barcode',
'serial_number': 'inbound_product:serial_number',
'status': 'inbound_product:status',
'quality_status': 'inbound_product:quality_status',
'in_quantity': 'inbound_product:in_quantity',
'stock_quantity': 'inbound_product:stock_quantity',
'available_quantity': 'inbound_product:available_quantity',
'warehouse_location': 'inbound_product:warehouse_location',
'bom_code': 'inbound_product:bom_code',
'bom_version': 'inbound_product:bom_version',
'work_order_code': 'inbound_product:work_order_code',
'order_id': 'inbound_product:order_id',
'production_manager': 'inbound_product:production_manager',
'production_start_time': 'inbound_product:production_start_time',
'production_end_time': 'inbound_product:production_end_time',
'raw_material_cost': 'inbound_product:raw_material_cost',
'manual_cost': 'inbound_product:manual_cost',
'sale_price': 'inbound_product:sale_price',
'product_photo': 'inbound_product:product_photo',
'quality_report_link': 'inbound_product:quality_report_link',
'inspection_report_link': 'inbound_product:inspection_report_link',
'detail_link': 'inbound_product:detail_link',
}
# 复制一份,避免遍历时修改字典
for field in list(data.keys()):
perm_code = field_to_perm.get(field)
if perm_code and perm_code not in user_permissions:
data.pop(field, None)
ProductInboundService.update_inbound(id, data)
return jsonify({"code": 200, "msg": "更新成功"})
except Exception as e:
traceback.print_exc()
@ -104,6 +283,7 @@ def update(id):
# 4. 删除
# ------------------------------------------------------------------
@inbound_product_bp.route('/<int:id>', methods=['DELETE'])
@permission_required('inbound_product:operation')
def delete(id):
try:
ProductInboundService.delete_inbound(id)
@ -117,6 +297,7 @@ def delete(id):
# 5. 获取出库历史
# ------------------------------------------------------------------
@inbound_product_bp.route('/<int:id>/history', methods=['GET'])
@permission_required('inbound_product')
def get_history(id):
try:
data = ProductInboundService.get_outbound_history(id)
@ -130,6 +311,7 @@ def get_history(id):
# 6. 系统用户建议
# ------------------------------------------------------------------
@inbound_product_bp.route('/suggestions/users', methods=['GET'])
@permission_required('inbound_product')
def get_user_suggestions():
keyword = request.args.get('keyword', '')
data = ProductInboundService.search_system_users(keyword)
@ -140,6 +322,7 @@ def get_user_suggestions():
# 7. 获取筛选选项
# ------------------------------------------------------------------
@inbound_product_bp.route('/options', methods=['GET'])
@permission_required('inbound_product')
def get_options():
try:
data = ProductInboundService.get_filter_options()

View File

@ -1,16 +1,90 @@
# inventory-backend/app/api/v1/inbound/semi.py
from flask import Blueprint, request, jsonify
from app.services.inbound.semi_service import SemiInboundService
from app.utils.decorators import permission_required
import traceback
# 定义蓝图
inbound_semi_bp = Blueprint('stock_semi', __name__)
# ==============================================================================
# 辅助函数:获取当前用户的完整权限列表(基于角色查询)
# ==============================================================================
def get_current_user_permissions():
"""
返回当前用户拥有的所有权限码列表(包括菜单和元素)
此函数根据角色查询数据库得到权限。
"""
from flask_jwt_extended import get_jwt
from app.services.auth_service import AuthService
claims = get_jwt()
user_role = claims.get('role')
if not user_role:
return []
# 超级管理员返回所有字段权限
if user_role == 'super_admin':
# 返回所有以 inbound_semi: 开头的权限码(这里我们返回一个特殊标记,表示全部)
# 为了简单,我们返回 ['inbound_semi:*'],在过滤函数中特殊处理
return ['inbound_semi:*']
perm_dict = AuthService.get_user_permissions(user_role)
# 合并菜单和元素权限
perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
return perms
def filter_item_by_permissions(item_dict, user_permissions):
"""
根据用户权限过滤 item 字典,无权限的字段值置为 None
"""
# 字段名到权限码的映射(与前端 permissionMap 保持一致)
field_to_perm = {
'id': 'inbound_semi:id',
'base_id': 'inbound_semi:base_id',
'company_name': 'inbound_semi:company_name',
'material_name': 'inbound_semi:material_name',
'category': 'inbound_semi:category',
'material_type': 'inbound_semi:material_type',
'spec_model': 'inbound_semi:spec_model',
'unit': 'inbound_semi:unit',
'sku': 'inbound_semi:sku',
'inbound_date': 'inbound_semi:inbound_date',
'barcode': 'inbound_semi:barcode',
'serial_number': 'inbound_semi:serial_number',
'batch_number': 'inbound_semi:batch_number',
'status': 'inbound_semi:status',
'quality_status': 'inbound_semi:quality_status',
'in_quantity': 'inbound_semi:in_quantity',
'stock_quantity': 'inbound_semi:stock_quantity',
'available_quantity': 'inbound_semi:available_quantity',
'warehouse_location': 'inbound_semi:warehouse_location',
'bom_code': 'inbound_semi:bom_code',
'bom_version': 'inbound_semi:bom_version',
'work_order_code': 'inbound_semi:work_order_code',
'raw_material_cost': 'inbound_semi:raw_material_cost',
'manual_cost': 'inbound_semi:manual_cost',
'unit_total_cost': 'inbound_semi:unit_total_cost',
'production_manager': 'inbound_semi:production_manager',
'production_start_time': 'inbound_semi:production_start_time',
'production_end_time': 'inbound_semi:production_end_time',
'arrival_photo': 'inbound_semi:arrival_photo',
'quality_report_link': 'inbound_semi:quality_report_link',
'detail_link': 'inbound_semi:detail_link',
}
# 如果用户是超级管理员且有 'inbound_semi:*',则不过滤
if 'inbound_semi:*' in user_permissions:
return item_dict
for field, perm_code in field_to_perm.items():
if field in item_dict and perm_code not in user_permissions:
item_dict[field] = None
return item_dict
# ------------------------------------------------------------------
# 0. 基础物料搜索 (复用逻辑)
# ------------------------------------------------------------------
@inbound_semi_bp.route('/search-base', methods=['GET'])
@permission_required('inbound_semi')
def search_base():
"""
供前端下拉框远程搜索使用 (搜索半成品类型的基础物料)
@ -20,10 +94,13 @@ def search_base():
keyword = request.args.get('keyword', '')
# 这里复用 Service 中的搜索逻辑
data = SemiInboundService.search_base_material(keyword)
# 字段级脱敏
user_permissions = get_current_user_permissions()
filtered_data = [filter_item_by_permissions(item, user_permissions) for item in data]
return jsonify({
"code": 200,
"msg": "success",
"data": data
"data": filtered_data
})
except Exception as e:
traceback.print_exc()
@ -33,6 +110,7 @@ def search_base():
# 0.5 [新增] BOM 搜索接口
# ------------------------------------------------------------------
@inbound_semi_bp.route('/search-bom', methods=['GET'])
@permission_required('inbound_semi')
def search_bom():
"""
供前端下拉框远程搜索使用 (搜索BOM)
@ -55,6 +133,7 @@ def search_bom():
# 1. 获取半成品列表
# ------------------------------------------------------------------
@inbound_semi_bp.route('/list', methods=['GET'])
@permission_required('inbound_semi')
def get_list():
try:
page = request.args.get('page', 1, type=int)
@ -67,6 +146,10 @@ def get_list():
statuses = statuses_str.split(',') if statuses_str else []
result = SemiInboundService.get_list(page, limit, keyword, statuses)
# 字段级脱敏
user_permissions = get_current_user_permissions()
if result.get('items'):
result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']]
return jsonify({"code": 200, "msg": "success", "data": result})
except Exception as e:
traceback.print_exc()
@ -77,12 +160,57 @@ def get_list():
# 2. 新增半成品入库 (修改:返回创建的对象数据)
# ------------------------------------------------------------------
@inbound_semi_bp.route('/submit', methods=['POST'])
@permission_required('inbound_semi:operation')
def submit():
try:
data = request.get_json()
if not data:
return jsonify({"code": 400, "msg": "No data"}), 400
# 数据清洗:移除用户没有权限的字段
user_permissions = get_current_user_permissions()
# 超级管理员不过滤
if 'inbound_semi:*' not in user_permissions:
# 字段名到权限码的映射(与前端 permissionMap 保持一致)
field_to_perm = {
'id': 'inbound_semi:id',
'base_id': 'inbound_semi:base_id',
'company_name': 'inbound_semi:company_name',
'material_name': 'inbound_semi:material_name',
'category': 'inbound_semi:category',
'material_type': 'inbound_semi:material_type',
'spec_model': 'inbound_semi:spec_model',
'unit': 'inbound_semi:unit',
'sku': 'inbound_semi:sku',
'inbound_date': 'inbound_semi:inbound_date',
'barcode': 'inbound_semi:barcode',
'serial_number': 'inbound_semi:serial_number',
'batch_number': 'inbound_semi:batch_number',
'status': 'inbound_semi:status',
'quality_status': 'inbound_semi:quality_status',
'in_quantity': 'inbound_semi:in_quantity',
'stock_quantity': 'inbound_semi:stock_quantity',
'available_quantity': 'inbound_semi:available_quantity',
'warehouse_location': 'inbound_semi:warehouse_location',
'bom_code': 'inbound_semi:bom_code',
'bom_version': 'inbound_semi:bom_version',
'work_order_code': 'inbound_semi:work_order_code',
'raw_material_cost': 'inbound_semi:raw_material_cost',
'manual_cost': 'inbound_semi:manual_cost',
'unit_total_cost': 'inbound_semi:unit_total_cost',
'production_manager': 'inbound_semi:production_manager',
'production_start_time': 'inbound_semi:production_start_time',
'production_end_time': 'inbound_semi:production_end_time',
'arrival_photo': 'inbound_semi:arrival_photo',
'quality_report_link': 'inbound_semi:quality_report_link',
'detail_link': 'inbound_semi:detail_link',
}
# 复制一份,避免遍历时修改字典
for field in list(data.keys()):
perm_code = field_to_perm.get(field)
if perm_code and perm_code not in user_permissions:
data.pop(field, None)
# 修改:调用 Service 处理入库,获取新创建的对象
new_stock = SemiInboundService.handle_inbound(data)
@ -101,9 +229,53 @@ def submit():
# 3. 更新半成品入库信息
# ------------------------------------------------------------------
@inbound_semi_bp.route('/<int:id>', methods=['PUT'])
@permission_required('inbound_semi:operation')
def update_semi(id):
try:
data = request.get_json()
# 数据清洗:移除用户没有权限的字段
user_permissions = get_current_user_permissions()
# 超级管理员不过滤
if 'inbound_semi:*' not in user_permissions:
field_to_perm = {
'id': 'inbound_semi:id',
'base_id': 'inbound_semi:base_id',
'company_name': 'inbound_semi:company_name',
'material_name': 'inbound_semi:material_name',
'category': 'inbound_semi:category',
'material_type': 'inbound_semi:material_type',
'spec_model': 'inbound_semi:spec_model',
'unit': 'inbound_semi:unit',
'sku': 'inbound_semi:sku',
'inbound_date': 'inbound_semi:inbound_date',
'barcode': 'inbound_semi:barcode',
'serial_number': 'inbound_semi:serial_number',
'batch_number': 'inbound_semi:batch_number',
'status': 'inbound_semi:status',
'quality_status': 'inbound_semi:quality_status',
'in_quantity': 'inbound_semi:in_quantity',
'stock_quantity': 'inbound_semi:stock_quantity',
'available_quantity': 'inbound_semi:available_quantity',
'warehouse_location': 'inbound_semi:warehouse_location',
'bom_code': 'inbound_semi:bom_code',
'bom_version': 'inbound_semi:bom_version',
'work_order_code': 'inbound_semi:work_order_code',
'raw_material_cost': 'inbound_semi:raw_material_cost',
'manual_cost': 'inbound_semi:manual_cost',
'unit_total_cost': 'inbound_semi:unit_total_cost',
'production_manager': 'inbound_semi:production_manager',
'production_start_time': 'inbound_semi:production_start_time',
'production_end_time': 'inbound_semi:production_end_time',
'arrival_photo': 'inbound_semi:arrival_photo',
'quality_report_link': 'inbound_semi:quality_report_link',
'detail_link': 'inbound_semi:detail_link',
}
# 复制一份,避免遍历时修改字典
for field in list(data.keys()):
perm_code = field_to_perm.get(field)
if perm_code and perm_code not in user_permissions:
data.pop(field, None)
SemiInboundService.update_inbound(id, data)
return jsonify({"code": 200, "msg": "更新成功"})
except Exception as e:
@ -115,6 +287,7 @@ def update_semi(id):
# 4. 删除半成品入库记录
# ------------------------------------------------------------------
@inbound_semi_bp.route('/<int:id>', methods=['DELETE'])
@permission_required('inbound_semi:operation')
def delete_semi(id):
try:
SemiInboundService.delete_inbound(id)
@ -128,6 +301,7 @@ def delete_semi(id):
# 5. [新增] 获取关联出库历史
# ------------------------------------------------------------------
@inbound_semi_bp.route('/<int:id>/history', methods=['GET'])
@permission_required('inbound_semi')
def get_history(id):
try:
data = SemiInboundService.get_outbound_history(id)
@ -145,6 +319,7 @@ def get_history(id):
# 6. 系统用户建议
# ------------------------------------------------------------------
@inbound_semi_bp.route('/suggestions/users', methods=['GET'])
@permission_required('inbound_semi')
def get_user_suggestions():
keyword = request.args.get('keyword', '')
data = SemiInboundService.search_system_users(keyword)
@ -155,6 +330,7 @@ def get_user_suggestions():
# 7. 获取筛选选项
# ------------------------------------------------------------------
@inbound_semi_bp.route('/options', methods=['GET'])
@permission_required('inbound_semi')
def get_options():
try:
data = SemiInboundService.get_filter_options()

View File

@ -3,21 +3,73 @@ from flask import request, jsonify, current_app
from flask_jwt_extended import jwt_required
from . import inbound_bp
from app.services.inbound.service_service import ServiceService
from app.utils.decorators import role_required
from app.utils.decorators import role_required, permission_required
import traceback
# ==============================================================================
# 辅助函数:获取当前用户的完整权限列表(基于角色查询)
# ==============================================================================
def get_current_user_permissions():
"""
返回当前用户拥有的所有权限码列表(包括菜单和元素)
此函数根据角色查询数据库得到权限。
"""
from flask_jwt_extended import get_jwt
from app.services.auth_service import AuthService
claims = get_jwt()
user_role = claims.get('role')
if not user_role:
return []
# 超级管理员返回所有字段权限
if user_role == 'super_admin':
return ['inbound_service:*']
perm_dict = AuthService.get_user_permissions(user_role)
# 合并菜单和元素权限
perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
return perms
def filter_item_by_permissions(item_dict, user_permissions):
"""
根据用户权限过滤 item 字典,无权限的字段值置为 None
"""
# 字段名到权限码的映射(与前端 permissionMap 保持一致)
field_to_perm = {
'id': 'inbound_service:id',
'base_id': 'inbound_service:base_id',
'sku': 'inbound_service:sku',
'material_name': 'inbound_service:material_name',
'provider_name': 'inbound_service:provider_name',
'sale_price': 'inbound_service:sale_price',
'description': 'inbound_service:description',
'created_at': 'inbound_service:created_at',
'material_type': 'inbound_service:material_type',
'category': 'inbound_service:category',
'spec_model': 'inbound_service:spec_model',
'unit': 'inbound_service:unit',
}
if 'inbound_service:*' in user_permissions:
return item_dict
for field, perm_code in field_to_perm.items():
if field in item_dict and perm_code not in user_permissions:
item_dict[field] = None
return item_dict
@inbound_bp.route('/service/search-base', methods=['GET'])
@jwt_required()
@permission_required('inbound_service')
def search_base():
"""搜索基础物料"""
keyword = request.args.get('keyword', '')
try:
data = ServiceService.search_base_material(keyword)
user_permissions = get_current_user_permissions()
filtered_data = [filter_item_by_permissions(item, user_permissions) for item in data]
return jsonify({
'code': 200,
'msg': 'success',
'data': data
'data': filtered_data
})
except Exception as e:
current_app.logger.error(f'搜索基础物料失败: {str(e)}')
@ -25,7 +77,7 @@ def search_base():
@inbound_bp.route('/service', methods=['GET'])
@jwt_required()
@permission_required('inbound_service')
def get_service_list():
"""获取服务权益列表"""
page = request.args.get('page', 1, type=int)
@ -44,6 +96,9 @@ def get_service_list():
end_date=end_date,
provider_name=provider_name
)
user_permissions = get_current_user_permissions()
if result.get('items'):
result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']]
return jsonify({
'code': 200,
'msg': 'success',
@ -56,8 +111,7 @@ def get_service_list():
@inbound_bp.route('/service', methods=['POST'])
@jwt_required()
@role_required('admin,manager')
@permission_required('inbound_service:operation')
def create_service():
"""创建服务权益"""
data = request.get_json()
@ -72,10 +126,12 @@ def create_service():
try:
service = ServiceService.create_service(data)
user_permissions = get_current_user_permissions()
filtered_data = filter_item_by_permissions(service.to_dict(), user_permissions)
return jsonify({
'code': 201,
'msg': '创建成功',
'data': service.to_dict()
'data': filtered_data
}), 201
except ValueError as e:
return jsonify({'code': 400, 'msg': str(e)}), 400
@ -86,15 +142,17 @@ def create_service():
@inbound_bp.route('/service/<int:service_id>', methods=['GET'])
@jwt_required()
@permission_required('inbound_service')
def get_service(service_id):
"""获取单个服务权益详情"""
try:
service = ServiceService.get_service(service_id)
user_permissions = get_current_user_permissions()
filtered_data = filter_item_by_permissions(service.to_dict(), user_permissions)
return jsonify({
'code': 200,
'msg': 'success',
'data': service.to_dict()
'data': filtered_data
})
except ValueError as e:
return jsonify({'code': 404, 'msg': str(e)}), 404
@ -104,8 +162,7 @@ def get_service(service_id):
@inbound_bp.route('/service/<int:service_id>', methods=['PUT'])
@jwt_required()
@role_required('admin,manager')
@permission_required('inbound_service:operation')
def update_service(service_id):
"""更新服务权益"""
data = request.get_json()
@ -124,10 +181,12 @@ def update_service(service_id):
try:
service = ServiceService.update_service(service_id, filtered_data)
user_permissions = get_current_user_permissions()
filtered_service = filter_item_by_permissions(service.to_dict(), user_permissions)
return jsonify({
'code': 200,
'msg': '更新成功',
'data': service.to_dict()
'data': filtered_service
})
except ValueError as e:
return jsonify({'code': 404, 'msg': str(e)}), 404
@ -137,8 +196,7 @@ def update_service(service_id):
@inbound_bp.route('/service/<int:service_id>', methods=['DELETE'])
@jwt_required()
@role_required('admin,manager')
@permission_required('inbound_service:operation')
def delete_service(service_id):
"""删除服务权益"""
try:
@ -155,7 +213,7 @@ def delete_service(service_id):
@inbound_bp.route('/service/suggestions/providers', methods=['GET'])
@jwt_required()
@permission_required('inbound_service')
def get_provider_suggestions():
base_id = request.args.get('base_id', type=int)
if not base_id:
@ -165,7 +223,7 @@ def get_provider_suggestions():
@inbound_bp.route('/service/suggestions/users', methods=['GET'])
@jwt_required()
@permission_required('inbound_service')
def get_user_suggestions():
keyword = request.args.get('keyword', '')
data = ServiceService.search_system_users(keyword)
@ -173,7 +231,7 @@ def get_user_suggestions():
@inbound_bp.route('/service/options', methods=['GET'])
@jwt_required()
@permission_required('inbound_service')
def get_options():
try:
data = ServiceService.get_filter_options()

View File

@ -2,6 +2,7 @@ from flask import Blueprint, jsonify, request
from app.extensions import db
# ★★★ 修复点:必须引入 datetime否则下方更新时间时会报错 500 ★★★
from datetime import datetime
from app.utils.decorators import permission_required
# 导入模型
from app.models.inbound.buy import StockBuy
@ -24,6 +25,7 @@ bp = Blueprint('stock_ops', __name__)
@bp.route('/all', methods=['GET'])
@permission_required('inventory_stocktake')
def get_all_stock():
"""
获取所有库存 > 0 的物品
@ -63,6 +65,7 @@ def get_all_stock():
# --- 草稿箱接口 ---
@bp.route('/draft/list', methods=['GET'])
@permission_required('inventory_stocktake')
def get_drafts():
"""获取当前用户的盘点进度"""
user_id = request.args.get('user_id', 'admin')
@ -71,6 +74,7 @@ def get_drafts():
@bp.route('/draft/add', methods=['POST'])
@permission_required('inventory_stocktake:operation')
def add_draft():
"""扫码同步 (支持更新数量)"""
try:
@ -100,6 +104,7 @@ def add_draft():
@bp.route('/draft/clear', methods=['POST'])
@permission_required('inventory_stocktake:operation')
def clear_draft():
"""清空进度"""
data = request.json
@ -113,6 +118,7 @@ def clear_draft():
# --- 打印接口 ---
@bp.route('/print/selection', methods=['POST'])
@permission_required('inventory_stocktake:operation')
def print_selection():
try:
data = request.json
@ -126,6 +132,7 @@ def print_selection():
@bp.route('/print/stocktake', methods=['POST'])
@permission_required('inventory_stocktake:operation')
def print_stocktake():
try:
data = request.json

View File

@ -1,17 +1,80 @@
from flask import Blueprint, request, jsonify
from app.services.outbound_service import OutboundService
from flask_jwt_extended import jwt_required, get_jwt_identity
from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt
from app.utils.decorators import permission_required
from app.services.auth_service import AuthService
import traceback
outbound_bp = Blueprint('outbound', __name__, url_prefix='/outbound')
# ==============================================================================
# 辅助函数:获取当前用户的完整权限列表(基于角色查询)
# ==============================================================================
def get_current_user_permissions():
"""
返回当前用户拥有的所有权限码列表(包括菜单和元素)
此函数根据角色查询数据库得到权限。
"""
from flask_jwt_extended import get_jwt
from app.services.auth_service import AuthService
claims = get_jwt()
user_role = claims.get('role')
if not user_role:
return []
# 超级管理员返回所有字段权限
if user_role == 'super_admin':
return ['outbound_list:*']
perm_dict = AuthService.get_user_permissions(user_role)
# 合并菜单和元素权限
perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
return perms
def filter_item_by_permissions(item_dict, user_permissions):
"""
根据用户权限过滤 item 字典,无权限的字段值置为 None
"""
# 字段名到权限码的映射(与前端 permissionMap 保持一致)
field_to_perm = {
'outbound_no': 'outbound_list:outbound_no',
'outbound_time': 'outbound_list:outbound_time',
'outbound_type': 'outbound_list:outbound_type',
'total_amount': 'outbound_list:total_amount',
'consumer_name': 'outbound_list:consumer_name',
'operator_name': 'outbound_list:operator_name',
'remark': 'outbound_list:remark',
'signature_path': 'outbound_list:signature_path',
# 明细字段
'sku': 'outbound_list:sku',
'name': 'outbound_list:name',
'material_type': 'outbound_list:material_type',
'category': 'outbound_list:category',
'spec_model': 'outbound_list:spec_model',
'quantity': 'outbound_list:quantity',
'unit_price': 'outbound_list:unit_price',
'subtotal': 'outbound_list:subtotal',
}
# 如果用户是超级管理员且有 'outbound_list:*',则不过滤
if 'outbound_list:*' in user_permissions:
return item_dict
for field, perm_code in field_to_perm.items():
if field in item_dict and perm_code not in user_permissions:
item_dict[field] = None
# 如果 item_dict 中包含 items 列表,递归处理每个子项
if 'items' in item_dict and isinstance(item_dict['items'], list):
for sub_item in item_dict['items']:
filter_item_by_permissions(sub_item, user_permissions)
return item_dict
# --------------------------------------------------------
# 1. 扫码查询库存接口 (关联三个库存表)
# GET /api/v1/outbound/scan?barcode=...
# --------------------------------------------------------
@outbound_bp.route('/scan', methods=['GET'])
@jwt_required()
@permission_required('outbound_selection')
def scan_barcode():
barcode = request.args.get('barcode')
if not barcode:
@ -45,6 +108,19 @@ def scan_barcode():
@outbound_bp.route('', methods=['POST'])
@jwt_required()
def create_outbound():
# 权限检查:需要 outbound_create:operation 或 outbound_selection:operation 之一
claims = get_jwt()
user_role = claims.get('role')
if not user_role:
return jsonify({'code': 403, 'msg': '未授权'}), 403
# 超级管理员直接放行
if user_role != 'super_admin':
perm_dict = AuthService.get_user_permissions(user_role)
perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
if ('outbound_create:operation' not in perms) and ('outbound_selection:operation' not in perms):
return jsonify({'code': 403, 'msg': '权限不足'}), 403
data = request.get_json()
if not data:
return jsonify({'code': 400, 'msg': '无有效数据'}), 400
@ -89,6 +165,7 @@ def create_outbound():
# --------------------------------------------------------
@outbound_bp.route('', methods=['GET'])
@jwt_required()
@permission_required('outbound_list')
def get_outbound_list():
try:
page = int(request.args.get('page', 1))
@ -99,6 +176,11 @@ def get_outbound_list():
# ★ [修改] 调用分组查询服务
result = OutboundService.get_grouped_list(page, limit, keyword)
# 字段级脱敏
user_permissions = get_current_user_permissions()
if result.get('items'):
result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']]
return jsonify({
'code': 200,
'msg': '获取成功',

View File

@ -0,0 +1,48 @@
# inventory-backend/app/api/v1/permission.py
from flask import Blueprint, request, jsonify, current_app
from flask_jwt_extended import jwt_required
from app.services.permission_service import PermissionService
permission_bp = Blueprint('permission', __name__)
@permission_bp.route('/tree', methods=['GET'])
@jwt_required()
def get_tree():
"""获取权限树"""
try:
data = PermissionService.get_permission_tree()
return jsonify({'code': 200, 'msg': '获取成功', 'data': data}), 200
except Exception as e:
# 打印详细错误到控制台,方便调试
current_app.logger.error(f"Get Tree Failed: {str(e)}")
# 返回 500 时带上错误信息
return jsonify({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}), 500
@permission_bp.route('/role/<string:role_code>', methods=['GET'])
@jwt_required()
def get_role_perms(role_code):
"""获取某个角色的权限列表"""
try:
data = PermissionService.get_role_permissions(role_code)
return jsonify({'code': 200, 'msg': '获取成功', 'data': data}), 200
except Exception as e:
current_app.logger.error(f"Get Role Perms Failed: {str(e)}")
return jsonify({'code': 500, 'msg': str(e)}), 500
@permission_bp.route('/assign', methods=['POST'])
@jwt_required()
def assign_perms():
"""保存权限分配"""
try:
data = request.get_json()
role_code = data.get('role_code')
permissions = data.get('permissions', []) # list of codes
PermissionService.assign_permissions(role_code, permissions)
return jsonify({'code': 200, 'msg': '保存成功'}), 200
except Exception as e:
current_app.logger.error(f"Assign Perms Failed: {str(e)}")
return jsonify({'code': 500, 'msg': str(e)}), 500

View File

@ -1,14 +1,64 @@
from flask import Blueprint, jsonify, request # .material -> .base refactor checked
from flask_jwt_extended import jwt_required, get_jwt_identity
from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt
from app.utils.decorators import permission_required
from app.services.auth_service import AuthService
from app.services.trans_service import TransService
import traceback
trans_bp = Blueprint('transactions', __name__, url_prefix='/transactions')
# ==============================================================================
# 辅助函数:获取当前用户的完整权限列表(基于角色查询)
# ==============================================================================
def get_current_user_permissions():
"""
返回当前用户拥有的所有权限码列表(包括菜单和元素)
此函数根据角色查询数据库得到权限。
"""
claims = get_jwt()
user_role = claims.get('role')
if not user_role:
return []
# 超级管理员返回所有字段权限
if user_role == 'super_admin':
return ['*']
perm_dict = AuthService.get_user_permissions(user_role)
# 合并菜单和元素权限
perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
return perms
def filter_item_by_permissions(item_dict, user_permissions, prefix='op_records'):
"""
根据用户权限过滤 item 字典,无权限的字段值置为 None
"""
# 字段名到权限码的映射(与前端 permissionMap 保持一致)
field_to_perm = {
'borrow_no': f'{prefix}:borrow_no',
'borrower_name': f'{prefix}:borrower_name',
'sku': f'{prefix}:sku',
'borrow_time': f'{prefix}:borrow_time',
'return_time': f'{prefix}:return_time',
'status': f'{prefix}:status',
'expected_return_time': f'{prefix}:expected_return_time',
'return_location': f'{prefix}:return_location',
'borrow_signature': f'{prefix}:borrow_signature',
'return_signature': f'{prefix}:return_signature',
}
# 如果用户是超级管理员且有 '*',则不过滤
if '*' in user_permissions:
return item_dict
for field, perm_code in field_to_perm.items():
if field in item_dict and perm_code not in user_permissions:
item_dict[field] = None
return item_dict
# --- 借库接口 ---
@trans_bp.route('/borrow', methods=['POST'])
@jwt_required()
@permission_required('op_borrow:operation')
def create_borrow():
data = request.get_json()
try:
@ -21,6 +71,7 @@ def create_borrow():
# --- 还库辅助:扫码查找借出记录 ---
@trans_bp.route('/return/scan', methods=['GET'])
@jwt_required()
@permission_required('op_return')
def scan_borrowed_item():
barcode = request.args.get('barcode')
if not barcode:
@ -36,6 +87,7 @@ def scan_borrowed_item():
# --- 还库提交 ---
@trans_bp.route('/return', methods=['POST'])
@jwt_required()
@permission_required('op_return:operation')
def submit_return():
data = request.get_json()
user = get_jwt_identity() # 库管
@ -49,10 +101,15 @@ def submit_return():
# --- 记录列表 ---
@trans_bp.route('/records', methods=['GET'])
@jwt_required()
@permission_required('op_records')
def get_records():
status = request.args.get('status', 'all')
page = int(request.args.get('page', 1))
keyword = request.args.get('keyword', '')
res = TransService.get_records(page=page, limit=10, status=status, keyword=keyword)
# 字段级脱敏
user_permissions = get_current_user_permissions()
if res.get('items'):
res['items'] = [filter_item_by_permissions(item, user_permissions, 'op_records') for item in res['items']]
return jsonify({'code': 200, 'data': res})

View File

@ -1,14 +1,17 @@
# app/models/system.py
# inventory-backend/app/models/system.py
from app.extensions import db
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
# ==========================================
# 1. 系统用户表
# ==========================================
class SysUser(db.Model):
"""
系统用户表
对应数据库: sys_user
username 字段存储格式约定: "真实姓名/登录账号" (例如: 张三/zhangsan)
username 字段存储格式约定: "真实姓名/登录账号" (例如: 张三/zhangsan01)
"""
__tablename__ = 'sys_user'
@ -19,8 +22,7 @@ class SysUser(db.Model):
role = db.Column(db.String(50))
status = db.Column(db.String(20), default='active')
password_hash = db.Column(db.Text)
# created_at 已在数据库脚本中移除,此处不再定义
created_at = db.Column(db.DateTime, default=datetime.now)
def set_password(self, password):
"""生成加密密码"""
@ -45,23 +47,27 @@ class SysUser(db.Model):
parts = raw_name.split('/')
real_name = parts[0]
acc_id = parts[1]
# 格式化为前端展示格式: 张三(zhangsan)
# 格式化为前端展示格式: 张三(zhangsan01)
display_name = f"{real_name}({acc_id})"
# 单独提取账号ID (如果前端需要单独用)
account_id = acc_id
return {
'id': self.id,
'username': display_name, # 列表显示: 张三(zhangsan)
'username': display_name, # 列表显示: 张三(zhangsan01)
'raw_username': self.username, # 原始数据
'account_id': account_id, # 纯账号ID: zhangsan
'account_id': account_id, # 纯账号ID: zhangsan01
'email': self.email,
'department': self.department,
'role': self.role,
'status': self.status
'status': self.status,
'created_at': self.created_at.isoformat() if self.created_at else None
}
# ==========================================
# 2. 系统日志表
# ==========================================
class SysLog(db.Model):
"""
系统操作日志表
@ -89,3 +95,57 @@ class SysLog(db.Model):
'action_type': self.action_type,
'description': self.description
}
# ==========================================
# 3. 权限管理模型 (RBAC) - [新增]
# ==========================================
class SysMenu(db.Model):
"""系统菜单/页面表"""
__tablename__ = 'sys_menu'
id = db.Column(db.Integer, primary_key=True)
parent_id = db.Column(db.Integer, default=0)
name = db.Column(db.String(50), nullable=False)
code = db.Column(db.String(100), unique=True, nullable=False)
path = db.Column(db.String(200))
sort_order = db.Column(db.Integer, default=0)
is_visible = db.Column(db.Boolean, default=True)
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'code': self.code,
'path': self.path,
'type': 'menu' # 前端树形控件图标判断用
}
class SysElement(db.Model):
"""页面元素/列定义表"""
__tablename__ = 'sys_element'
id = db.Column(db.Integer, primary_key=True)
menu_code = db.Column(db.String(100), db.ForeignKey('sys_menu.code'))
name = db.Column(db.String(100), nullable=False)
code = db.Column(db.String(100), nullable=False) # 如: unit_price
element_type = db.Column(db.String(20), default='column')
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'code': self.code,
'menu_code': self.menu_code,
'type': 'element',
'element_type': self.element_type
}
class SysRolePermission(db.Model):
"""角色权限关联表"""
__tablename__ = 'sys_role_permission'
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'

View File

@ -1,11 +1,10 @@
# app/services/auth_service.py
from app.models.system import SysUser
from app.models.system import SysUser, SysRolePermission # <== 引入 SysRolePermission
from app.extensions import db
from flask_jwt_extended import create_access_token
from app.utils.constants import UserRole
from datetime import timedelta
class AuthService:
# 硬编码的超级管理员凭证
SUPER_ADMIN_USER = "IRIS"
@ -212,3 +211,36 @@ class AuthService:
db.session.delete(user)
db.session.commit()
return True
@staticmethod
def get_user_permissions(role_code):
"""
获取指定角色的所有权限代码列表
返回格式: {
'menus': ['inbound_buy', 'system_user'],
'elements': ['inbound_buy:unit_price', ...]
}
"""
# 1. 查菜单权限
menu_perms = SysRolePermission.query.filter_by(
role_code=role_code,
type='menu'
).all()
menu_codes = [p.target_code for p in menu_perms]
# 2. 查元素(列)权限
# 注意:这里我们只返回用户拥有的。前端逻辑是:"如果列配置了Key且用户没这个Key则隐藏"
element_perms = SysRolePermission.query.filter_by(
role_code=role_code,
type='element'
).all()
# 这里的 target_code 就是列的 code (如 unit_price)
# 为了防止不同页面有相同列名导致的混淆,我们之前数据库设计是做了隔离的
# 但为了前端处理方便,我们直接返回列的 code 集合
element_codes = [p.target_code for p in element_perms]
return {
'menus': menu_codes,
'elements': element_codes
}

View File

@ -0,0 +1,149 @@
from app.models.system import SysMenu, SysElement, SysRolePermission
from app.extensions import db
from sqlalchemy.exc import SQLAlchemyError
class PermissionService:
@staticmethod
def get_permission_tree():
"""
获取完整的权限树(菜单嵌套菜单 + 菜单包含元素)
供前端权限配置页面展示
"""
# 1. 获取所有菜单 (按 parent_id 和 sort_order 排序,保证父子处理顺序)
menus = SysMenu.query.order_by(SysMenu.parent_id, SysMenu.sort_order).all()
# 2. 获取所有元素
elements = SysElement.query.all()
# --- 核心逻辑:构建树形结构 ---
# 3. 创建一个 lookup 字典,方便通过 ID 查找菜单节点
# 同时将 SQLAlchemy 对象转为字典,方便后续操作
menu_map = {}
for m in menus:
m_dict = m.to_dict()
m_dict['children'] = [] # 初始化 children
menu_map[m.id] = m_dict
# 4. 创建 code 到 id 的映射,用于把 element 挂载到 menu 上
# 因为 SysElement 关联的是 menu_code而不是 menu_id
code_to_id = {m.code: m.id for m in menus}
# 5. 将元素 (Elements) 挂载到对应的菜单 (Menu) 下
for el in elements:
# 找到该元素所属菜单的 ID
parent_menu_id = code_to_id.get(el.menu_code)
if parent_menu_id and parent_menu_id in menu_map:
el_dict = el.to_dict()
# 标记类型为 element前端 transformData 需要用到
el_dict['type'] = 'element'
menu_map[parent_menu_id]['children'].append(el_dict)
# 6. 将子菜单挂载到父菜单下,并构建最终的树
tree_data = []
for m in menus:
current_node = menu_map[m.id]
if m.parent_id == 0 or m.parent_id is None:
# 如果是顶级菜单,直接放入结果集
tree_data.append(current_node)
else:
# 如果是子菜单,找到它的父级,把它塞进父级的 children 里
if m.parent_id in menu_map:
menu_map[m.parent_id]['children'].append(current_node)
else:
# 如果找不到父级(比如父级被删了),为了防止数据丢失,暂时作为顶级显示
tree_data.append(current_node)
return tree_data
@staticmethod
def get_role_permissions(role_code):
"""获取指定角色拥有的所有权限Code"""
try:
# === 新增逻辑:超级管理员上帝模式 ===
if role_code == 'SUPER_ADMIN':
# 直接获取所有菜单和元素,无视配置表
all_menus = [m.code for m in SysMenu.query.all()]
all_elements = [e.code for e in SysElement.query.all()]
return {
'menus': all_menus,
'elements': all_elements
}
# =================================
perms = SysRolePermission.query.filter_by(role_code=role_code).all()
menu_codes = []
element_codes = []
for p in perms:
# 这里假设你的数据库存的是 target_code
if p.type == 'menu':
menu_codes.append(p.target_code)
else:
element_codes.append(p.target_code)
# 前端 handleRoleSelect 会合并这两个数组,所以分开返回没问题
return {
'menus': menu_codes,
'elements': element_codes
}
except Exception as e:
# 记录日志或处理错误
print(f"Error fetching role permissions: {e}")
return {'menus': [], 'elements': []}
@staticmethod
def assign_permissions(role_code, permissions):
"""
保存角色的权限
permissions: 前端传来的 list混合了 menu_code 和 element_code
"""
if not role_code:
raise ValueError("角色代码不能为空")
session = db.session
try:
# 1. 开启事务 (Flask-SQLAlchemy 自动管理,但明确逻辑更好)
# 2. 删除该角色旧的所有权限
SysRolePermission.query.filter_by(role_code=role_code).delete()
# 3. 准备新数据
if permissions:
# 3.1 去重
unique_codes = set(permissions)
# 3.2 预加载所有 Menu Code用于区分是 Menu 还是 Element
# 这一步很重要,因为 SysRolePermission 表需要 type 字段
all_menu_codes = {res[0] for res in session.query(SysMenu.code).all()}
new_records = []
for code in unique_codes:
if not code: continue
# 判断类型:如果 code 存在于菜单表中,就是 menu否则就是 element
p_type = 'menu' if code in all_menu_codes else 'element'
new_records.append(SysRolePermission(
role_code=role_code,
target_code=code,
type=p_type
))
# 3.3 批量插入
if new_records:
session.add_all(new_records)
# 4. 提交
session.commit()
return True
except SQLAlchemyError as e:
session.rollback()
raise e
except Exception as e:
session.rollback()
raise e

View File

@ -1,7 +1,8 @@
# app/utils/decorators.py
from functools import wraps
from flask_jwt_extended import get_jwt
from flask import jsonify
from flask_jwt_extended import get_jwt, verify_jwt_in_request
from flask import jsonify, g
import logging
def role_required(*roles):
@ -28,3 +29,60 @@ def role_required(*roles):
return decorator
return wrapper
def login_required(fn):
"""
验证 JWT 令牌是否存在且有效
"""
@wraps(fn)
def decorator(*args, **kwargs):
try:
verify_jwt_in_request()
except Exception as e:
logging.warning(f"JWT verification failed: {e}")
return jsonify(msg='登录已过期,请重新登录'), 401
return fn(*args, **kwargs)
return decorator
def permission_required(permission_code):
"""
检查当前用户是否拥有指定权限码
使用方法: @permission_required('material:base:read')
"""
def wrapper(fn):
@wraps(fn)
def decorator(*args, **kwargs):
# 首先验证 JWT
try:
verify_jwt_in_request()
except Exception as e:
logging.warning(f"JWT verification failed: {e}")
return jsonify(msg='登录已过期,请重新登录'), 401
claims = get_jwt()
user_role = claims.get('role')
# 超级管理员放行
if user_role == 'super_admin':
return fn(*args, **kwargs)
# 根据角色查询数据库中的权限
try:
from app.services.auth_service import AuthService
perm_dict = AuthService.get_user_permissions(user_role)
except Exception as e:
logging.warning(f"Failed to fetch permissions for role {user_role}: {e}")
return jsonify(msg='权限查询失败'), 403
# 合并菜单和元素权限
all_perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
if permission_code not in all_perms:
# 详细的调试日志
print(f"🔴 [权限拦截] 角色 '{user_role}' 访问被拒!需要权限码: '{permission_code}', 但该角色实际拥有: {all_perms}")
logging.warning(
f"权限检查失败: 角色={user_role}, 所需权限={permission_code}, 实际权限列表={all_perms}")
return jsonify(msg='权限不足:您没有访问此资源的权限'), 403
return fn(*args, **kwargs)
return decorator
return wrapper

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessageBox, ElMessage } from 'element-plus'
import { InfoFilled, SwitchButton, UserFilled } from '@element-plus/icons-vue'
@ -14,6 +14,13 @@ const isLoginPage = computed(() => {
return route.path === '/login'
})
// 页面加载时刷新权限
onMounted(() => {
if (userStore.token) {
userStore.refreshUserPermissions()
}
})
// --- 退出登录逻辑 Start ---
const handleLogout = () => {
ElMessageBox.confirm(
@ -82,7 +89,7 @@ const handleLogout = () => {
<footer v-if="!isLoginPage" class="app-footer">
<span class="version-tag">
<el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon>
当前版本: 1.3 Beta (2.25权限管理版)
当前版本: 1.4 Beta (2.27权限管理版)
</span>
</footer>
</div>

View File

@ -0,0 +1,26 @@
import request from '@/utils/request'
// 获取所有可用的权限树(菜单+列)
export function getAllPermissionTree() {
return request({
url: '/v1/permissions/tree',
method: 'get'
})
}
// 获取某个角色已拥有的权限列表
export function getRolePermissions(roleCode: string) {
return request({
url: '/v1/permissions/role/' + roleCode,
method: 'get'
})
}
// 保存角色的权限配置
export function saveRolePermissions(data: any) {
return request({
url: '/v1/permissions/assign',
method: 'post',
data
})
}

View File

@ -0,0 +1,134 @@
<template>
<div class="base-table">
<el-table
v-bind="$attrs"
:data="data"
border
stripe
v-loading="loading"
header-cell-class-name="table-header-gray"
>
<template v-for="col in visibleColumns" :key="col.prop">
<el-table-column
v-if="!col.slot"
v-bind="col"
:prop="col.prop"
:label="col.label"
:min-width="col.minWidth || '120'"
:width="col.width"
:fixed="col.fixed"
show-overflow-tooltip
/>
<el-table-column
v-else
v-bind="col"
:prop="col.prop"
:label="col.label"
:min-width="col.minWidth || '120'"
:width="col.width"
:fixed="col.fixed"
>
<template #default="scope">
<slot :name="col.prop" :row="scope.row" :index="scope.$index"></slot>
</template>
</el-table-column>
</template>
<template #empty>
<el-empty description="暂无数据" />
</template>
</el-table>
<div v-if="showPagination" class="pagination-container">
<el-pagination
v-bind="paginationConfig"
v-model:current-page="localPage"
v-model:page-size="localLimit"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { usePermissionStore } from '@/stores/permission'
// --- Props 定义 ---
const props = defineProps({
// 数据源
data: { type: Array, default: () => [] },
// 列配置 (核心)
columns: { type: Array as () => any[], required: true },
// 页面编码 (用于权限隔离,如果列名全局唯一可不传,但建议传)
pageCode: { type: String, default: '' },
loading: { type: Boolean, default: false },
total: { type: Number, default: 0 },
showPagination: { type: Boolean, default: true },
page: { type: Number, default: 1 },
limit: { type: Number, default: 10 }
})
const emit = defineEmits(['update:page', 'update:limit', 'pagination'])
const permStore = usePermissionStore()
// --- 核心逻辑:计算当前可见的列 ---
const visibleColumns = computed(() => {
return props.columns.filter(col => {
// 1. 获取该列在数据库中对应的 code
// 如果列配置里显式写了 code就用写的如果没有默认认为 prop 就是 code
const permissionKey = col.code || col.prop
// 2. 如果这个列不需要权限控制 (比如序号 index),可以在配置里加个 ignoreAuth: true
if (col.ignoreAuth) return true
// 3. 问 Store我有这个权限吗
// 注意:我们在 PermissionStore 里存的是全局唯一的 code
return permStore.hasColumnPermission(props.pageCode, permissionKey)
})
})
// --- 分页逻辑处理 ---
const localPage = ref(props.page)
const localLimit = ref(props.limit)
watch(() => props.page, (val) => localPage.value = val)
watch(() => props.limit, (val) => localLimit.value = val)
const handleSizeChange = (val: number) => {
emit('update:limit', val)
emit('pagination', { page: localPage.value, limit: val })
}
const handleCurrentChange = (val: number) => {
emit('update:page', val)
emit('pagination', { page: val, limit: localLimit.value })
}
const paginationConfig = {
pageSizes: [10, 20, 50, 100],
background: true
}
</script>
<style scoped>
.pagination-container {
margin-top: 15px;
display: flex;
justify-content: flex-end;
}
:deep(.table-header-gray th) {
background-color: #f8f9fb !important;
color: #606266;
font-weight: 600;
height: 45px;
}
</style>

View File

@ -195,9 +195,19 @@ const routes: Array<RouteRecordRaw> = [
meta: {
title: '账号开通',
icon: 'User',
// 子路由也建议加上权限限制
roles: ['SUPER_ADMIN', 'SUPERVISOR']
}
},
// [新增] 权限分配页面,只有超级管理员可进
{
path: 'permission',
name: 'PermissionConfig',
component: () => import('@/views/system/PermissionConfig.vue'),
meta: {
title: '权限分配',
icon: 'Lock',
roles: ['SUPER_ADMIN']
}
}
]
},
@ -224,11 +234,10 @@ router.beforeEach((to, from, next) => {
const token = userStore.token || localStorage.getItem('token')
// [修复] 优先从 user 对象获取,并统一转大写,防止大小写不一致导致权限失效
// 注意Store 中存储的可能是 user.role 或者直接是 role根据你之前的 store 结构适配
const rawRole = userStore.user?.role || userStore.role || localStorage.getItem('role') || 'user'
const userRole = String(rawRole).toUpperCase()
// 调试日志:如果跳转有问题,请按 F12 查看控制台输出
// 调试日志
if (to.path.includes('/system')) {
console.log(`路由守卫检查: Path=${to.path}, UserRole=${userRole}, Required=${to.meta.roles}`)
}
@ -249,7 +258,6 @@ router.beforeEach((to, from, next) => {
// 权限检查逻辑
if (to.meta.roles && Array.isArray(to.meta.roles)) {
// [修复] to.meta.roles 里已经是大写了userRole 也转大写了,现在可以安全比对
if (to.meta.roles.includes(userRole)) {
next()
} else {

View File

@ -0,0 +1,49 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import request from '@/utils/request'
export const usePermissionStore = defineStore('permission', () => {
// 存储我能看到的页面代码 (如 ['inbound_buy', ...])
const menuPermissions = ref<string[]>([])
// 存储我能看到的列代码 (如 ['unit_price', 'sale_price'])
const elementPermissions = ref<string[]>([])
// 初始化加载权限 (登录后调用)
const loadPermissions = async () => {
try {
const res: any = await request({
url: '/v1/auth/my-permissions',
method: 'get'
})
if (res.code === 200 && res.data) {
menuPermissions.value = res.data.menus || []
elementPermissions.value = res.data.elements || []
console.log('权限字典加载完成:', elementPermissions.value.length, '个列权限')
}
} catch (e) {
console.error('加载权限失败', e)
// 失败时清空,防止残留
menuPermissions.value = []
elementPermissions.value = []
}
}
// ★ 核心判断函数:判断当前用户是否拥有某个列/按钮的权限
// page: 页面代码 (预留字段目前全局唯一code暂不使用page隔离)
// code: 权限标识 (如 'unit_price')
const hasColumnPermission = (page: string, code: string) => {
// 1. 如果列没有配置 permissionKey说明是公开列直接放行
if (!code) return true
// 2. 检查权限池里是否有这个 code
return elementPermissions.value.includes(code)
}
return {
menuPermissions,
elementPermissions,
loadPermissions,
hasColumnPermission
}
})

View File

@ -1,5 +1,6 @@
import { defineStore } from 'pinia'
import { login } from '@/api/auth'
import { getRolePermissions } from '@/api/system/permission'
import { ref } from 'vue'
export const useUserStore = defineStore('user', () => {
@ -7,6 +8,7 @@ export const useUserStore = defineStore('user', () => {
const token = ref(localStorage.getItem('token') || '')
const role = ref(localStorage.getItem('role') || '')
const username = ref(localStorage.getItem('username') || '')
const permissions = ref<string[]>(JSON.parse(localStorage.getItem('permissions') || '[]'))
// 2. Actions
// 登录逻辑
@ -44,6 +46,25 @@ export const useUserStore = defineStore('user', () => {
// 持久化存储 Token
localStorage.setItem('token', data.access_token)
// 登录成功后,根据角色获取权限
if (role.value) {
try {
const permRes = await getRolePermissions(role.value)
const permData = permRes.data || permRes
// 合并 menus 和 elements 两个数组
const allPerms = [
...(permData.menus || []),
...(permData.elements || [])
]
permissions.value = allPerms
localStorage.setItem('permissions', JSON.stringify(allPerms))
} catch (error) {
console.error('获取权限失败:', error)
permissions.value = []
localStorage.setItem('permissions', '[]')
}
}
return true // 返回 true 表示登录成功
}
@ -53,11 +74,36 @@ export const useUserStore = defineStore('user', () => {
token.value = ''
role.value = ''
username.value = ''
permissions.value = []
// 2. 清空 LocalStorage (硬盘)
localStorage.removeItem('token')
localStorage.removeItem('role')
localStorage.removeItem('username')
localStorage.removeItem('permissions')
}
// 刷新用户权限(不重新登录)
const refreshUserPermissions = async () => {
if (!token.value || !role.value) {
console.warn('无法刷新权限:用户未登录')
return
}
try {
const permRes = await getRolePermissions(role.value)
const permData = permRes.data || permRes
// 合并 menus 和 elements 两个数组
const allPerms = [
...(permData.menus || []),
...(permData.elements || [])
]
permissions.value = allPerms
localStorage.setItem('permissions', JSON.stringify(allPerms))
console.log('用户权限已刷新')
} catch (error) {
console.error('刷新权限失败:', error)
// 可选:保留原有权限
}
}
// 3. Getters / Helpers
@ -66,12 +112,20 @@ export const useUserStore = defineStore('user', () => {
return roles.includes(role.value)
}
// 判断当前用户是否拥有某个权限(菜单或元素)
const hasPermission = (code: string) => {
return permissions.value.includes(code)
}
return {
token,
role,
username,
permissions,
handleLogin,
logout,
hasRole
refreshUserPermissions,
hasRole,
hasPermission
}
})

View File

@ -17,29 +17,29 @@
<el-button :icon="Search" @click="fetchBomList" />
</template>
</el-input>
<el-button type="primary" :icon="Plus" @click="handleCreate">新建 BOM</el-button>
<el-button v-if="userStore.hasPermission('bom_manage:operation')" type="primary" :icon="Plus" @click="handleCreate">新建 BOM</el-button>
</div>
</div>
</template>
<el-table v-loading="loading" :data="bomList" border style="width: 100%">
<el-table-column prop="bom_no" label="BOM编号" min-width="180" sortable />
<el-table-column prop="parent_name" label="父件名称" min-width="150" />
<el-table-column prop="parent_spec" label="父件规格" min-width="150" />
<el-table-column prop="version" label="版本" width="100" align="center">
<el-table-column v-if="hasColumnPermission('bom_no')" prop="bom_no" label="BOM编号" min-width="180" sortable />
<el-table-column v-if="hasColumnPermission('parent_name')" prop="parent_name" label="父件名称" min-width="150" />
<el-table-column v-if="hasColumnPermission('parent_spec')" prop="parent_spec" label="父件规格" min-width="150" />
<el-table-column v-if="hasColumnPermission('version')" prop="version" label="版本" width="100" align="center">
<template #default="{ row }">
<el-tag>{{ row.version }}</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<el-table-column v-if="hasColumnPermission('status')" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.is_enabled ? 'success' : 'danger'">
{{ row.is_enabled ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="child_count" label="子件数" width="80" align="center" />
<el-table-column label="操作" width="250" align="center" fixed="right">
<el-table-column v-if="hasColumnPermission('child_count')" prop="child_count" label="子件数" width="80" align="center" />
<el-table-column v-if="userStore.hasPermission('bom_manage:operation')" label="操作" width="250" align="center" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="success" link @click="handleSaveAs(row)">另存为</el-button>
@ -80,7 +80,7 @@
</el-col>
<el-col :span="8">
<el-form-item label="是否启用" prop="is_enabled">
<el-switch v-model="form.is_enabled" active-text="启用" inactive-text="禁用" />
<el-switch v-model="form.is_enabled" active-text="启用" inactive-text="禁用" :disabled="!userStore.hasPermission('bom_manage:operation')" />
</el-form-item>
</el-col>
</el-row>
@ -169,6 +169,7 @@ import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'
import { Plus, Search } from '@element-plus/icons-vue'
import { getBomList, getBomDetail, saveBom, deleteBom } from '@/api/bom'
import { getMaterialBaseList } from '@/api/inbound/stock'
import { useUserStore } from '@/stores/user'
// 类型定义
interface BomItem {
@ -190,6 +191,7 @@ interface ChildRow {
remark: string
}
const userStore = useUserStore()
const loading = ref(false)
const dialogVisible = ref(false)
const saving = ref(false)
@ -199,6 +201,25 @@ const bomList = ref<BomItem[]>([])
const materialOptions = ref<MaterialBase[]>([])
const searchKeyword = ref('')
// 列与权限Code的映射关系数据库中的code
const permissionMap: Record<string, string> = {
bom_no: 'bom_manage:bom_no',
parent_name: 'bom_manage:parent_name',
parent_spec: 'bom_manage:parent_spec',
version: 'bom_manage:version',
status: 'bom_manage:status',
child_count: 'bom_manage:child_count',
}
// 检查列权限
const hasColumnPermission = (prop: string) => {
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return true
}
const code = permissionMap[prop]
return code ? userStore.hasPermission(code) : false
}
const formRef = ref<FormInstance>()
const form = reactive({
bom_prefix: '', // 自动生成的父件规格前缀
@ -229,7 +250,9 @@ const fetchBomList = async () => {
try {
const res = await getBomList({ keyword: searchKeyword.value })
if (res.code === 200) bomList.value = res.data
} catch (error) { ElMessage.error('网络错误') }
} catch (error) {
// 错误已由全局拦截器统一处理
}
finally { loading.value = false }
}
@ -237,7 +260,9 @@ const fetchMaterialOptions = async () => {
try {
const res = await getMaterialBaseList()
if (res.code === 200) materialOptions.value = res.data
} catch (error) {}
} catch (error) {
// 错误已由全局拦截器统一处理
}
}
// 监听父件变化,自动设置前缀
@ -300,7 +325,9 @@ const loadDetail = async (bomNo: string, version: string) => {
form.bom_suffix = bomNo
}
}
} catch (e) { ElMessage.error('获取详情失败') }
} catch (e) {
// 错误已由全局拦截器统一处理
}
}
const handleDelete = (row: BomItem) => {
@ -353,7 +380,9 @@ const submitForm = async () => {
dialogVisible.value = false
fetchBomList()
} else { ElMessage.error(res.msg || '保存失败') }
} catch (e) { ElMessage.error('网络错误') }
} catch (e) {
// 错误已由全局拦截器统一处理
}
finally { saving.value = false }
})
}

View File

@ -52,11 +52,13 @@
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { ElMessageBox } from 'element-plus' // 引入 ElMessageBox
import { usePermissionStore } from '@/stores/permission' // [新增] 引入权限Store
import { ElMessageBox } from 'element-plus'
import { User, Lock } from '@element-plus/icons-vue'
const router = useRouter()
const userStore = useUserStore()
const permissionStore = usePermissionStore() // [新增]
const loading = ref(false)
const loginFormRef = ref()
@ -74,23 +76,25 @@ const onLogin = async () => {
if (valid) {
loading.value = true
try {
// 执行登录请求
// 1. 执行登录请求
const success = await userStore.handleLogin(loginForm)
if (success) {
// 成功:跳转
// [新增] 2. 登录成功后,立即拉取当前用户的权限字典
// 这样进入 Dashboard 时,所有按钮/列的显示状态就已经确定了
await permissionStore.loadPermissions()
// 3. 跳转
router.push('/dashboard')
} else {
// 失败(业务逻辑拒绝,如账号密码错):弹出模态框
// 失败(业务逻辑拒绝):弹出模态框
showLoginFailAlert('用户名或密码错误')
}
} catch (error: any) {
// 失败(系统错误,如网络断开/500报错):弹出模态框
// 优先取后端的报错信息,没有则显示默认
// 失败(系统错误):弹出模态框
const msg = error.response?.data?.msg || error.message || '登录遇到未知错误'
showLoginFailAlert(msg)
} finally {
// 停止转圈,让用户可以看清弹窗
loading.value = false
}
}
@ -103,8 +107,6 @@ const showLoginFailAlert = (msg: string) => {
confirmButtonText: '确定',
type: 'error',
callback: () => {
// 点击确定后,清空密码框,让用户重试
// 页面绝对不会刷新,光标还在
loginForm.password = ''
}
})

View File

@ -71,7 +71,7 @@
<el-icon style="margin-right: 5px"><Download /></el-icon>导出库存统计
</el-button>
<el-button type="primary" @click="handleAdd" style="margin-right: 10px">
<el-button v-if="userStore.hasPermission('material_list:operation')" type="primary" @click="handleAdd" style="margin-right: 10px">
<el-icon style="margin-right: 5px"><Plus /></el-icon>新增
</el-button>
@ -98,18 +98,18 @@
<div style="font-weight: bold; margin-bottom: 5px; border-bottom: 1px solid #eee; padding-bottom: 5px">
列展示设置
</div>
<el-checkbox v-model="columns.id.visible" label="ID" />
<el-checkbox v-model="columns.companyName.visible" label="所属公司" />
<el-checkbox v-model="columns.name.visible" label="名称" />
<el-checkbox v-model="columns.commonName.visible" label="俗名" />
<el-checkbox v-model="columns.category.visible" label="类别" />
<el-checkbox v-model="columns.type.visible" label="类型" />
<el-checkbox v-model="columns.spec.visible" label="规格型号" />
<el-checkbox v-model="columns.unit.visible" label="单位" />
<el-checkbox v-model="columns.inventory.visible" label="库存数" />
<el-checkbox v-model="columns.available.visible" label="可用数" />
<el-checkbox v-model="columns.files.visible" label="资料" />
<el-checkbox v-model="columns.isEnabled.visible" label="状态" />
<el-checkbox v-model="columns.id.visible" label="ID" :disabled="!userStore.hasPermission(permissionMap.id)" />
<el-checkbox v-model="columns.companyName.visible" label="所属公司" :disabled="!userStore.hasPermission(permissionMap.companyName)" />
<el-checkbox v-model="columns.name.visible" label="名称" :disabled="!userStore.hasPermission(permissionMap.name)" />
<el-checkbox v-model="columns.commonName.visible" label="俗名" :disabled="!userStore.hasPermission(permissionMap.commonName)" />
<el-checkbox v-model="columns.category.visible" label="类别" :disabled="!userStore.hasPermission(permissionMap.category)" />
<el-checkbox v-model="columns.type.visible" label="类型" :disabled="!userStore.hasPermission(permissionMap.type)" />
<el-checkbox v-model="columns.spec.visible" label="规格型号" :disabled="!userStore.hasPermission(permissionMap.spec)" />
<el-checkbox v-model="columns.unit.visible" label="单位" :disabled="!userStore.hasPermission(permissionMap.unit)" />
<el-checkbox v-model="columns.inventory.visible" label="库存数" :disabled="!userStore.hasPermission(permissionMap.inventory)" />
<el-checkbox v-model="columns.available.visible" label="可用数" :disabled="!userStore.hasPermission(permissionMap.available)" />
<el-checkbox v-model="columns.files.visible" label="资料" :disabled="!userStore.hasPermission(permissionMap.files)" />
<el-checkbox v-model="columns.isEnabled.visible" label="状态" :disabled="!userStore.hasPermission(permissionMap.isEnabled)" />
</div>
</el-popover>
</div>
@ -210,14 +210,15 @@
:active-value="1"
:inactive-value="0"
:loading="scope.row.statusLoading"
:disabled="!userStore.hasPermission('material_list:operation')"
@change="handleStatusChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column label="操作" min-width="150" fixed="right" align="center">
<el-table-column v-if="userStore.hasPermission('material_list:operation')" label="操作" min-width="150" fixed="right" align="center">
<template #default="scope">
<el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
<el-button v-if="userStore.hasPermission('material_list:operation')" link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button v-if="userStore.hasPermission('material_list:operation')" link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
@ -246,12 +247,12 @@
<el-row>
<el-col :span="12">
<el-form-item label="名称" prop="name">
<el-form-item label="名称" prop="name" v-if="hasFieldPermission('name')">
<el-input v-model="form.name" placeholder="内部名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="俗名" prop="commonName">
<el-form-item label="俗名" prop="commonName" v-if="hasFieldPermission('commonName')">
<el-input v-model="form.commonName" placeholder="标准名称" />
</el-form-item>
</el-col>
@ -259,7 +260,7 @@
<el-row>
<el-col :span="12">
<el-form-item label="所属公司" prop="companyName">
<el-form-item label="所属公司" prop="companyName" v-if="hasFieldPermission('companyName')">
<el-autocomplete
v-model="form.companyName"
:fetch-suggestions="querySearchCompany"
@ -270,7 +271,7 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="类别" prop="category">
<el-form-item label="类别" prop="category" v-if="hasFieldPermission('category')">
<div style="display: flex; width: 100%; align-items: center;">
<el-cascader
v-model="tempCategoryPrefix"
@ -298,7 +299,7 @@
<el-row>
<el-col :span="12">
<el-form-item label="类型" prop="type">
<el-form-item label="类型" prop="type" v-if="hasFieldPermission('type')">
<el-autocomplete
v-model="form.type"
:fetch-suggestions="querySearchType"
@ -309,7 +310,7 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="规格型号" prop="spec">
<el-form-item label="规格型号" prop="spec" v-if="hasFieldPermission('spec')">
<el-input v-model="form.spec" placeholder="请输入规格型号" />
</el-form-item>
</el-col>
@ -317,7 +318,7 @@
<el-row>
<el-col :span="12">
<el-form-item label="计量单位" prop="unit">
<el-form-item label="计量单位" prop="unit" v-if="hasFieldPermission('unit')">
<el-input v-model="form.unit" placeholder=": , , " />
</el-form-item>
</el-col>
@ -329,7 +330,7 @@
</el-col>
</el-row>
<el-form-item label="产品图" prop="generalImage">
<el-form-item label="产品图" prop="generalImage" v-if="hasFieldPermission('files')">
<div class="upload-container">
<el-upload
v-model:file-list="fileListImage"
@ -357,7 +358,7 @@
</el-input>
</el-form-item>
<el-form-item label="说明书" prop="generalManual">
<el-form-item label="说明书" prop="generalManual" v-if="hasFieldPermission('files')">
<div class="upload-container">
<el-upload
v-model:file-list="fileListManual"
@ -385,7 +386,7 @@
</el-input>
</el-form-item>
<el-form-item label="状态" prop="isEnabled">
<el-form-item label="状态" prop="isEnabled" v-if="hasFieldPermission('isEnabled')">
<el-radio-group v-model="form.isEnabled">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">禁用</el-radio>
@ -420,6 +421,7 @@ import { ref, reactive, onMounted, nextTick } from 'vue';
import { Plus, Document, Refresh, Setting, Rank, Camera, Link, Download } from '@element-plus/icons-vue';
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
import { useUserStore } from '@/stores/user';
import {
listMaterialBase,
@ -432,6 +434,8 @@ import {
import { uploadFile, deleteFile } from '@/api/common/upload';
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue';
const userStore = useUserStore();
// --- 类型定义 ---
interface MaterialBaseVO {
id: number;
@ -501,6 +505,52 @@ const columns = reactive({
isEnabled: { visible: true }
});
// 列与权限Code的映射关系数据库中的code
const permissionMap: Record<string, string> = {
id: 'material_list:id',
companyName: 'material_list:companyName',
name: 'material_list:name',
commonName: 'material_list:commonName',
category: 'material_list:category',
type: 'material_list:type',
spec: 'material_list:spec',
unit: 'material_list:unit',
inventory: 'material_list:inventoryCount',
available: 'material_list:availableCount',
files: 'material_list:files',
isEnabled: 'material_list:isEnabled'
};
// 根据用户权限初始化列显示状态
const initColumnPermissions = () => {
// 超级管理员跳过权限检查,显示所有列
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return;
}
// 普通用户:严格执行列级权限控制,没有权限的列必须隐藏
Object.keys(columns).forEach(key => {
const code = permissionMap[key];
if (code) {
// 如果不具备该权限,必须设为 false
columns[key].visible = !!userStore.hasPermission(code);
}
});
};
// 检查字段权限(用于表单)
const hasFieldPermission = (field: string) => {
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return true;
}
const code = permissionMap[field];
// 如果permissionMap中没有该字段默认允许
if (!code) {
return true;
}
return userStore.hasPermission(code);
};
const companyOptions = ref<string[]>([]);
const categoryOptions = ref<string[]>([]);
const typeOptions = ref<string[]>([]);
@ -988,6 +1038,8 @@ const handleCameraConfirm = async (file: File) => {
};
onMounted(() => {
// 先根据权限初始化列显示状态
initColumnPermissions();
getList();
getOptionsList();
});

View File

@ -8,14 +8,14 @@
<span class="subtitle">(请添加需要出库的物品)</span>
</div>
<div>
<el-button type="primary" :icon="Plus" @click="openManualSelect">
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="primary" :icon="Plus" @click="openManualSelect">
手动添加库存
</el-button>
<el-button type="warning" :icon="List" @click="openBomSelect">
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="warning" :icon="List" @click="openBomSelect">
BOM 套餐添加
</el-button>
<el-divider direction="vertical" />
<el-button type="success" :icon="Printer" :disabled="selectedItems.length === 0" @click="handlePreview">
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="success" :icon="Printer" :disabled="selectedItems.length === 0" @click="handlePreview">
生成预览 & 打印
</el-button>
</div>
@ -71,6 +71,7 @@
size="small"
style="width: 100%"
controls-position="right"
:disabled="!userStore.hasPermission('outbound_selection:operation')"
@change="(val) => handleMainQuantityChange(val, row)"
/>
</template>
@ -78,7 +79,7 @@
<el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="{ $index }">
<el-button type="danger" link @click="removeRow($index)">移除</el-button>
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="danger" link @click="removeRow($index)">移除</el-button>
</template>
</el-table-column>
</el-table>
@ -133,6 +134,7 @@
size="small"
style="width: 100%"
placeholder="0"
:disabled="!userStore.hasPermission('outbound_selection:operation')"
@click.stop
@change="(val) => handleManualQuantityChange(val, row)"
/>
@ -144,7 +146,7 @@
已勾选 {{ tempSelection.length }}
</span>
<el-button @click="manualDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmManualAdd">确认添加</el-button>
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="primary" @click="confirmManualAdd">确认添加</el-button>
</template>
</el-dialog>
@ -161,7 +163,7 @@
</el-select>
</el-form-item>
<el-form-item label="生产套数">
<el-input-number v-model="bomSets" :min="1" label="套" style="width: 200px;" />
<el-input-number v-model="bomSets" :min="1" label="套" style="width: 200px;" :disabled="!userStore.hasPermission('outbound_selection:operation')" />
</el-form-item>
</el-form>
<div style="margin-left: 100px; color: #909399; font-size: 12px;">
@ -169,7 +171,7 @@
</div>
<template #footer>
<el-button @click="bomSelectVisible = false">取消</el-button>
<el-button type="primary" @click="confirmBomAdd">一键计算并添加</el-button>
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="primary" @click="confirmBomAdd">一键计算并添加</el-button>
</template>
</el-dialog>
@ -204,11 +206,11 @@
<span class="dialog-footer">
<el-button @click="previewVisible = false">取消</el-button>
<el-button type="warning" :icon="Download" :loading="exportLoading" @click="confirmExport">
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="warning" :icon="Download" :loading="exportLoading" @click="confirmExport">
导出 Excel
</el-button>
<el-button type="primary" :icon="Printer" :loading="printLoading" @click="confirmPrint">
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="primary" :icon="Printer" :loading="printLoading" @click="confirmPrint">
确认打印 (A4)
</el-button>
</span>
@ -283,6 +285,9 @@ import { Printer, Search, Plus, Download, List } from '@element-plus/icons-vue'
import { ElMessage, ElTable, ElMessageBox } from 'element-plus'
import { getAllStock, printSelectionList } from '@/api/inbound/stock'
import { getBomList, getBomDetail } from '@/api/bom'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// --- 状态变量 ---
const selectedItems = ref<any[]>([])

View File

@ -17,10 +17,14 @@
<div class="scan-section">
<div class="camera-placeholder" @click="showCamera = true">
<div v-if="userStore.hasPermission('outbound_create:operation')" class="camera-placeholder" @click="showCamera = true">
<el-icon :size="40" color="#409EFF"><CameraFilled /></el-icon>
<span class="text">点击开启全屏扫码</span>
</div>
<div v-else class="camera-placeholder" style="background-color: #f5f5f5; cursor: not-allowed;">
<el-icon :size="40" color="#909399"><CameraFilled /></el-icon>
<span class="text">无扫码权限</span>
</div>
<div class="input-box">
<el-input
@ -30,12 +34,13 @@
clearable
ref="barcodeRef"
size="large"
:disabled="!userStore.hasPermission('outbound_create:operation')"
>
<template #prefix>
<el-icon><Scissor /></el-icon>
</template>
<template #append>
<el-button @click="handleManualInput">添加</el-button>
<el-button @click="handleManualInput" :disabled="!userStore.hasPermission('outbound_create:operation')">添加</el-button>
</template>
</el-input>
</div>
@ -64,13 +69,14 @@
:max="parseFloat(row.available_quantity)"
size="small"
style="width: 100px"
:disabled="!userStore.hasPermission('outbound_create:operation')"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="60" align="center" fixed="right">
<template #default="{$index}">
<el-button type="danger" icon="Delete" circle size="small" @click="removeFromCart($index)" />
<el-button v-if="userStore.hasPermission('outbound_create:operation')" type="danger" icon="Delete" circle size="small" @click="removeFromCart($index)" />
</template>
</el-table-column>
</el-table>
@ -120,7 +126,7 @@
</el-form-item>
<el-form-item label="电子签名确认" required>
<div class="signature-box" @click="openSignatureDialog">
<div class="signature-box" @click="openSignatureDialog" v-if="userStore.hasPermission('outbound_create:operation')">
<div v-if="signaturePreviewUrl" class="signed-img">
<img :src="signaturePreviewUrl" alt="签名" />
<span class="re-sign-tip">点击重签</span>
@ -130,11 +136,17 @@
<span>点击此处进行全屏签名</span>
</div>
</div>
<div v-else class="signature-box" style="background-color: #f5f5f5; cursor: not-allowed;">
<div class="unsigned-placeholder">
<el-icon :size="24"><EditPen /></el-icon>
<span>无签名权限</span>
</div>
</div>
</el-form-item>
<div class="bottom-actions">
<el-button @click="clearAll" icon="Refresh">清空</el-button>
<el-button type="primary" size="large" :loading="loading" @click="submitForm" icon="Select">
<el-button v-if="userStore.hasPermission('outbound_create:operation')" @click="clearAll" icon="Refresh">清空</el-button>
<el-button v-if="userStore.hasPermission('outbound_create:operation')" type="primary" size="large" :loading="loading" @click="submitForm" icon="Select">
确认出库
</el-button>
</div>
@ -205,6 +217,8 @@ import { getStockByBarcode, submitOutbound, getOutboundList } from '@/api/outbou
import { uploadFile } from '@/api/common/upload'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// --- 状态定义 ---
const barcodeInput = ref('')
const cartItems = ref<any[]>([])
@ -212,7 +226,6 @@ const loading = ref(false)
const showCamera = ref(false)
const barcodeRef = ref()
const formRef = ref()
const userStore = useUserStore()
// 签名相关
const showSignatureDialog = ref(false)
@ -292,6 +305,10 @@ const onScanSuccess = (code: string) => {
}
const handleManualInput = async () => {
if (!userStore.hasPermission('outbound_create:operation')) {
ElMessage.warning('无操作权限')
return
}
const code = barcodeInput.value.trim()
if (!code) return
@ -355,10 +372,18 @@ const handleManualInput = async () => {
}
const removeFromCart = (index: number) => {
if (!userStore.hasPermission('outbound_create:operation')) {
ElMessage.warning('无操作权限')
return
}
cartItems.value.splice(index, 1)
}
const clearAll = () => {
if (!userStore.hasPermission('outbound_create:operation')) {
ElMessage.warning('无操作权限')
return
}
ElMessageBox.confirm('确定清空所有已选商品吗?', '提示', { type: 'warning' })
.then(() => {
cartItems.value = []
@ -372,6 +397,10 @@ const clearAll = () => {
// --- 提交逻辑 ---
const submitForm = async () => {
if (!userStore.hasPermission('outbound_create:operation')) {
ElMessage.warning('无操作权限')
return
}
if (!formRef.value) return
if (cartItems.value.length === 0) return ElMessage.warning('请先添加商品')
@ -427,7 +456,13 @@ const submitForm = async () => {
}
// --- 签名逻辑 (Canvas) ---
const openSignatureDialog = () => { showSignatureDialog.value = true }
const openSignatureDialog = () => {
if (!userStore.hasPermission('outbound_create:operation')) {
ElMessage.warning('无签名权限')
return
}
showSignatureDialog.value = true
}
const initCanvas = async () => {
await nextTick()

View File

@ -20,7 +20,7 @@
value-format="YYYY-MM-DD"
/>
<el-button type="primary" class="filter-item" style="margin-left: 10px;" @click="fetchData">查询</el-button>
<el-button type="success" class="filter-item" @click="$router.push('/outbound/create')">新建出库</el-button>
<el-button v-if="userStore.hasPermission('outbound_create:operation')" type="success" class="filter-item" @click="$router.push('/outbound/create')">新建出库</el-button>
</div>
<el-table
@ -35,18 +35,18 @@
<div style="padding: 10px 40px; background: #fafafa;">
<h4 style="margin: 0 0 10px 0; color: #409EFF;">商品明细 (按单价降序)</h4>
<el-table :data="props.row.items" border size="small">
<el-table-column prop="sku" label="SKU" width="150" />
<el-table-column v-if="hasColumnPermission('sku')" prop="sku" label="SKU" width="150" />
<el-table-column prop="name" label="名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="material_type" label="类型" width="120" show-overflow-tooltip />
<el-table-column prop="category" label="类别" width="120" show-overflow-tooltip />
<el-table-column prop="spec_model" label="规格型号" min-width="150" show-overflow-tooltip />
<el-table-column v-if="hasColumnPermission('name')" prop="name" label="名称" min-width="150" show-overflow-tooltip />
<el-table-column v-if="hasColumnPermission('material_type')" prop="material_type" label="类型" width="120" show-overflow-tooltip />
<el-table-column v-if="hasColumnPermission('category')" prop="category" label="类别" width="120" show-overflow-tooltip />
<el-table-column v-if="hasColumnPermission('spec_model')" prop="spec_model" label="规格型号" min-width="150" show-overflow-tooltip />
<el-table-column prop="quantity" label="数量" width="100" />
<el-table-column prop="unit_price" label="单价" width="120">
<el-table-column v-if="hasColumnPermission('quantity')" prop="quantity" label="数量" width="100" />
<el-table-column v-if="hasColumnPermission('unit_price')" prop="unit_price" label="单价" width="120">
<template #default="{row}">¥{{ row.unit_price }}</template>
</el-table-column>
<el-table-column prop="subtotal" label="小计">
<el-table-column v-if="hasColumnPermission('subtotal')" prop="subtotal" label="小计">
<template #default="{row}">
<span style="color: #F56C6C; font-weight: bold;">¥{{ row.subtotal.toFixed(2) }}</span>
</template>
@ -56,33 +56,33 @@
</template>
</el-table-column>
<el-table-column prop="outbound_no" label="出库单号" min-width="200" show-overflow-tooltip />
<el-table-column v-if="hasColumnPermission('outbound_no')" prop="outbound_no" label="出库单号" min-width="200" show-overflow-tooltip />
<el-table-column prop="outbound_time" label="出库时间" width="170" align="center">
<el-table-column v-if="hasColumnPermission('outbound_time')" prop="outbound_time" label="出库时间" width="170" align="center">
<template #default="{ row }">
<span>{{ row.outbound_time ? row.outbound_time.substring(0, 16) : '' }}</span>
</template>
</el-table-column>
<el-table-column prop="outbound_type" label="类型" width="100" align="center">
<el-table-column v-if="hasColumnPermission('outbound_type')" prop="outbound_type" label="类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getTagType(row.outbound_type)">{{ formatType(row.outbound_type) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="total_amount" label="总金额" width="120" align="right">
<el-table-column v-if="hasColumnPermission('total_amount')" prop="total_amount" label="总金额" width="120" align="right">
<template #default="{ row }">
<span style="color: #F56C6C; font-weight: bold; font-size: 15px;">¥{{ row.total_amount }}</span>
</template>
</el-table-column>
<el-table-column prop="consumer_name" label="领用/客户" min-width="120" show-overflow-tooltip />
<el-table-column v-if="hasColumnPermission('consumer_name')" prop="consumer_name" label="领用/客户" min-width="120" show-overflow-tooltip />
<el-table-column prop="operator_name" label="操作员" min-width="100" show-overflow-tooltip />
<el-table-column v-if="hasColumnPermission('operator_name')" prop="operator_name" label="操作员" min-width="100" show-overflow-tooltip />
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
<el-table-column v-if="hasColumnPermission('remark')" prop="remark" label="备注" min-width="150" show-overflow-tooltip />
<el-table-column label="签名" width="120" align="center">
<el-table-column v-if="hasColumnPermission('signature_path')" label="签名" width="120" align="center">
<template #default="{ row }">
<div v-if="row.signature_path" class="signature-cell">
<el-image
@ -121,6 +121,39 @@
import { ref, onMounted, reactive } from 'vue'
import { getOutboundList } from '@/api/outbound'
import { Picture } from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 列与权限Code的映射关系数据库中的code
const permissionMap: Record<string, string> = {
outbound_no: 'outbound_list:outbound_no',
outbound_time: 'outbound_list:outbound_time',
outbound_type: 'outbound_list:outbound_type',
total_amount: 'outbound_list:total_amount',
consumer_name: 'outbound_list:consumer_name',
operator_name: 'outbound_list:operator_name',
remark: 'outbound_list:remark',
signature_path: 'outbound_list:signature_path',
// 明细列
sku: 'outbound_list:sku',
name: 'outbound_list:name',
material_type: 'outbound_list:material_type',
category: 'outbound_list:category',
spec_model: 'outbound_list:spec_model',
quantity: 'outbound_list:quantity',
unit_price: 'outbound_list:unit_price',
subtotal: 'outbound_list:subtotal',
}
// 检查列权限
const hasColumnPermission = (prop: string) => {
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return true
}
const code = permissionMap[prop]
return code ? userStore.hasPermission(code) : false
}
const list = ref([])
const total = ref(0)

View File

@ -56,7 +56,7 @@
</div>
<div class="right-actions">
<el-button type="primary" :icon="Plus" @click="handleCreate" class="add-btn">新增</el-button>
<el-button v-if="userStore.hasPermission('inbound_buy:operation')" type="primary" :icon="Plus" @click="handleCreate" class="add-btn">新增</el-button>
<el-button :icon="Refresh" circle @click="fetchData" class="circle-btn" />
<el-popover placement="bottom-end" title="列配置" :width="500" trigger="click">
@ -67,13 +67,13 @@
<div class="col-group-title">基础信息</div>
<el-row :gutter="10">
<el-col :span="12" v-for="c in baseColumns" :key="c.prop">
<el-checkbox :label="c.prop">{{ c.label }}</el-checkbox>
<el-checkbox :label="c.prop" :disabled="!hasColumnPermission(c.prop)">{{ c.label }}</el-checkbox>
</el-col>
</el-row>
<div class="col-group-title" style="margin-top:10px">库存与商务</div>
<el-row :gutter="10">
<el-col :span="12" v-for="c in stockColumns" :key="c.prop">
<el-checkbox :label="c.prop">{{ c.label }}</el-checkbox>
<el-checkbox :label="c.prop" :disabled="!hasColumnPermission(c.prop)">{{ c.label }}</el-checkbox>
</el-col>
</el-row>
</el-checkbox-group>
@ -167,7 +167,7 @@
</el-table-column>
</template>
<el-table-column label="操作" width="220" fixed="right" align="center">
<el-table-column v-if="userStore.hasPermission('inbound_buy:operation')" label="操作" width="220" fixed="right" align="center">
<template #default="{ row }">
<el-button link type="warning" size="default" @click="handlePrint(row)">
<el-icon><Printer/></el-icon> 打印
@ -587,6 +587,7 @@ import {
} from '@/api/inbound/buy'
import {getLabelPreview, executePrint} from '@/api/common/print'
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
import { useUserStore } from '@/stores/user'
// ------------------------------------
// 自定义指令v-loadmore (适配 Teleport 到 Body 的下拉框)
@ -613,6 +614,7 @@ const vLoadmore = {
// ------------------------------------
// 状态与变量
// ------------------------------------
const userStore = useUserStore()
const loading = ref(false)
const submitting = ref(false)
const visible = ref(false)
@ -701,6 +703,78 @@ const stockColumns = [
{prop: 'inspection_report', label: '检测报告', minWidth: '100'}
]
// 列与权限Code的映射关系数据库中的code
const permissionMap: Record<string, string> = {
id: 'inbound_buy:id',
base_id: 'inbound_buy:base_id',
company_name: 'inbound_buy:company_name',
material_name: 'inbound_buy:material_name',
material_type: 'inbound_buy:material_type',
category: 'inbound_buy:category',
spec_model: 'inbound_buy:spec_model',
unit: 'inbound_buy:unit',
sku: 'inbound_buy:sku',
inbound_date: 'inbound_buy:inbound_date',
barcode: 'inbound_buy:barcode',
sn_bn: 'inbound_buy:sn_bn',
status: 'inbound_buy:status',
inspection_status: 'inbound_buy:inspection_status',
qty_inbound: 'inbound_buy:qty_inbound',
qty_stock: 'inbound_buy:qty_stock',
qty_available: 'inbound_buy:qty_available',
warehouse_loc: 'inbound_buy:warehouse_loc',
tax_rate: 'inbound_buy:tax_rate',
unit_price: 'inbound_buy:unit_price',
total_price: 'inbound_buy:total_price',
currency: 'inbound_buy:currency',
exchange_rate: 'inbound_buy:exchange_rate',
supplier_name: 'inbound_buy:supplier_name',
purchaser: 'inbound_buy:purchaser',
purchaser_email: 'inbound_buy:purchaser_email',
source_link: 'inbound_buy:source_link',
detail_link: 'inbound_buy:detail_link',
arrival_photo: 'inbound_buy:arrival_photo',
inspection_report: 'inbound_buy:inspection_report'
}
// 根据用户权限初始化列显示状态
const initColumnPermissions = () => {
// 超级管理员跳过权限检查,显示所有列
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return
}
// 普通用户:严格执行列级权限控制,没有权限的列必须隐藏
// 遍历 allColumns将没有权限的列从 visibleColumnProps 中移除
const allowedColumns = allColumns.filter(col => {
const code = permissionMap[col.prop]
if (code) {
return userStore.hasPermission(code)
}
// 如果没有映射,默认隐藏
return false
}).map(col => col.prop)
// 更新 visibleColumnProps只保留有权限的列
// 同时保持用户之前已经选择的有权限的列
const currentVisible = visibleColumnProps.value.filter(prop => allowedColumns.includes(prop))
// 如果当前没有可见列,则使用 allowedColumns 作为默认
if (currentVisible.length === 0) {
visibleColumnProps.value = allowedColumns
} else {
visibleColumnProps.value = currentVisible
}
}
// 检查列权限
const hasColumnPermission = (prop: string) => {
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return true
}
const code = permissionMap[prop]
return code ? userStore.hasPermission(code) : false
}
const allColumns = [...baseColumns, ...stockColumns]
const defaultColumns = [
@ -1180,6 +1254,8 @@ const getStatusType = (status: string) => { const map: any = {'在库': 'success
const formatMoney = (val: any, currency = '¥') => { const num = Number(val); return isNaN(num) ? '-' : `${currency} ${num.toFixed(2)}` }
onMounted(() => {
// 先根据权限初始化列显示状态
initColumnPermissions()
fetchData()
fetchOptions()
})

View File

@ -41,13 +41,13 @@
</div>
<div class="right-tools">
<el-button type="primary" :icon="Plus" @click="handleCreate" class="action-btn">成品入库登记</el-button>
<el-button v-if="userStore.hasPermission('inbound_product:operation')" type="primary" :icon="Plus" @click="handleCreate" class="action-btn">成品入库登记</el-button>
<el-button :icon="Refresh" @click="fetchData" class="action-btn">刷新</el-button>
<el-popover placement="bottom-end" title="列配置" :width="500" trigger="click">
<template #reference><el-button :icon="Setting" class="action-btn">表头</el-button></template>
<el-checkbox-group v-model="visibleColumnProps" class="column-selector">
<el-row :gutter="10">
<el-col :span="8" v-for="c in allColumns" :key="c.prop"><el-checkbox :label="c.prop">{{ c.label }}</el-checkbox></el-col>
<el-col :span="8" v-for="c in allColumns" :key="c.prop"><el-checkbox :label="c.prop" :disabled="!hasColumnPermission(c.prop)">{{ c.label }}</el-checkbox></el-col>
</el-row>
</el-checkbox-group>
</el-popover>
@ -137,7 +137,7 @@
</el-table-column>
</template>
<el-table-column label="操作" width="180" fixed="right" align="center">
<el-table-column v-if="userStore.hasPermission('inbound_product:operation')" label="操作" width="180" fixed="right" align="center">
<template #default="{ row }">
<el-button link type="warning" size="default" @click="handlePrint(row)">
<el-icon><Printer/></el-icon>
@ -211,12 +211,12 @@
</el-row>
<div class="read-only-grid">
<el-row :gutter="24">
<el-col :span="8"><el-form-item label="所属公司"><el-input v-model="form.company_name" readonly class="is-text-view" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" readonly class="is-text-view" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="规格"><el-input v-model="form.spec_model" readonly class="is-text-view" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="单位"><el-input v-model="form.unit" readonly class="is-text-view" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" readonly class="is-text-view" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="类别"><el-input v-model="form.category" readonly class="is-text-view" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="所属公司" v-if="hasFormFieldPermission('company_name')"><el-input v-model="form.company_name" readonly class="is-text-view" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="名称" v-if="hasFormFieldPermission('material_name')"><el-input v-model="form.material_name" readonly class="is-text-view" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="规格" v-if="hasFormFieldPermission('spec_model')"><el-input v-model="form.spec_model" readonly class="is-text-view" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="单位" v-if="hasFormFieldPermission('unit')"><el-input v-model="form.unit" readonly class="is-text-view" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="类型" v-if="hasFormFieldPermission('material_type')"><el-input v-model="form.material_type" readonly class="is-text-view" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="类别" v-if="hasFormFieldPermission('category')"><el-input v-model="form.category" readonly class="is-text-view" /></el-form-item></el-col>
</el-row>
</div>
</div>
@ -432,6 +432,7 @@ import {
import { uploadFile, deleteFile } from '@/api/inbound/buy'
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
import { getLabelPreview, executePrint } from '@/api/common/print'
import { useUserStore } from '@/stores/user'
// ------------------------------------
// v-loadmore
@ -455,6 +456,7 @@ const vLoadmore = {
}
}
const userStore = useUserStore()
const loading = ref(false)
const submitting = ref(false)
const visible = ref(false)
@ -523,6 +525,69 @@ const allColumns = [
{ prop: 'detail_link', label: '详情', minWidth: '100' }
]
// 列与权限Code的映射关系数据库中的code
const permissionMap: Record<string, string> = {
company_name: 'inbound_product:company_name',
material_name: 'inbound_product:material_name',
sku: 'inbound_product:sku',
serial_number: 'inbound_product:serial_number',
qty_stock: 'inbound_product:stock_quantity',
status: 'inbound_product:status',
quality_status: 'inbound_product:quality_status',
spec_model: 'inbound_product:spec_model',
unit: 'inbound_product:unit',
product_photo: 'inbound_product:product_photo',
sale_price: 'inbound_product:sale_price',
order_id: 'inbound_product:order_id',
work_order_code: 'inbound_product:work_order_code',
quality_report_link: 'inbound_product:quality_report_link',
inspection_report_link: 'inbound_product:inspection_report_link',
bom_code: 'inbound_product:bom_code',
production_manager: 'inbound_product:production_manager',
raw_material_cost: 'inbound_product:raw_material_cost',
manual_cost: 'inbound_product:manual_cost',
inbound_date: 'inbound_product:inbound_date',
detail_link: 'inbound_product:detail_link',
}
// 根据用户权限初始化列显示状态
const initColumnPermissions = () => {
// 超级管理员跳过权限检查,显示所有列
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return
}
// 普通用户:严格执行列级权限控制,没有权限的列必须隐藏
// 遍历 allColumns将没有权限的列从 visibleColumnProps 中移除
const allowedColumns = allColumns.filter(col => {
const code = permissionMap[col.prop]
if (code) {
return userStore.hasPermission(code)
}
// 如果没有映射,默认隐藏
return false
}).map(col => col.prop)
// 更新 visibleColumnProps只保留有权限的列
// 同时保持用户之前已经选择的有权限的列
const currentVisible = visibleColumnProps.value.filter(prop => allowedColumns.includes(prop))
// 如果当前没有可见列,则使用 allowedColumns 作为默认
if (currentVisible.length === 0) {
visibleColumnProps.value = allowedColumns
} else {
visibleColumnProps.value = currentVisible
}
}
// 检查列权限
const hasColumnPermission = (prop: string) => {
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return true
}
const code = permissionMap[prop]
return code ? userStore.hasPermission(code) : false
}
const defaultVisibleCols = ['company_name', 'material_name', 'sku', 'serial_number', 'qty_stock', 'status', 'quality_status', 'product_photo', 'sale_price', 'order_id']
const visibleColumnProps = ref(defaultVisibleCols)
@ -560,6 +625,54 @@ const handleBomSelect = (val: string) => {
form.bom_version = version
}
// ------------------------------------
// 表单字段权限检查
// ------------------------------------
const hasFormFieldPermission = (fieldName: string) => {
// 超级管理员直接返回true
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return true
}
// 根据字段名映射到权限码
const map: Record<string, string> = {
company_name: 'inbound_product:company_name',
material_name: 'inbound_product:material_name',
spec_model: 'inbound_product:spec_model',
material_type: 'inbound_product:material_type',
category: 'inbound_product:category',
unit: 'inbound_product:unit',
sku: 'inbound_product:sku',
barcode: 'inbound_product:barcode',
serial_number: 'inbound_product:serial_number',
in_date: 'inbound_product:inbound_date',
in_quantity: 'inbound_product:in_quantity',
stock_quantity: 'inbound_product:stock_quantity',
available_quantity: 'inbound_product:available_quantity',
warehouse_location: 'inbound_product:warehouse_location',
status: 'inbound_product:status',
quality_status: 'inbound_product:quality_status',
bom_code: 'inbound_product:bom_code',
bom_version: 'inbound_product:bom_version',
work_order_code: 'inbound_product:work_order_code',
order_id: 'inbound_product:order_id',
production_manager: 'inbound_product:production_manager',
production_time_range: 'inbound_product:production_start_time',
raw_material_cost: 'inbound_product:raw_material_cost',
manual_cost: 'inbound_product:manual_cost',
sale_price: 'inbound_product:sale_price',
quality_report_link: 'inbound_product:quality_report_link',
inspection_report_link: 'inbound_product:inspection_report_link',
product_photo: 'inbound_product:product_photo',
detail_link: 'inbound_product:detail_link',
}
const code = map[fieldName]
if (!code) {
// 没有映射的字段默认显示
return true
}
return userStore.hasPermission(code)
}
// ------------------------------------
// Validation Logic
// ------------------------------------
@ -834,6 +947,8 @@ const getStatusType = (s:string) => ({'在库':'success','出库':'info','借库
const getQualityType = (s:string) => ({'合格':'success','不合格':'danger','待检':'info'}[s]||'info')
const formatMoney = (val:any) => isNaN(Number(val)) ? '-' : `¥ ${Number(val).toFixed(2)}`
onMounted(() => {
// 先根据权限初始化列显示状态
initColumnPermissions()
fetchData()
fetchOptions()
})

View File

@ -69,7 +69,7 @@
</div>
<div class="right-tools">
<el-button type="primary" :icon="Plus" @click="handleCreate" class="add-btn">半成品入库</el-button>
<el-button v-if="userStore.hasPermission('inbound_semi:operation')" type="primary" :icon="Plus" @click="handleCreate" class="add-btn">半成品入库</el-button>
<el-button :icon="Refresh" circle @click="fetchData" class="circle-btn" />
<el-popover placement="bottom-end" title="列配置" :width="500" trigger="click">
@ -80,13 +80,13 @@
<div class="col-group-title">基础信息</div>
<el-row :gutter="10">
<el-col :span="12" v-for="c in baseColumns" :key="c.prop">
<el-checkbox :label="c.prop">{{ c.label }}</el-checkbox>
<el-checkbox :label="c.prop" :disabled="!hasColumnPermission(c.prop)">{{ c.label }}</el-checkbox>
</el-col>
</el-row>
<div class="col-group-title" style="margin-top:10px">生产与库存</div>
<el-row :gutter="10">
<el-col :span="12" v-for="c in stockColumns" :key="c.prop">
<el-checkbox :label="c.prop">{{ c.label }}</el-checkbox>
<el-checkbox :label="c.prop" :disabled="!hasColumnPermission(c.prop)">{{ c.label }}</el-checkbox>
</el-col>
</el-row>
</el-checkbox-group>
@ -189,7 +189,7 @@
</el-table-column>
</template>
<el-table-column label="操作" width="220" fixed="right" align="center">
<el-table-column v-if="userStore.hasPermission('inbound_semi:operation')" label="操作" width="220" fixed="right" align="center">
<template #default="{ row }">
<el-button link type="warning" size="default" @click="handlePrint(row)">
<el-icon><Printer/></el-icon> 打印
@ -293,12 +293,12 @@
<div class="read-only-grid">
<el-row :gutter="24">
<el-col :span="8"><el-form-item label="所属公司"><el-input v-model="form.company_name" readonly class="is-text-view"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" readonly class="is-text-view"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="规格型号"><el-input v-model="form.spec_model" readonly class="is-text-view"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="单位"><el-input v-model="form.unit" readonly class="is-text-view"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="类别"><el-input v-model="form.category" readonly class="is-text-view"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" readonly class="is-text-view"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="所属公司" v-if="hasFormFieldPermission('company_name')"><el-input v-model="form.company_name" readonly class="is-text-view"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="名称" v-if="hasFormFieldPermission('material_name')"><el-input v-model="form.material_name" readonly class="is-text-view"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="规格型号" v-if="hasFormFieldPermission('spec_model')"><el-input v-model="form.spec_model" readonly class="is-text-view"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="单位" v-if="hasFormFieldPermission('unit')"><el-input v-model="form.unit" readonly class="is-text-view"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="类别" v-if="hasFormFieldPermission('category')"><el-input v-model="form.category" readonly class="is-text-view"/></el-form-item></el-col>
<el-col :span="8"><el-form-item label="类型" v-if="hasFormFieldPermission('material_type')"><el-input v-model="form.material_type" readonly class="is-text-view"/></el-form-item></el-col>
</el-row>
</div>
</div>
@ -533,6 +533,7 @@ import {
import { uploadFile, deleteFile } from '@/api/inbound/buy'
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
import {getLabelPreview, executePrint} from '@/api/common/print'
import { useUserStore } from '@/stores/user'
// ------------------------------------
// 自定义指令v-loadmore (适配 Teleport 到 Body 的下拉框)
@ -559,6 +560,7 @@ const vLoadmore = {
// ------------------------------------
// 状态与变量
// ------------------------------------
const userStore = useUserStore()
const loading = ref(false)
const submitting = ref(false)
const visible = ref(false)
@ -640,6 +642,78 @@ const stockColumns = [
]
const allColumns = [...baseColumns, ...stockColumns]
// 列与权限Code的映射关系数据库中的code
const permissionMap: Record<string, string> = {
id: 'inbound_semi:id',
base_id: 'inbound_semi:base_id',
company_name: 'inbound_semi:company_name',
material_name: 'inbound_semi:material_name',
category: 'inbound_semi:category',
material_type: 'inbound_semi:material_type',
spec_model: 'inbound_semi:spec_model',
unit: 'inbound_semi:unit',
sku: 'inbound_semi:sku',
inbound_date: 'inbound_semi:inbound_date',
barcode: 'inbound_semi:barcode',
sn_bn: 'inbound_semi:sn_bn',
status: 'inbound_semi:status',
quality_status: 'inbound_semi:quality_status',
qty_inbound: 'inbound_semi:qty_inbound',
qty_stock: 'inbound_semi:qty_stock',
qty_available: 'inbound_semi:qty_available',
warehouse_loc: 'inbound_semi:warehouse_loc',
bom_code: 'inbound_semi:bom_code',
bom_version: 'inbound_semi:bom_version',
work_order_code: 'inbound_semi:work_order_code',
raw_material_cost: 'inbound_semi:raw_material_cost',
manual_cost: 'inbound_semi:manual_cost',
unit_total_cost: 'inbound_semi:unit_total_cost',
production_manager: 'inbound_semi:production_manager',
production_start_time: 'inbound_semi:production_start_time',
production_end_time: 'inbound_semi:production_end_time',
arrival_photo: 'inbound_semi:arrival_photo',
quality_report_link: 'inbound_semi:quality_report_link',
detail_link: 'inbound_semi:detail_link',
}
// 根据用户权限初始化列显示状态
const initColumnPermissions = () => {
// 超级管理员跳过权限检查,显示所有列
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return
}
// 普通用户:严格执行列级权限控制,没有权限的列必须隐藏
// 遍历 allColumns将没有权限的列从 visibleColumnProps 中移除
const allowedColumns = allColumns.filter(col => {
const code = permissionMap[col.prop]
if (code) {
return userStore.hasPermission(code)
}
// 如果没有映射,默认隐藏
return false
}).map(col => col.prop)
// 更新 visibleColumnProps只保留有权限的列
// 同时保持用户之前已经选择的有权限的列
const currentVisible = visibleColumnProps.value.filter(prop => allowedColumns.includes(prop))
// 如果当前没有可见列,则使用 allowedColumns 作为默认
if (currentVisible.length === 0) {
visibleColumnProps.value = allowedColumns
} else {
visibleColumnProps.value = currentVisible
}
}
// 检查列权限
const hasColumnPermission = (prop: string) => {
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return true
}
const code = permissionMap[prop]
return code ? userStore.hasPermission(code) : false
}
const defaultColumns = ['company_name', 'material_name', 'spec_model', 'unit', 'inbound_date', 'sn_bn', 'status', 'quality_status', 'bom_code', 'work_order_code', 'qty_stock', 'qty_available', 'unit_total_cost', 'arrival_photo', 'quality_report_link']
const visibleColumnProps = ref(defaultColumns)
@ -765,6 +839,53 @@ const rules = {
batch_number: [{validator: validateIdentity, trigger: 'blur'}, {validator: validateUnique, trigger: 'blur'}]
}
// ------------------------------------
// 表单字段权限检查
// ------------------------------------
const hasFormFieldPermission = (fieldName: string) => {
// 超级管理员直接返回true
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return true
}
// 根据字段名映射到权限码
const map: Record<string, string> = {
company_name: 'inbound_semi:company_name',
material_name: 'inbound_semi:material_name',
spec_model: 'inbound_semi:spec_model',
category: 'inbound_semi:category',
material_type: 'inbound_semi:material_type',
unit: 'inbound_semi:unit',
sku: 'inbound_semi:sku',
in_date: 'inbound_semi:inbound_date',
barcode: 'inbound_semi:barcode',
serial_number: 'inbound_semi:serial_number',
batch_number: 'inbound_semi:batch_number',
status: 'inbound_semi:status',
quality_status: 'inbound_semi:quality_status',
in_quantity: 'inbound_semi:in_quantity',
stock_quantity: 'inbound_semi:stock_quantity',
available_quantity: 'inbound_semi:available_quantity',
warehouse_location: 'inbound_semi:warehouse_location',
bom_code: 'inbound_semi:bom_code',
bom_version: 'inbound_semi:bom_version',
work_order_code: 'inbound_semi:work_order_code',
raw_material_cost: 'inbound_semi:raw_material_cost',
manual_cost: 'inbound_semi:manual_cost',
unit_total_cost: 'inbound_semi:unit_total_cost',
production_manager: 'inbound_semi:production_manager',
production_time_range: 'inbound_semi:production_start_time', // 使用开始时间权限
arrival_photo: 'inbound_semi:arrival_photo',
quality_report_link: 'inbound_semi:quality_report_link',
detail_link: 'inbound_semi:detail_link',
}
const code = map[fieldName]
if (!code) {
// 没有映射的字段默认显示
return true
}
return userStore.hasPermission(code)
}
// ------------------------------------
// Core Logic
// ------------------------------------
@ -985,6 +1106,8 @@ const getQualityType = (status: string) => { const map: any = { '合格': 'succe
const formatMoney = (val: any) => { const num = Number(val); return isNaN(num) ? '-' : `¥ ${num.toFixed(2)}` }
onMounted(() => {
// 先根据权限初始化列显示状态
initColumnPermissions()
fetchData()
fetchOptions()
})

View File

@ -45,21 +45,21 @@
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
<el-button type="success" @click="handleAdd">新增服务</el-button>
<el-button v-if="userStore.hasPermission('inbound_service:operation')" type="success" @click="handleAdd">新增服务</el-button>
</el-form-item>
</el-form>
</div>
<el-table :data="tableData" border stripe style="width: 100%;" v-loading="loading">
<el-table-column prop="sku" label="SKU" width="200" />
<el-table-column prop="material_name" label="物料名称" />
<el-table-column prop="provider_name" label="服务商" width="150" />
<el-table-column prop="sale_price" label="售价" width="120">
<el-table-column v-if="hasColumnPermission('sku')" prop="sku" label="SKU" width="200" />
<el-table-column v-if="hasColumnPermission('material_name')" prop="material_name" label="物料名称" />
<el-table-column v-if="hasColumnPermission('provider_name')" prop="provider_name" label="服务商" width="150" />
<el-table-column v-if="hasColumnPermission('sale_price')" prop="sale_price" label="售价" width="120">
<template #default="{row}">{{ row.sale_price.toFixed(2) }}</template>
</el-table-column>
<el-table-column prop="description" label="简介" show-overflow-tooltip />
<el-table-column prop="created_at" label="创建时间" width="160" />
<el-table-column label="操作" width="180" fixed="right">
<el-table-column v-if="hasColumnPermission('description')" prop="description" label="简介" show-overflow-tooltip />
<el-table-column v-if="hasColumnPermission('created_at')" prop="created_at" label="创建时间" width="160" />
<el-table-column v-if="userStore.hasPermission('inbound_service:operation')" label="操作" width="180" fixed="right">
<template #default="{row}">
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
@ -198,6 +198,7 @@ import { ref, reactive, onMounted } from 'vue'
import { InfoFilled, Box, House } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useUserStore } from '@/stores/user'
import {
getServiceList,
createService,
@ -212,6 +213,27 @@ import {
type MaterialBaseItem
} from '@/api/inbound/service'
const userStore = useUserStore()
// 列与权限Code的映射关系数据库中的code
const permissionMap: Record<string, string> = {
sku: 'inbound_service:sku',
material_name: 'inbound_service:material_name',
provider_name: 'inbound_service:provider_name',
sale_price: 'inbound_service:sale_price',
description: 'inbound_service:description',
created_at: 'inbound_service:created_at',
}
// 检查列权限
const hasColumnPermission = (prop: string) => {
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return true
}
const code = permissionMap[prop]
return code ? userStore.hasPermission(code) : false
}
// 表格数据
const tableData = ref<ServiceItem[]>([])
const loading = ref(false)

View File

@ -10,12 +10,12 @@
<p class="subtitle">单件自动确认多件弹窗录入</p>
<div class="idle-actions">
<el-button type="primary" size="large" class="action-btn-full" @click="startNewSession" :loading="btnLoading">
<el-button v-if="userStore.hasPermission('inventory_stocktake:operation')" type="primary" size="large" class="action-btn-full" @click="startNewSession" :loading="btnLoading">
开始新盘点
</el-button>
<el-button
v-if="serverDraftCount > 0"
v-if="serverDraftCount > 0 && userStore.hasPermission('inventory_stocktake:operation')"
type="warning"
plain
size="large"
@ -43,7 +43,7 @@
<el-tag v-else-if="syncStatus === 'syncing'" type="warning" size="small" effect="dark" round>同步中...</el-tag>
<el-tag v-else type="danger" size="small" effect="dark" round>同步失败</el-tag>
</div>
<el-button type="info" text bg size="small" @click="pauseSession" :icon="VideoPause">
<el-button v-if="userStore.hasPermission('inventory_stocktake:operation')" type="info" text bg size="small" @click="pauseSession" :icon="VideoPause">
暂停
</el-button>
</div>
@ -84,7 +84,7 @@
</el-button>
</el-col>
<el-col :span="12">
<el-button type="danger" size="large" class="w-100 action-btn" @click="openFinishDialog" :icon="Checked">
<el-button v-if="userStore.hasPermission('inventory_stocktake:operation')" type="danger" size="large" class="w-100 action-btn" @click="openFinishDialog" :icon="Checked">
结束盘点
</el-button>
</el-col>
@ -139,7 +139,7 @@
<template #footer>
<div class="dialog-footer">
<el-button @click="showQtyDialog = false">取消</el-button>
<el-button type="primary" @click="handleManualConfirm" size="large">确认数量</el-button>
<el-button v-if="userStore.hasPermission('inventory_stocktake:operation')" type="primary" @click="handleManualConfirm" size="large">确认数量</el-button>
</div>
</template>
</el-dialog>
@ -190,6 +190,7 @@
<el-table-column label="操作" width="90" align="center" fixed="right">
<template #default="scope">
<el-button
v-if="userStore.hasPermission('inventory_stocktake:operation')"
type="primary"
link
icon="Edit"
@ -237,7 +238,7 @@
<el-button @click="showFinishDialog = false">返回修改</el-button>
<div class="footer-right">
<el-button type="success" @click="exportToExcel" :icon="Download">导出Excel</el-button>
<el-button type="danger" @click="finishStocktake" :loading="printing" :icon="Checked">结束</el-button>
<el-button v-if="userStore.hasPermission('inventory_stocktake:operation')" type="danger" @click="finishStocktake" :loading="printing" :icon="Checked">结束</el-button>
</div>
</div>
</template>

View File

@ -0,0 +1,627 @@
<template>
<div class="app-container">
<el-card class="permission-card" shadow="never">
<template #header>
<div class="card-header">
<div class="header-left">
<div class="title-block">
<span class="main-title">权限配置中心</span>
<span class="sub-title">设定各角色的系统访问级别与数据可见性</span>
</div>
</div>
<el-button
type="primary"
:loading="saving"
@click="handleSave"
:disabled="!currentRole"
size="large"
icon="Check"
class="save-btn"
>
保存配置
</el-button>
</div>
</template>
<div class="main-layout">
<div class="left-panel">
<div class="panel-header">
<el-icon><User /></el-icon> 角色列表
</div>
<el-scrollbar>
<ul class="role-list">
<li
v-for="role in roleList"
:key="role.value"
:class="['role-item', { active: currentRole === role.value }]"
@click="handleRoleSelect(role.value)"
>
<div class="role-icon">
<el-icon v-if="role.value === 'SUPER_ADMIN'"><Avatar /></el-icon>
<el-icon v-else><UserFilled /></el-icon>
</div>
<div class="role-info">
<span class="role-name">{{ role.label }}</span>
<span class="role-code">{{ role.value }}</span>
</div>
<el-icon v-if="currentRole === role.value" class="arrow-icon"><ArrowRight /></el-icon>
</li>
</ul>
</el-scrollbar>
</div>
<div class="right-panel" v-loading="loading">
<div v-if="!currentRole" class="empty-state">
<img src="https://gw.alipayobjects.com/zos/antfincdn/ZHrcdLPrvN/empty.svg" alt="empty" style="width: 200px; opacity: 0.5;" />
<p>请在左侧选择一个角色开始配置</p>
</div>
<div v-else class="config-content">
<div class="panel-header">
<span>权限明细</span>
<el-tag type="warning" effect="plain" round>当前配置: {{ getRoleLabel(currentRole) }}</el-tag>
</div>
<el-table
:data="tableData"
row-key="id"
border
default-expand-all
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
class="permission-table"
>
<el-table-column prop="name" label="页面 / 模块" min-width="200">
<template #default="{ row }">
<span style="font-weight: 500;">{{ row.name }}</span>
</template>
</el-table-column>
<el-table-column label="访问权限" width="150" align="center">
<template #default="{ row }">
<el-checkbox
v-model="row.hasRead"
@change="(val) => handleReadChange(val, row)"
class="custom-checkbox"
>
<span :class="{ 'text-active': row.hasRead }">可见 (Read)</span>
</el-checkbox>
</template>
</el-table-column>
<el-table-column label="操作权限" width="180" align="center">
<template #default="{ row }">
<div v-if="row.operationCode">
<el-checkbox
v-model="row.hasWrite"
:disabled="!row.hasRead"
@change="(val) => handleWriteChange(val, row)"
class="custom-checkbox"
>
<span :class="{ 'text-active': row.hasWrite, 'text-disabled': !row.hasRead }">可编辑 (Write)</span>
</el-checkbox>
</div>
<span v-else class="text-gray">-</span>
</template>
</el-table-column>
<el-table-column label="字段级控制 (敏感列)" min-width="300">
<template #default="{ row }">
<div v-if="row.elements && row.elements.length > 0">
<el-popover placement="left" :width="300" trigger="click">
<template #reference>
<el-button link type="primary" :disabled="!row.hasRead">
<el-icon style="margin-right: 4px"><Setting /></el-icon>
配置 {{ getCheckedCount(row) }}/{{ row.elements.length }} 个字段
</el-button>
</template>
<div class="field-config-box">
<div class="field-header">
<span>勾选可见字段</span>
<el-checkbox
v-model="row.checkAllElements"
:indeterminate="row.isIndeterminate"
@change="(val) => handleCheckAllElements(val, row)"
size="small"
>全选</el-checkbox>
</div>
<el-checkbox-group v-model="row.checkedElements" @change="(val) => handleCheckedElementsChange(val, row)">
<el-row :gutter="10">
<el-col :span="12" v-for="col in row.elements" :key="col.code">
<el-checkbox :label="col.code" :title="col.name" style="width: 100%; overflow: hidden; text-overflow: ellipsis;">
{{ col.name }}
</el-checkbox>
</el-col>
</el-row>
</el-checkbox-group>
</div>
</el-popover>
<span class="preview-tags">
<span v-for="code in row.checkedElements.slice(0, 2)" :key="code" class="mini-tag">{{ getElementName(row, code) }}</span>
<span v-if="row.checkedElements.length > 2" class="mini-tag">...</span>
</span>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { User, UserFilled, ArrowRight, Setting, Check, Avatar } from '@element-plus/icons-vue'
import { getAllPermissionTree, getRolePermissions, saveRolePermissions } from '@/api/system/permission'
// --- 类型定义 ---
interface PermissionNode {
id: number
name: string
code: string
type: string
children?: PermissionNode[]
// 前端辅助字段
hasRead: boolean // 是否勾选了页面
hasWrite: boolean // 是否勾选了 operation
operationCode: string | null // 存储 operation 的 code
elements: any[] // 存储该页面的普通列
checkedElements: string[] // 已勾选的列code
checkAllElements: boolean
isIndeterminate: boolean
}
// --- 状态定义 ---
const loading = ref(false)
const saving = ref(false)
const currentRole = ref('')
const roleList = [
{ label: '超级管理员', value: 'SUPER_ADMIN' },
{ label: '主管', value: 'SUPERVISOR' },
{ label: '财务', value: 'FINANCE' },
{ label: '库管', value: 'WAREHOUSE_MGR' },
{ label: '入库员', value: 'INBOUND' },
{ label: '出库员', value: 'OUTBOUND' },
{ label: '采购员', value: 'PURCHASER' },
{ label: '销售', value: 'SALES' }
]
// 原始树数据
const rawTreeData = ref<any[]>([])
// 表格展示数据 (经过处理)
const tableData = ref<PermissionNode[]>([])
// --- 方法 ---
// 1. 初始化:获取权限树结构
const fetchTree = async () => {
try {
const res: any = await getAllPermissionTree()
if (res.code === 200) {
rawTreeData.value = res.data
// 初始化表格结构(此时没有勾选状态)
tableData.value = transformData(res.data)
}
} catch (e) {
ElMessage.error('加载权限配置失败')
}
}
// ★ 核心:将后端嵌套的 Tree 数据转换为适合表格展示的结构
// 分离 "页面"、"操作列" 和 "普通列"
const transformData = (nodes: any[]): PermissionNode[] => {
return nodes.map(node => {
// 找出所有子节点中的 element
const allChildren = node.children || []
// 1. 找操作列 (作为 Write 权限)
const opNode = allChildren.find((c: any) => c.type === 'element' && (c.code === 'operation' || c.code.endsWith(':operation') || c.code.endsWith('_op')))
const operationCode = opNode ? opNode.code : null
// 2. 找普通列 (作为字段权限)
const columns = allChildren.filter((c: any) => c.type === 'element' && c.code !== 'operation' && !c.code.endsWith('_op') && !c.code.endsWith(':operation'))
// 3. 找子菜单 (如果有)
const subMenus = allChildren.filter((c: any) => c.type === 'menu')
const childrenTransformed = subMenus.length > 0 ? transformData(subMenus) : undefined
return {
id: node.id,
name: node.name,
code: node.code,
type: node.type,
children: childrenTransformed,
// 状态字段初始化
hasRead: false,
hasWrite: false,
operationCode: operationCode,
elements: columns,
checkedElements: [],
checkAllElements: false,
isIndeterminate: false
}
})
}
// 2. 切换角色:回显权限
const handleRoleSelect = async (roleCode: string) => {
currentRole.value = roleCode
loading.value = true
try {
// 获取该角色拥有的所有 code
const res: any = await getRolePermissions(roleCode)
if (res.code === 200) {
const perms = new Set([...(res.data.menus || []), ...(res.data.elements || [])])
// 递归设置表格每一行的状态
setRowStatus(tableData.value, perms)
}
} catch (e) {
ElMessage.error('获取角色权限失败')
} finally {
loading.value = false
}
}
// 递归回显状态
const setRowStatus = (rows: PermissionNode[], perms: Set<any>) => {
rows.forEach(row => {
// 1. 设置可读 (页面Code是否存在)
row.hasRead = perms.has(row.code)
// 2. 设置可编辑 (操作列Code是否存在)
if (row.operationCode) {
row.hasWrite = perms.has(row.operationCode)
}
// 3. 设置已选字段
if (row.elements && row.elements.length > 0) {
row.checkedElements = row.elements
.filter(el => perms.has(el.code))
.map(el => el.code)
updateCheckAllStatus(row)
}
// 4. 递归子菜单
if (row.children) {
setRowStatus(row.children, perms)
}
})
}
// --- 交互逻辑 ---
// 当“可读”改变
const handleReadChange = (val: boolean, row: PermissionNode) => {
// 如果关闭可读,强制关闭可编辑,并清空字段选择
if (!val) {
row.hasWrite = false
// row.checkedElements = [] // 可选:是否同时也清空字段勾选?通常建议保留,方便误触恢复,但这里为了逻辑严谨先不清空,只在保存时过滤
} else {
// 如果开启可读,默认全选字段 (提升体验)
// row.checkedElements = row.elements.map(e => e.code)
// updateCheckAllStatus(row)
}
// 联动子菜单:如果父级关闭,子级是否关闭?通常不强制,但可以做
}
// 当“可编辑”改变
const handleWriteChange = (val: boolean, row: PermissionNode) => {
// 如果开启编辑,必须开启可读
if (val && !row.hasRead) {
row.hasRead = true
}
}
// 字段全选逻辑
const handleCheckAllElements = (val: boolean, row: PermissionNode) => {
row.checkedElements = val ? row.elements.map(e => e.code) : []
row.isIndeterminate = false
}
// 字段单选逻辑
const handleCheckedElementsChange = (val: string[], row: PermissionNode) => {
updateCheckAllStatus(row)
}
const updateCheckAllStatus = (row: PermissionNode) => {
const checkedCount = row.checkedElements.length
row.checkAllElements = checkedCount === row.elements.length && row.elements.length > 0
row.isIndeterminate = checkedCount > 0 && checkedCount < row.elements.length
}
// --- 保存逻辑 ---
const handleSave = async () => {
if (!currentRole.value) return
const permissions: string[] = []
// 递归收集所有状态为 true 的 code
const collectPermissions = (rows: PermissionNode[]) => {
rows.forEach(row => {
// 1. 页面权限
if (row.hasRead) {
permissions.push(row.code)
}
// 2. 操作权限 (只有当页面可读时才有效)
if (row.hasRead && row.hasWrite && row.operationCode) {
permissions.push(row.operationCode)
}
// 3. 字段权限 (只有当页面可读时才有效)
if (row.hasRead && row.checkedElements.length > 0) {
permissions.push(...row.checkedElements)
}
// 递归
if (row.children) {
collectPermissions(row.children)
}
})
}
collectPermissions(tableData.value)
saving.value = true
try {
const res: any = await saveRolePermissions({
role_code: currentRole.value,
permissions: permissions
})
if (res.code === 200) {
ElMessage.success('权限保存成功!')
} else {
ElMessage.error(res.msg || '保存失败')
}
} catch (e) {
ElMessage.error('保存失败')
} finally {
saving.value = false
}
}
// --- 辅助 ---
const getRoleLabel = (val: string) => {
const found = roleList.find(r => r.value === val)
return found ? found.label : val
}
const getCheckedCount = (row: PermissionNode) => row.checkedElements.length
const getElementName = (row: PermissionNode, code: string) => {
const found = row.elements.find(e => e.code === code)
return found ? found.name : code
}
onMounted(() => {
fetchTree()
})
</script>
<style scoped>
.app-container {
padding: 15px;
height: calc(100vh - 84px);
background-color: #f0f2f5;
}
.permission-card {
height: 100%;
display: flex;
flex-direction: column;
border: none;
border-radius: 8px;
}
:deep(.el-card__header) {
padding: 15px 20px;
border-bottom: 1px solid #f0f0f0;
}
:deep(.el-card__body) {
flex: 1;
padding: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.title-block {
display: flex;
flex-direction: column;
}
.main-title {
font-size: 18px;
font-weight: 600;
color: #303133;
}
.sub-title {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
/* 布局 */
.main-layout {
display: flex;
flex: 1;
height: 100%;
}
/* 左侧样式 */
.left-panel {
width: 260px;
background: #fff;
border-right: 1px solid #f0f0f0;
display: flex;
flex-direction: column;
}
.panel-header {
padding: 15px 20px;
font-size: 14px;
font-weight: 600;
color: #606266;
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
gap: 8px;
}
.role-list {
list-style: none;
padding: 10px;
margin: 0;
}
.role-item {
display: flex;
align-items: center;
padding: 12px 15px;
margin-bottom: 8px;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
color: #606266;
}
.role-item:hover {
background-color: #f5f7fa;
}
.role-item.active {
background-color: #e6f7ff;
color: #1890ff;
font-weight: 500;
}
.role-icon {
margin-right: 12px;
font-size: 18px;
display: flex;
align-items: center;
}
.role-info {
flex: 1;
display: flex;
flex-direction: column;
}
.role-name {
font-size: 14px;
}
.role-code {
font-size: 11px;
color: #999;
margin-top: 2px;
}
.arrow-icon {
font-size: 14px;
}
/* 右侧样式 */
.right-panel {
flex: 1;
background: #fff;
display: flex;
flex-direction: column;
padding: 20px;
overflow: hidden;
}
.empty-state {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #909399;
font-size: 14px;
}
.config-content {
height: 100%;
display: flex;
flex-direction: column;
}
.config-content .panel-header {
background: #fff;
padding: 0 0 15px 0;
justify-content: space-between;
}
.permission-table {
flex: 1;
overflow: auto;
}
/* 表格内样式 */
.custom-checkbox {
height: auto;
}
.text-active {
color: #1890ff;
font-weight: 500;
}
.text-disabled {
color: #c0c4cc;
}
.text-gray {
color: #d9d9d9;
}
/* 字段配置 Popover 样式 */
.field-config-box {
padding: 5px;
}
.field-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 10px;
font-weight: bold;
}
.preview-tags {
display: inline-flex;
align-items: center;
margin-left: 8px;
gap: 4px;
}
.mini-tag {
background: #f5f5f5;
color: #909399;
font-size: 11px;
padding: 1px 4px;
border-radius: 3px;
}
</style>

View File

@ -4,7 +4,7 @@
<template #header>
<div class="card-header">
<span style="font-weight: bold;">员工账号管理</span>
<el-button type="primary" @click="handleCreate">
<el-button v-if="userStore.hasPermission('system_user:operation')" type="primary" @click="handleCreate">
+ 新增员工
</el-button>
</div>
@ -16,23 +16,33 @@
border
style="width: 100%"
>
<el-table-column prop="username" label="用户标识" min-width="180" />
<el-table-column v-if="hasColumnPermission('username')" prop="username" label="用户标识" min-width="180" />
<el-table-column prop="department" label="所属部门" width="150">
<el-table-column v-if="hasColumnPermission('department')" prop="department" label="所属部门" width="150">
<template #default="scope">
<el-tag>{{ scope.row.department }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="role" label="系统角色" width="180">
<el-table-column v-if="hasColumnPermission('role')" prop="role" label="系统角色" width="180">
<template #default="scope">
{{ formatRole(scope.row.role) }}
</template>
</el-table-column>
<el-table-column prop="email" label="邮箱" min-width="200" />
<el-table-column v-if="hasColumnPermission('email')" prop="email" label="邮箱" min-width="200" />
<el-table-column label="操作" width="180" fixed="right">
<el-table-column v-if="hasColumnPermission('status')" prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.status === 'active' ? 'success' : 'danger'">
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column v-if="hasColumnPermission('created_at')" prop="created_at" label="创建时间" width="160" />
<el-table-column v-if="userStore.hasPermission('system_user:operation')" label="操作" width="180" fixed="right">
<template #default="scope">
<el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-popconfirm title="确定要删除该用户吗?" @confirm="handleDelete(scope.row)">
@ -61,7 +71,7 @@
<el-input
v-model="form.cn_name"
placeholder="请输入中文姓名 (如: 张三)"
:disabled="isEdit"
:disabled="isEdit || !userStore.hasPermission('system_user:operation')"
@input="handleNameInput"
/>
</el-form-item>
@ -70,7 +80,7 @@
<el-input
v-model="form.username"
placeholder="自动生成,可修改 (如: zhangsan)"
:disabled="isEdit"
:disabled="isEdit || !userStore.hasPermission('system_user:operation')"
>
<template #append>
<span v-if="!isEdit" style="font-size: 12px; color: #999;">重复自动+1</span>
@ -84,6 +94,7 @@
type="password"
show-password
:placeholder="isEdit ? '不修改请留空' : '设置初始密码'"
:disabled="!userStore.hasPermission('system_user:operation')"
/>
</el-form-item>
@ -95,13 +106,14 @@
filterable
allow-create
default-first-option
:disabled="!userStore.hasPermission('system_user:operation')"
>
<el-option v-for="item in departmentOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
<el-form-item label="系统角色" prop="role">
<el-select v-model="form.role" placeholder="授予权限" style="width: 100%">
<el-select v-model="form.role" placeholder="授予权限" style="width: 100%" :disabled="!userStore.hasPermission('system_user:operation')">
<el-option
v-for="option in roleOptions"
:key="option.value"
@ -112,14 +124,14 @@
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" />
<el-input v-model="form.email" placeholder="请输入邮箱" :disabled="!userStore.hasPermission('system_user:operation')" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="onSubmit" :loading="submitLoading">
<el-button v-if="userStore.hasPermission('system_user:operation')" type="primary" @click="onSubmit" :loading="submitLoading">
{{ isEdit ? '确认修改' : '确认创建' }}
</el-button>
</div>
@ -136,6 +148,25 @@ import { ElMessage } from 'element-plus'
import { pinyin } from 'pinyin-pro' // ★ 务必安装: npm install pinyin-pro
const userStore = useUserStore()
// 列与权限Code的映射关系数据库中的code
const permissionMap: Record<string, string> = {
username: 'system_user:username',
department: 'system_user:department',
role: 'system_user:role',
email: 'system_user:email',
status: 'system_user:status',
created_at: 'system_user:created_at',
}
// 检查列权限
const hasColumnPermission = (prop: string) => {
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return true
}
const code = permissionMap[prop]
return code ? userStore.hasPermission(code) : false
}
const tableLoading = ref(false)
const submitLoading = ref(false)
const dialogVisible = ref(false)
@ -241,6 +272,7 @@ const getList = async () => {
tableData.value = res.data || []
extractDepartments(tableData.value)
} catch (error) {
// 错误已由全局拦截器统一处理
console.error('Fetch users failed:', error)
} finally {
tableLoading.value = false
@ -317,7 +349,7 @@ const onSubmit = async () => {
dialogVisible.value = false
getList()
} catch (error) {
// request 拦截器会处理错误
// 错误已由全局拦截器统一处理
} finally {
submitLoading.value = false
}
@ -342,6 +374,7 @@ const handleDelete = async (row: any) => {
ElMessage.success('删除成功')
getList()
} catch (error) {
// 错误已由全局拦截器统一处理
}
}

View File

@ -13,10 +13,14 @@
</template>
<div class="scan-section">
<div class="camera-placeholder" @click="showCamera = true">
<div v-if="userStore.hasPermission('op_borrow:operation')" class="camera-placeholder" @click="showCamera = true">
<el-icon :size="40" color="#409EFF"><CameraFilled /></el-icon>
<span class="text">点击开启全屏扫码</span>
</div>
<div v-else class="camera-placeholder" style="background-color: #f5f5f5; cursor: not-allowed;">
<el-icon :size="40" color="#909399"><CameraFilled /></el-icon>
<span class="text">无扫码权限</span>
</div>
<div class="input-box">
<el-input
@ -26,12 +30,13 @@
clearable
ref="barcodeRef"
size="large"
:disabled="!userStore.hasPermission('op_borrow:operation')"
>
<template #prefix>
<el-icon><Scissor /></el-icon>
</template>
<template #append>
<el-button @click="handleManualInput">添加</el-button>
<el-button @click="handleManualInput" :disabled="!userStore.hasPermission('op_borrow:operation')">添加</el-button>
</template>
</el-input>
</div>
@ -40,16 +45,16 @@
<div class="cart-section">
<div v-if="cartItems.length > 0">
<el-table :data="cartItems" border stripe style="width: 100%">
<el-table-column prop="name" label="物品名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="sku" label="SKU" width="120" show-overflow-tooltip />
<el-table-column v-if="hasColumnPermission('name')" prop="name" label="物品名称" min-width="120" show-overflow-tooltip />
<el-table-column v-if="hasColumnPermission('sku')" prop="sku" label="SKU" width="120" show-overflow-tooltip />
<el-table-column label="可用库存" width="90" align="center">
<el-table-column v-if="hasColumnPermission('available_quantity')" label="可用库存" width="90" align="center">
<template #default="{row}">
<el-tag type="info">{{ parseFloat(row.available_quantity) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="借用数" width="130" align="center">
<el-table-column v-if="hasColumnPermission('out_quantity')" label="借用数" width="130" align="center">
<template #default="{row}">
<el-input-number
v-model="row.out_quantity"
@ -57,11 +62,12 @@
:max="parseFloat(row.available_quantity)"
size="small"
style="width: 100px"
:disabled="!userStore.hasPermission('op_borrow:operation')"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="60" align="center" fixed="right">
<el-table-column v-if="userStore.hasPermission('op_borrow:operation')" label="操作" width="60" align="center" fixed="right">
<template #default="{$index}">
<el-button type="danger" icon="Delete" circle size="small" @click="removeFromCart($index)" />
</template>
@ -102,7 +108,7 @@
</el-form-item>
<el-form-item label="领用人签名确认" required>
<div class="signature-box" @click="openSignatureDialog">
<div class="signature-box" @click="openSignatureDialog" v-if="userStore.hasPermission('op_borrow:operation')">
<div v-if="signaturePreviewUrl" class="signed-img">
<img :src="signaturePreviewUrl" alt="签名" />
<span class="re-sign-tip">点击重签</span>
@ -112,11 +118,17 @@
<span>点击此处进行全屏签名</span>
</div>
</div>
<div v-else class="signature-box" style="background-color: #f5f5f5; cursor: not-allowed;">
<div class="unsigned-placeholder">
<el-icon :size="24"><EditPen /></el-icon>
<span>无签名权限</span>
</div>
</div>
</el-form-item>
<div class="bottom-actions">
<el-button @click="clearAll" icon="Refresh">清空</el-button>
<el-button type="primary" size="large" :loading="loading" @click="submitForm" icon="Select">
<el-button v-if="userStore.hasPermission('op_borrow:operation')" @click="clearAll" icon="Refresh">清空</el-button>
<el-button v-if="userStore.hasPermission('op_borrow:operation')" type="primary" size="large" :loading="loading" @click="submitForm" icon="Select">
确认借出
</el-button>
</div>
@ -187,6 +199,27 @@ import QrScanner from '@/components/QrScanner/index.vue'
import { getStockByBarcode } from '@/api/outbound'
import request from '@/utils/request'
import { uploadFile } from '@/api/common/upload'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 列与权限Code的映射关系数据库中的code
const permissionMap: Record<string, string> = {
borrower_name: 'op_borrow:borrower_name',
sku: 'op_borrow:sku',
available_quantity: 'op_borrow:available_quantity',
out_quantity: 'op_borrow:out_quantity',
// 其他字段可根据需要添加
}
// 检查列权限
const hasColumnPermission = (prop: string) => {
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return true
}
const code = permissionMap[prop]
return code ? userStore.hasPermission(code) : false
}
// --- 状态定义 ---
const barcodeInput = ref('')

View File

@ -19,12 +19,12 @@
v-loading="loading"
:row-class-name="tableRowClassName"
>
<el-table-column prop="borrow_no" label="单号" width="180" show-overflow-tooltip />
<el-table-column prop="borrower_name" label="借用人" width="100" />
<el-table-column prop="sku" label="SKU" width="120" show-overflow-tooltip />
<el-table-column prop="borrow_time" label="借出时间" width="160" sortable />
<el-table-column v-if="hasColumnPermission('borrow_no')" prop="borrow_no" label="单号" width="180" show-overflow-tooltip />
<el-table-column v-if="hasColumnPermission('borrower_name')" prop="borrower_name" label="借用人" width="100" />
<el-table-column v-if="hasColumnPermission('sku')" prop="sku" label="SKU" width="120" show-overflow-tooltip />
<el-table-column v-if="hasColumnPermission('borrow_time')" prop="borrow_time" label="借出时间" width="160" sortable />
<el-table-column label="归还时间 / 预计" min-width="200">
<el-table-column v-if="hasColumnPermission('expected_return_time') || hasColumnPermission('return_time')" label="归还时间 / 预计" min-width="200">
<template #default="{row}">
<div v-if="row.status === 'returned'">
<el-tag type="success" size="small">实际</el-tag>
@ -40,7 +40,7 @@
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<el-table-column v-if="hasColumnPermission('status')" label="状态" width="100" align="center">
<template #default="{row}">
<el-tag :type="row.status==='returned'?'success':'warning'">
{{ row.status==='returned'?'已还':'借出中' }}
@ -48,22 +48,22 @@
</template>
</el-table-column>
<el-table-column label="归还库位" min-width="120">
<el-table-column v-if="hasColumnPermission('return_location')" label="归还库位" min-width="120">
<template #default="{row}">
<span v-if="row.return_location">{{ row.return_location }}</span>
<span v-else style="color:#ccc">-</span>
</template>
</el-table-column>
<el-table-column label="电子签名" width="140" align="center">
<el-table-column v-if="hasColumnPermission('borrow_signature') || hasColumnPermission('return_signature')" label="电子签名" width="140" align="center">
<template #default="{row}">
<div style="display:flex; justify-content: center; gap:10px">
<el-popover trigger="hover" placement="top" v-if="row.borrow_signature" width="220">
<el-popover trigger="hover" placement="top" v-if="row.borrow_signature && hasColumnPermission('borrow_signature')" width="220">
<template #reference><el-tag size="small"></el-tag></template>
<img :src="row.borrow_signature" style="width:200px; border:1px solid #eee" />
</el-popover>
<el-popover trigger="hover" placement="top" v-if="row.return_signature" width="220">
<el-popover trigger="hover" placement="top" v-if="row.return_signature && hasColumnPermission('return_signature')" width="220">
<template #reference><el-tag type="success" size="small"></el-tag></template>
<img :src="row.return_signature" style="width:200px; border:1px solid #eee" />
</el-popover>
@ -88,6 +88,32 @@ import request from '@/utils/request'
import dayjs from 'dayjs' // 建议使用 dayjs 处理日期,如果没有安装,可以用原生 Date
import 'dayjs/locale/zh-cn' // 导入中文包
dayjs.locale('zh-cn')
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 列与权限Code的映射关系数据库中的code
const permissionMap: Record<string, string> = {
borrow_no: 'op_records:borrow_no',
borrower_name: 'op_records:borrower_name',
sku: 'op_records:sku',
borrow_time: 'op_records:borrow_time',
return_time: 'op_records:return_time',
status: 'op_records:status',
expected_return_time: 'op_records:expected_return_time',
return_location: 'op_records:return_location',
borrow_signature: 'op_records:borrow_signature',
return_signature: 'op_records:return_signature',
}
// 检查列权限
const hasColumnPermission = (prop: string) => {
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return true
}
const code = permissionMap[prop]
return code ? userStore.hasPermission(code) : false
}
const list = ref<any[]>([])
const total = ref(0)

View File

@ -13,10 +13,14 @@
</template>
<div class="scan-section">
<div class="camera-placeholder" @click="showCamera = true">
<div v-if="userStore.hasPermission('op_return:operation')" class="camera-placeholder" @click="showCamera = true">
<el-icon :size="40" color="#409EFF"><CameraFilled /></el-icon>
<span class="text">点击开启全屏扫码</span>
</div>
<div v-else class="camera-placeholder" style="background-color: #f5f5f5; cursor: not-allowed;">
<el-icon :size="40" color="#909399"><CameraFilled /></el-icon>
<span class="text">无扫码权限</span>
</div>
<div class="input-box">
<el-input
@ -26,12 +30,13 @@
clearable
ref="barcodeRef"
size="large"
:disabled="!userStore.hasPermission('op_return:operation')"
>
<template #prefix>
<el-icon><Scissor /></el-icon>
</template>
<template #append>
<el-button @click="scanItem">识别</el-button>
<el-button @click="scanItem" :disabled="!userStore.hasPermission('op_return:operation')">识别</el-button>
</template>
</el-input>
</div>
@ -40,16 +45,17 @@
<div class="cart-section">
<div v-if="returnList.length > 0">
<el-table :data="returnList" border stripe style="width: 100%">
<el-table-column prop="borrower_name" label="借用人" width="90" show-overflow-tooltip />
<el-table-column prop="sku" label="SKU" width="120" show-overflow-tooltip />
<el-table-column v-if="hasColumnPermission('borrower_name')" prop="borrower_name" label="借用人" width="90" show-overflow-tooltip />
<el-table-column v-if="hasColumnPermission('sku')" prop="sku" label="SKU" width="120" show-overflow-tooltip />
<el-table-column label="归还库位(可改)" min-width="160">
<el-table-column v-if="hasColumnPermission('return_location')" label="归还库位(可改)" min-width="160">
<template #default="{row}">
<el-input
v-model="row.return_location"
:placeholder="`原: ${row.current_location || '无'}`"
clearable
size="small"
:disabled="!userStore.hasPermission('op_return:operation')"
>
<template #append v-if="row.return_location !== row.current_location">
<span style="color: #E6A23C; font-size: 12px;">变更</span>
@ -58,7 +64,7 @@
</template>
</el-table-column>
<el-table-column label="操作" width="60" align="center" fixed="right">
<el-table-column v-if="userStore.hasPermission('op_return:operation')" label="操作" width="60" align="center" fixed="right">
<template #default="{$index}">
<el-button type="danger" icon="Delete" circle size="small" @click="returnList.splice($index, 1)" />
</template>
@ -77,7 +83,7 @@
<el-form label-position="top">
<el-form-item required>
<div class="signature-box" @click="openSignatureDialog">
<div class="signature-box" @click="openSignatureDialog" v-if="userStore.hasPermission('op_return:operation')">
<div v-if="signaturePreviewUrl" class="signed-img">
<img :src="signaturePreviewUrl" alt="签名" />
<span class="re-sign-tip">点击重签</span>
@ -87,12 +93,18 @@
<span>点击此处进行库管签名</span>
</div>
</div>
<div v-else class="signature-box" style="background-color: #f5f5f5; cursor: not-allowed;">
<div class="unsigned-placeholder">
<el-icon :size="24"><EditPen /></el-icon>
<span>无签名权限</span>
</div>
</div>
</el-form-item>
</el-form>
<div class="bottom-actions">
<el-button @click="clearAll" icon="Refresh">清空</el-button>
<el-button type="success" size="large" :loading="loading" @click="preSubmitCheck" icon="Select">
<el-button v-if="userStore.hasPermission('op_return:operation')" @click="clearAll" icon="Refresh">清空</el-button>
<el-button v-if="userStore.hasPermission('op_return:operation')" type="success" size="large" :loading="loading" @click="preSubmitCheck" icon="Select">
确认归还
</el-button>
</div>
@ -161,6 +173,25 @@ import { uploadFile } from '@/api/common/upload'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Scissor, EditPen, Delete, CameraFilled, Close, Refresh, Select } from '@element-plus/icons-vue'
import QrScanner from '@/components/QrScanner/index.vue'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 列与权限Code的映射关系数据库中的code
const permissionMap: Record<string, string> = {
borrower_name: 'op_return:borrower_name',
sku: 'op_return:sku',
return_location: 'op_return:return_location',
}
// 检查列权限
const hasColumnPermission = (prop: string) => {
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return true
}
const code = permissionMap[prop]
return code ? userStore.hasPermission(code) : false
}
// --- 状态 ---
const barcode = ref('')