Compare commits
32 Commits
1.0入库操作
...
42b0cddd3e
| Author | SHA1 | Date | |
|---|---|---|---|
| 42b0cddd3e | |||
| a2b1a62132 | |||
| 5065410662 | |||
| 3714dd180b | |||
| af41eb1803 | |||
| f79fb53b17 | |||
| 38f0bbe41d | |||
| 1ad477eda8 | |||
| 1d2e8feced | |||
| 246fb45cde | |||
| 6e914f1e96 | |||
| b5b1efdc4e | |||
| 56bb6a1c84 | |||
| 379bc5786f | |||
| a96597da33 | |||
| 4c1c61065e | |||
| 25487dbede | |||
| a547d6b164 | |||
| 661ce4e5a0 | |||
| d6d9621bf3 | |||
| f178b9cd00 | |||
| 11fafde5e3 | |||
| 1f9a363545 | |||
| b3e1ac6245 | |||
| 73ee163352 | |||
| c86e67b793 | |||
| 57c2c532ca | |||
| dad7ffdc66 | |||
| b798c42abf | |||
| 8698b2582c | |||
| 220f50dba6 | |||
| 7431f1f41e |
@ -80,7 +80,6 @@ def create_app():
|
|||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 2.4 注册业务操作模块 (Transactions - 借还/维修/报废)
|
# 2.4 注册业务操作模块 (Transactions - 借还/维修/报废)
|
||||||
# ★★★ 关键修改:将前缀改为 /api/v1/transactions 以匹配前端请求 ★★★
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
try:
|
try:
|
||||||
from app.api.v1.transactions import trans_bp
|
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')
|
app.register_blueprint(trans_bp, url_prefix='/api/transactions', name='trans_legacy')
|
||||||
print("✅ Transactions 模块注册成功")
|
print("✅ Transactions 模块注册成功")
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
# 允许模块不存在时不崩溃,但在开发借还功能时这里报错说明 trans_bp 定义有问题
|
print(f"⚠️ 提示: Transaction 模块导入失败: {e}")
|
||||||
print(f"⚠️ 提示: Transaction 模块导入失败 (请检查 app/api/v1/transactions.py): {e}")
|
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 2.5 注册出库模块 (Outbound)
|
# 2.5 注册出库模块 (Outbound)
|
||||||
@ -119,6 +117,19 @@ def create_app():
|
|||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
print(f"❌ 错误: BOM 模块导入失败: {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. 预加载数据模型
|
# 3. 预加载数据模型
|
||||||
# =========================================================
|
# =========================================================
|
||||||
@ -133,8 +144,8 @@ def create_app():
|
|||||||
# 出库模型
|
# 出库模型
|
||||||
from app.models.outbound import TransOutbound
|
from app.models.outbound import TransOutbound
|
||||||
|
|
||||||
# 系统与业务模型
|
# 系统与业务模型 (SysRolePermission 等在 models.system 中)
|
||||||
from app.models.system import SysUser, SysLog
|
from app.models.system import SysUser, SysLog, SysMenu, SysElement, SysRolePermission
|
||||||
# 确保借还模型被加载
|
# 确保借还模型被加载
|
||||||
from app.models.transaction import TransBorrow, TransRepair, TransScrap
|
from app.models.transaction import TransBorrow, TransRepair, TransScrap
|
||||||
|
|
||||||
|
|||||||
@ -88,3 +88,20 @@ def delete_user(user_id):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"Delete User Failed: {str(e)}")
|
current_app.logger.error(f"Delete User Failed: {str(e)}")
|
||||||
return jsonify({'msg': str(e)}), 400
|
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
|
||||||
@ -1,22 +1,77 @@
|
|||||||
# 文件路径: app/api/v1/inbound/base.py
|
# 文件路径: 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.services.inbound.base_service import MaterialBaseService
|
||||||
|
from app.utils.decorators import login_required, permission_required
|
||||||
import traceback
|
import traceback
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
inbound_base_bp = Blueprint('stock_base', __name__)
|
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)
|
# 1. 搜索接口 (GET /api/v1/inbound/base/search)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@inbound_base_bp.route('/search', methods=['GET'])
|
@inbound_base_bp.route('/search', methods=['GET'])
|
||||||
|
@permission_required('material_list')
|
||||||
def search_base():
|
def search_base():
|
||||||
try:
|
try:
|
||||||
keyword = request.args.get('keyword', '')
|
keyword = request.args.get('keyword', '')
|
||||||
data = MaterialBaseService.search_material(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:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||||
@ -26,6 +81,7 @@ def search_base():
|
|||||||
# 2. 列表接口 (GET /api/v1/inbound/base/list)
|
# 2. 列表接口 (GET /api/v1/inbound/base/list)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@inbound_base_bp.route('/list', methods=['GET'])
|
@inbound_base_bp.route('/list', methods=['GET'])
|
||||||
|
@permission_required('material_list')
|
||||||
def get_list():
|
def get_list():
|
||||||
try:
|
try:
|
||||||
page = request.args.get('pageNum', 1, type=int)
|
page = request.args.get('pageNum', 1, type=int)
|
||||||
@ -41,6 +97,10 @@ def get_list():
|
|||||||
}
|
}
|
||||||
|
|
||||||
result = MaterialBaseService.get_list(page, limit, filters)
|
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})
|
return jsonify({"code": 200, "msg": "success", "data": result})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@ -51,6 +111,7 @@ def get_list():
|
|||||||
# 2.1 选项接口 (GET /api/v1/inbound/base/options)
|
# 2.1 选项接口 (GET /api/v1/inbound/base/options)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@inbound_base_bp.route('/options', methods=['GET'])
|
@inbound_base_bp.route('/options', methods=['GET'])
|
||||||
|
@permission_required('material_list')
|
||||||
def get_options():
|
def get_options():
|
||||||
try:
|
try:
|
||||||
data = MaterialBaseService.get_distinct_options()
|
data = MaterialBaseService.get_distinct_options()
|
||||||
@ -64,6 +125,7 @@ def get_options():
|
|||||||
# 2.2 导出接口 (GET /api/v1/inbound/base/export)
|
# 2.2 导出接口 (GET /api/v1/inbound/base/export)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@inbound_base_bp.route('/export', methods=['GET'])
|
@inbound_base_bp.route('/export', methods=['GET'])
|
||||||
|
@permission_required('material_list')
|
||||||
def export_data():
|
def export_data():
|
||||||
try:
|
try:
|
||||||
# 获取筛选条件
|
# 获取筛选条件
|
||||||
@ -101,6 +163,7 @@ def export_data():
|
|||||||
# 3. 新增接口 (POST /api/v1/inbound/base/)
|
# 3. 新增接口 (POST /api/v1/inbound/base/)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@inbound_base_bp.route('/', methods=['POST'])
|
@inbound_base_bp.route('/', methods=['POST'])
|
||||||
|
@permission_required('material_list:operation')
|
||||||
def create():
|
def create():
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
@ -122,6 +185,7 @@ def create():
|
|||||||
# 4. 修改接口 (PUT /api/v1/inbound/base/<id>)
|
# 4. 修改接口 (PUT /api/v1/inbound/base/<id>)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@inbound_base_bp.route('/<int:id>', methods=['PUT'])
|
@inbound_base_bp.route('/<int:id>', methods=['PUT'])
|
||||||
|
@permission_required('material_list:operation')
|
||||||
def update(id):
|
def update(id):
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
@ -136,6 +200,7 @@ def update(id):
|
|||||||
# 5. 删除接口 (DELETE /api/v1/inbound/base/<id>)
|
# 5. 删除接口 (DELETE /api/v1/inbound/base/<id>)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@inbound_base_bp.route('/<int:id>', methods=['DELETE'])
|
@inbound_base_bp.route('/<int:id>', methods=['DELETE'])
|
||||||
|
@permission_required('material_list:operation')
|
||||||
def delete(id):
|
def delete(id):
|
||||||
try:
|
try:
|
||||||
MaterialBaseService.delete_material(id)
|
MaterialBaseService.delete_material(id)
|
||||||
|
|||||||
@ -1,14 +1,89 @@
|
|||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from app.services.inbound.buy_service import BuyInboundService
|
from app.services.inbound.buy_service import BuyInboundService
|
||||||
|
from app.utils.decorators import permission_required
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
inbound_buy_bp = Blueprint('stock_buy', __name__)
|
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. 基础物料搜索
|
# 0. 基础物料搜索
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_buy_bp.route('/search-base', methods=['GET'])
|
@inbound_buy_bp.route('/search-base', methods=['GET'])
|
||||||
|
@permission_required('inbound_buy')
|
||||||
def search_base():
|
def search_base():
|
||||||
try:
|
try:
|
||||||
keyword = request.args.get('keyword', '')
|
keyword = request.args.get('keyword', '')
|
||||||
@ -33,6 +108,7 @@ def search_base():
|
|||||||
# 1. 获取列表 (修改:接收 category 和 material_type)
|
# 1. 获取列表 (修改:接收 category 和 material_type)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_buy_bp.route('/list', methods=['GET'])
|
@inbound_buy_bp.route('/list', methods=['GET'])
|
||||||
|
@permission_required('inbound_buy')
|
||||||
def get_list():
|
def get_list():
|
||||||
try:
|
try:
|
||||||
page = request.args.get('page', 1, type=int)
|
page = request.args.get('page', 1, type=int)
|
||||||
@ -48,6 +124,10 @@ def get_list():
|
|||||||
statuses = statuses_str.split(',') if statuses_str else []
|
statuses = statuses_str.split(',') if statuses_str else []
|
||||||
|
|
||||||
result = BuyInboundService.get_list(page, limit, keyword, statuses, category, material_type)
|
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})
|
return jsonify({"code": 200, "msg": "success", "data": result})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@ -58,6 +138,7 @@ def get_list():
|
|||||||
# 2. 新增入库
|
# 2. 新增入库
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_buy_bp.route('/submit', methods=['POST'])
|
@inbound_buy_bp.route('/submit', methods=['POST'])
|
||||||
|
@permission_required('inbound_buy:operation')
|
||||||
def submit():
|
def submit():
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
@ -80,6 +161,7 @@ def submit():
|
|||||||
# 3. 更新入库
|
# 3. 更新入库
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_buy_bp.route('/<int:id>', methods=['PUT'])
|
@inbound_buy_bp.route('/<int:id>', methods=['PUT'])
|
||||||
|
@permission_required('inbound_buy:operation')
|
||||||
def update_buy(id):
|
def update_buy(id):
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
@ -93,6 +175,7 @@ def update_buy(id):
|
|||||||
# 4. 删除
|
# 4. 删除
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_buy_bp.route('/<int:id>', methods=['DELETE'])
|
@inbound_buy_bp.route('/<int:id>', methods=['DELETE'])
|
||||||
|
@permission_required('inbound_buy:operation')
|
||||||
def delete_buy(id):
|
def delete_buy(id):
|
||||||
try:
|
try:
|
||||||
BuyInboundService.delete_inbound(id)
|
BuyInboundService.delete_inbound(id)
|
||||||
@ -105,6 +188,7 @@ def delete_buy(id):
|
|||||||
# 5. [新增] 获取筛选下拉选项 (修复404的关键)
|
# 5. [新增] 获取筛选下拉选项 (修复404的关键)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_buy_bp.route('/options', methods=['GET'])
|
@inbound_buy_bp.route('/options', methods=['GET'])
|
||||||
|
@permission_required('inbound_buy')
|
||||||
def get_options():
|
def get_options():
|
||||||
try:
|
try:
|
||||||
data = BuyInboundService.get_filter_options()
|
data = BuyInboundService.get_filter_options()
|
||||||
@ -117,6 +201,7 @@ def get_options():
|
|||||||
# 6. 获取关联的出库历史 (如果有)
|
# 6. 获取关联的出库历史 (如果有)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_buy_bp.route('/<int:id>/history', methods=['GET'])
|
@inbound_buy_bp.route('/<int:id>/history', methods=['GET'])
|
||||||
|
@permission_required('inbound_buy')
|
||||||
def get_history(id):
|
def get_history(id):
|
||||||
# 如果没有出库模块,这个接口可能为空,但为保持兼容性保留
|
# 如果没有出库模块,这个接口可能为空,但为保持兼容性保留
|
||||||
return jsonify({"code": 200, "msg": "success", "data": []})
|
return jsonify({"code": 200, "msg": "success", "data": []})
|
||||||
@ -126,6 +211,7 @@ def get_history(id):
|
|||||||
# 7. 供应商建议
|
# 7. 供应商建议
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_buy_bp.route('/suggestions/suppliers', methods=['GET'])
|
@inbound_buy_bp.route('/suggestions/suppliers', methods=['GET'])
|
||||||
|
@permission_required('inbound_buy')
|
||||||
def get_supplier_suggestions():
|
def get_supplier_suggestions():
|
||||||
base_id = request.args.get('base_id', type=int)
|
base_id = request.args.get('base_id', type=int)
|
||||||
if not base_id:
|
if not base_id:
|
||||||
@ -138,6 +224,7 @@ def get_supplier_suggestions():
|
|||||||
# 8. 采购人建议 (全局)
|
# 8. 采购人建议 (全局)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_buy_bp.route('/suggestions/users', methods=['GET'])
|
@inbound_buy_bp.route('/suggestions/users', methods=['GET'])
|
||||||
|
@permission_required('inbound_buy')
|
||||||
def get_user_suggestions():
|
def get_user_suggestions():
|
||||||
keyword = request.args.get('keyword', '')
|
keyword = request.args.get('keyword', '')
|
||||||
data = BuyInboundService.get_history_purchasers(keyword)
|
data = BuyInboundService.get_history_purchasers(keyword)
|
||||||
@ -148,6 +235,7 @@ def get_user_suggestions():
|
|||||||
# 9. 链接建议
|
# 9. 链接建议
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_buy_bp.route('/suggestions/links', methods=['GET'])
|
@inbound_buy_bp.route('/suggestions/links', methods=['GET'])
|
||||||
|
@permission_required('inbound_buy')
|
||||||
def get_link_suggestions():
|
def get_link_suggestions():
|
||||||
base_id = request.args.get('base_id', type=int)
|
base_id = request.args.get('base_id', type=int)
|
||||||
link_type = request.args.get('type', 'original') # original or detail
|
link_type = request.args.get('type', 'original') # original or detail
|
||||||
@ -161,6 +249,7 @@ def get_link_suggestions():
|
|||||||
# 10. 库位建议
|
# 10. 库位建议
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_buy_bp.route('/suggestions/locations', methods=['GET'])
|
@inbound_buy_bp.route('/suggestions/locations', methods=['GET'])
|
||||||
|
@permission_required('inbound_buy')
|
||||||
def get_location_suggestions():
|
def get_location_suggestions():
|
||||||
base_id = request.args.get('base_id', type=int)
|
base_id = request.args.get('base_id', type=int)
|
||||||
if not base_id:
|
if not base_id:
|
||||||
|
|||||||
@ -1,15 +1,90 @@
|
|||||||
# inventory-backend/app/api/v1/inbound/product.py
|
# inventory-backend/app/api/v1/inbound/product.py
|
||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from app.services.inbound.product_service import ProductInboundService
|
from app.services.inbound.product_service import ProductInboundService
|
||||||
|
from app.utils.decorators import permission_required
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
inbound_product_bp = Blueprint('stock_product', __name__)
|
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 实现自动回填)
|
# 0. 基础物料搜索 (关键接口:配合 Service 实现自动回填)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_product_bp.route('/search-base', methods=['GET'])
|
@inbound_product_bp.route('/search-base', methods=['GET'])
|
||||||
|
@permission_required('inbound_product')
|
||||||
def search_base():
|
def search_base():
|
||||||
"""
|
"""
|
||||||
对应前端 API: /inbound/product/search-base
|
对应前端 API: /inbound/product/search-base
|
||||||
@ -19,7 +94,10 @@ def search_base():
|
|||||||
keyword = request.args.get('keyword', '')
|
keyword = request.args.get('keyword', '')
|
||||||
# 调用 Service 层已修复的 search_base_material 方法
|
# 调用 Service 层已修复的 search_base_material 方法
|
||||||
data = ProductInboundService.search_base_material(keyword)
|
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:
|
except Exception as e:
|
||||||
# 捕获异常并打印堆栈,方便调试
|
# 捕获异常并打印堆栈,方便调试
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@ -29,6 +107,7 @@ def search_base():
|
|||||||
# 0.5 [新增] BOM 搜索接口
|
# 0.5 [新增] BOM 搜索接口
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_product_bp.route('/search-bom', methods=['GET'])
|
@inbound_product_bp.route('/search-bom', methods=['GET'])
|
||||||
|
@permission_required('inbound_product')
|
||||||
def search_bom():
|
def search_bom():
|
||||||
"""
|
"""
|
||||||
供前端下拉框远程搜索使用 (搜索BOM)
|
供前端下拉框远程搜索使用 (搜索BOM)
|
||||||
@ -50,6 +129,7 @@ def search_bom():
|
|||||||
# 1. 获取列表 (支持 status 多选筛选)
|
# 1. 获取列表 (支持 status 多选筛选)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_product_bp.route('/list', methods=['GET'])
|
@inbound_product_bp.route('/list', methods=['GET'])
|
||||||
|
@permission_required('inbound_product')
|
||||||
def get_list():
|
def get_list():
|
||||||
try:
|
try:
|
||||||
page = request.args.get('page', 1, type=int)
|
page = request.args.get('page', 1, type=int)
|
||||||
@ -61,6 +141,10 @@ def get_list():
|
|||||||
statuses = statuses_str.split(',') if statuses_str else []
|
statuses = statuses_str.split(',') if statuses_str else []
|
||||||
|
|
||||||
result = ProductInboundService.get_list(page, limit, keyword, statuses)
|
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})
|
return jsonify({"code": 200, "msg": "success", "data": result})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@ -71,6 +155,7 @@ def get_list():
|
|||||||
# 2. 新增入库
|
# 2. 新增入库
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_product_bp.route('/submit', methods=['POST'])
|
@inbound_product_bp.route('/submit', methods=['POST'])
|
||||||
|
@permission_required('inbound_product:operation')
|
||||||
def submit():
|
def submit():
|
||||||
try:
|
try:
|
||||||
# 调用 Service 处理入库,获取新创建的对象
|
# 调用 Service 处理入库,获取新创建的对象
|
||||||
@ -91,6 +176,7 @@ def submit():
|
|||||||
# 3. 更新入库
|
# 3. 更新入库
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_product_bp.route('/<int:id>', methods=['PUT'])
|
@inbound_product_bp.route('/<int:id>', methods=['PUT'])
|
||||||
|
@permission_required('inbound_product:operation')
|
||||||
def update(id):
|
def update(id):
|
||||||
try:
|
try:
|
||||||
ProductInboundService.update_inbound(id, request.get_json())
|
ProductInboundService.update_inbound(id, request.get_json())
|
||||||
@ -104,6 +190,7 @@ def update(id):
|
|||||||
# 4. 删除
|
# 4. 删除
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_product_bp.route('/<int:id>', methods=['DELETE'])
|
@inbound_product_bp.route('/<int:id>', methods=['DELETE'])
|
||||||
|
@permission_required('inbound_product:operation')
|
||||||
def delete(id):
|
def delete(id):
|
||||||
try:
|
try:
|
||||||
ProductInboundService.delete_inbound(id)
|
ProductInboundService.delete_inbound(id)
|
||||||
@ -117,6 +204,7 @@ def delete(id):
|
|||||||
# 5. 获取出库历史
|
# 5. 获取出库历史
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_product_bp.route('/<int:id>/history', methods=['GET'])
|
@inbound_product_bp.route('/<int:id>/history', methods=['GET'])
|
||||||
|
@permission_required('inbound_product')
|
||||||
def get_history(id):
|
def get_history(id):
|
||||||
try:
|
try:
|
||||||
data = ProductInboundService.get_outbound_history(id)
|
data = ProductInboundService.get_outbound_history(id)
|
||||||
@ -130,6 +218,7 @@ def get_history(id):
|
|||||||
# 6. 系统用户建议
|
# 6. 系统用户建议
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_product_bp.route('/suggestions/users', methods=['GET'])
|
@inbound_product_bp.route('/suggestions/users', methods=['GET'])
|
||||||
|
@permission_required('inbound_product')
|
||||||
def get_user_suggestions():
|
def get_user_suggestions():
|
||||||
keyword = request.args.get('keyword', '')
|
keyword = request.args.get('keyword', '')
|
||||||
data = ProductInboundService.search_system_users(keyword)
|
data = ProductInboundService.search_system_users(keyword)
|
||||||
@ -140,6 +229,7 @@ def get_user_suggestions():
|
|||||||
# 7. 获取筛选选项
|
# 7. 获取筛选选项
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_product_bp.route('/options', methods=['GET'])
|
@inbound_product_bp.route('/options', methods=['GET'])
|
||||||
|
@permission_required('inbound_product')
|
||||||
def get_options():
|
def get_options():
|
||||||
try:
|
try:
|
||||||
data = ProductInboundService.get_filter_options()
|
data = ProductInboundService.get_filter_options()
|
||||||
|
|||||||
@ -1,16 +1,90 @@
|
|||||||
# inventory-backend/app/api/v1/inbound/semi.py
|
# inventory-backend/app/api/v1/inbound/semi.py
|
||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from app.services.inbound.semi_service import SemiInboundService
|
from app.services.inbound.semi_service import SemiInboundService
|
||||||
|
from app.utils.decorators import permission_required
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
# 定义蓝图
|
# 定义蓝图
|
||||||
inbound_semi_bp = Blueprint('stock_semi', __name__)
|
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. 基础物料搜索 (复用逻辑)
|
# 0. 基础物料搜索 (复用逻辑)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_semi_bp.route('/search-base', methods=['GET'])
|
@inbound_semi_bp.route('/search-base', methods=['GET'])
|
||||||
|
@permission_required('inbound_semi')
|
||||||
def search_base():
|
def search_base():
|
||||||
"""
|
"""
|
||||||
供前端下拉框远程搜索使用 (搜索半成品类型的基础物料)
|
供前端下拉框远程搜索使用 (搜索半成品类型的基础物料)
|
||||||
@ -20,10 +94,13 @@ def search_base():
|
|||||||
keyword = request.args.get('keyword', '')
|
keyword = request.args.get('keyword', '')
|
||||||
# 这里复用 Service 中的搜索逻辑
|
# 这里复用 Service 中的搜索逻辑
|
||||||
data = SemiInboundService.search_base_material(keyword)
|
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({
|
return jsonify({
|
||||||
"code": 200,
|
"code": 200,
|
||||||
"msg": "success",
|
"msg": "success",
|
||||||
"data": data
|
"data": filtered_data
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@ -33,6 +110,7 @@ def search_base():
|
|||||||
# 0.5 [新增] BOM 搜索接口
|
# 0.5 [新增] BOM 搜索接口
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_semi_bp.route('/search-bom', methods=['GET'])
|
@inbound_semi_bp.route('/search-bom', methods=['GET'])
|
||||||
|
@permission_required('inbound_semi')
|
||||||
def search_bom():
|
def search_bom():
|
||||||
"""
|
"""
|
||||||
供前端下拉框远程搜索使用 (搜索BOM)
|
供前端下拉框远程搜索使用 (搜索BOM)
|
||||||
@ -55,6 +133,7 @@ def search_bom():
|
|||||||
# 1. 获取半成品列表
|
# 1. 获取半成品列表
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_semi_bp.route('/list', methods=['GET'])
|
@inbound_semi_bp.route('/list', methods=['GET'])
|
||||||
|
@permission_required('inbound_semi')
|
||||||
def get_list():
|
def get_list():
|
||||||
try:
|
try:
|
||||||
page = request.args.get('page', 1, type=int)
|
page = request.args.get('page', 1, type=int)
|
||||||
@ -67,6 +146,10 @@ def get_list():
|
|||||||
statuses = statuses_str.split(',') if statuses_str else []
|
statuses = statuses_str.split(',') if statuses_str else []
|
||||||
|
|
||||||
result = SemiInboundService.get_list(page, limit, keyword, statuses)
|
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})
|
return jsonify({"code": 200, "msg": "success", "data": result})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@ -77,6 +160,7 @@ def get_list():
|
|||||||
# 2. 新增半成品入库 (修改:返回创建的对象数据)
|
# 2. 新增半成品入库 (修改:返回创建的对象数据)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_semi_bp.route('/submit', methods=['POST'])
|
@inbound_semi_bp.route('/submit', methods=['POST'])
|
||||||
|
@permission_required('inbound_semi:operation')
|
||||||
def submit():
|
def submit():
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
@ -101,6 +185,7 @@ def submit():
|
|||||||
# 3. 更新半成品入库信息
|
# 3. 更新半成品入库信息
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_semi_bp.route('/<int:id>', methods=['PUT'])
|
@inbound_semi_bp.route('/<int:id>', methods=['PUT'])
|
||||||
|
@permission_required('inbound_semi:operation')
|
||||||
def update_semi(id):
|
def update_semi(id):
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
@ -115,6 +200,7 @@ def update_semi(id):
|
|||||||
# 4. 删除半成品入库记录
|
# 4. 删除半成品入库记录
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_semi_bp.route('/<int:id>', methods=['DELETE'])
|
@inbound_semi_bp.route('/<int:id>', methods=['DELETE'])
|
||||||
|
@permission_required('inbound_semi:operation')
|
||||||
def delete_semi(id):
|
def delete_semi(id):
|
||||||
try:
|
try:
|
||||||
SemiInboundService.delete_inbound(id)
|
SemiInboundService.delete_inbound(id)
|
||||||
@ -128,6 +214,7 @@ def delete_semi(id):
|
|||||||
# 5. [新增] 获取关联出库历史
|
# 5. [新增] 获取关联出库历史
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_semi_bp.route('/<int:id>/history', methods=['GET'])
|
@inbound_semi_bp.route('/<int:id>/history', methods=['GET'])
|
||||||
|
@permission_required('inbound_semi')
|
||||||
def get_history(id):
|
def get_history(id):
|
||||||
try:
|
try:
|
||||||
data = SemiInboundService.get_outbound_history(id)
|
data = SemiInboundService.get_outbound_history(id)
|
||||||
@ -145,6 +232,7 @@ def get_history(id):
|
|||||||
# 6. 系统用户建议
|
# 6. 系统用户建议
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_semi_bp.route('/suggestions/users', methods=['GET'])
|
@inbound_semi_bp.route('/suggestions/users', methods=['GET'])
|
||||||
|
@permission_required('inbound_semi')
|
||||||
def get_user_suggestions():
|
def get_user_suggestions():
|
||||||
keyword = request.args.get('keyword', '')
|
keyword = request.args.get('keyword', '')
|
||||||
data = SemiInboundService.search_system_users(keyword)
|
data = SemiInboundService.search_system_users(keyword)
|
||||||
@ -155,6 +243,7 @@ def get_user_suggestions():
|
|||||||
# 7. 获取筛选选项
|
# 7. 获取筛选选项
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@inbound_semi_bp.route('/options', methods=['GET'])
|
@inbound_semi_bp.route('/options', methods=['GET'])
|
||||||
|
@permission_required('inbound_semi')
|
||||||
def get_options():
|
def get_options():
|
||||||
try:
|
try:
|
||||||
data = SemiInboundService.get_filter_options()
|
data = SemiInboundService.get_filter_options()
|
||||||
|
|||||||
@ -3,21 +3,73 @@ from flask import request, jsonify, current_app
|
|||||||
from flask_jwt_extended import jwt_required
|
from flask_jwt_extended import jwt_required
|
||||||
from . import inbound_bp
|
from . import inbound_bp
|
||||||
from app.services.inbound.service_service import ServiceService
|
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
|
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'])
|
@inbound_bp.route('/service/search-base', methods=['GET'])
|
||||||
@jwt_required()
|
@permission_required('inbound_service')
|
||||||
def search_base():
|
def search_base():
|
||||||
"""搜索基础物料"""
|
"""搜索基础物料"""
|
||||||
keyword = request.args.get('keyword', '')
|
keyword = request.args.get('keyword', '')
|
||||||
try:
|
try:
|
||||||
data = ServiceService.search_base_material(keyword)
|
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({
|
return jsonify({
|
||||||
'code': 200,
|
'code': 200,
|
||||||
'msg': 'success',
|
'msg': 'success',
|
||||||
'data': data
|
'data': filtered_data
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f'搜索基础物料失败: {str(e)}')
|
current_app.logger.error(f'搜索基础物料失败: {str(e)}')
|
||||||
@ -25,7 +77,7 @@ def search_base():
|
|||||||
|
|
||||||
|
|
||||||
@inbound_bp.route('/service', methods=['GET'])
|
@inbound_bp.route('/service', methods=['GET'])
|
||||||
@jwt_required()
|
@permission_required('inbound_service')
|
||||||
def get_service_list():
|
def get_service_list():
|
||||||
"""获取服务权益列表"""
|
"""获取服务权益列表"""
|
||||||
page = request.args.get('page', 1, type=int)
|
page = request.args.get('page', 1, type=int)
|
||||||
@ -44,6 +96,9 @@ def get_service_list():
|
|||||||
end_date=end_date,
|
end_date=end_date,
|
||||||
provider_name=provider_name
|
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({
|
return jsonify({
|
||||||
'code': 200,
|
'code': 200,
|
||||||
'msg': 'success',
|
'msg': 'success',
|
||||||
@ -56,8 +111,7 @@ def get_service_list():
|
|||||||
|
|
||||||
|
|
||||||
@inbound_bp.route('/service', methods=['POST'])
|
@inbound_bp.route('/service', methods=['POST'])
|
||||||
@jwt_required()
|
@permission_required('inbound_service:operation')
|
||||||
@role_required('admin,manager')
|
|
||||||
def create_service():
|
def create_service():
|
||||||
"""创建服务权益"""
|
"""创建服务权益"""
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
@ -72,10 +126,12 @@ def create_service():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
service = ServiceService.create_service(data)
|
service = ServiceService.create_service(data)
|
||||||
|
user_permissions = get_current_user_permissions()
|
||||||
|
filtered_data = filter_item_by_permissions(service.to_dict(), user_permissions)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'code': 201,
|
'code': 201,
|
||||||
'msg': '创建成功',
|
'msg': '创建成功',
|
||||||
'data': service.to_dict()
|
'data': filtered_data
|
||||||
}), 201
|
}), 201
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return jsonify({'code': 400, 'msg': str(e)}), 400
|
return jsonify({'code': 400, 'msg': str(e)}), 400
|
||||||
@ -86,15 +142,17 @@ def create_service():
|
|||||||
|
|
||||||
|
|
||||||
@inbound_bp.route('/service/<int:service_id>', methods=['GET'])
|
@inbound_bp.route('/service/<int:service_id>', methods=['GET'])
|
||||||
@jwt_required()
|
@permission_required('inbound_service')
|
||||||
def get_service(service_id):
|
def get_service(service_id):
|
||||||
"""获取单个服务权益详情"""
|
"""获取单个服务权益详情"""
|
||||||
try:
|
try:
|
||||||
service = ServiceService.get_service(service_id)
|
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({
|
return jsonify({
|
||||||
'code': 200,
|
'code': 200,
|
||||||
'msg': 'success',
|
'msg': 'success',
|
||||||
'data': service.to_dict()
|
'data': filtered_data
|
||||||
})
|
})
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return jsonify({'code': 404, 'msg': str(e)}), 404
|
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'])
|
@inbound_bp.route('/service/<int:service_id>', methods=['PUT'])
|
||||||
@jwt_required()
|
@permission_required('inbound_service:operation')
|
||||||
@role_required('admin,manager')
|
|
||||||
def update_service(service_id):
|
def update_service(service_id):
|
||||||
"""更新服务权益"""
|
"""更新服务权益"""
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
@ -124,10 +181,12 @@ def update_service(service_id):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
service = ServiceService.update_service(service_id, filtered_data)
|
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({
|
return jsonify({
|
||||||
'code': 200,
|
'code': 200,
|
||||||
'msg': '更新成功',
|
'msg': '更新成功',
|
||||||
'data': service.to_dict()
|
'data': filtered_service
|
||||||
})
|
})
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return jsonify({'code': 404, 'msg': str(e)}), 404
|
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'])
|
@inbound_bp.route('/service/<int:service_id>', methods=['DELETE'])
|
||||||
@jwt_required()
|
@permission_required('inbound_service:operation')
|
||||||
@role_required('admin,manager')
|
|
||||||
def delete_service(service_id):
|
def delete_service(service_id):
|
||||||
"""删除服务权益"""
|
"""删除服务权益"""
|
||||||
try:
|
try:
|
||||||
@ -155,7 +213,7 @@ def delete_service(service_id):
|
|||||||
|
|
||||||
|
|
||||||
@inbound_bp.route('/service/suggestions/providers', methods=['GET'])
|
@inbound_bp.route('/service/suggestions/providers', methods=['GET'])
|
||||||
@jwt_required()
|
@permission_required('inbound_service')
|
||||||
def get_provider_suggestions():
|
def get_provider_suggestions():
|
||||||
base_id = request.args.get('base_id', type=int)
|
base_id = request.args.get('base_id', type=int)
|
||||||
if not base_id:
|
if not base_id:
|
||||||
@ -165,7 +223,7 @@ def get_provider_suggestions():
|
|||||||
|
|
||||||
|
|
||||||
@inbound_bp.route('/service/suggestions/users', methods=['GET'])
|
@inbound_bp.route('/service/suggestions/users', methods=['GET'])
|
||||||
@jwt_required()
|
@permission_required('inbound_service')
|
||||||
def get_user_suggestions():
|
def get_user_suggestions():
|
||||||
keyword = request.args.get('keyword', '')
|
keyword = request.args.get('keyword', '')
|
||||||
data = ServiceService.search_system_users(keyword)
|
data = ServiceService.search_system_users(keyword)
|
||||||
@ -173,7 +231,7 @@ def get_user_suggestions():
|
|||||||
|
|
||||||
|
|
||||||
@inbound_bp.route('/service/options', methods=['GET'])
|
@inbound_bp.route('/service/options', methods=['GET'])
|
||||||
@jwt_required()
|
@permission_required('inbound_service')
|
||||||
def get_options():
|
def get_options():
|
||||||
try:
|
try:
|
||||||
data = ServiceService.get_filter_options()
|
data = ServiceService.get_filter_options()
|
||||||
|
|||||||
@ -2,6 +2,7 @@ from flask import Blueprint, jsonify, request
|
|||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
# ★★★ 修复点:必须引入 datetime,否则下方更新时间时会报错 500 ★★★
|
# ★★★ 修复点:必须引入 datetime,否则下方更新时间时会报错 500 ★★★
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from app.utils.decorators import permission_required
|
||||||
|
|
||||||
# 导入模型
|
# 导入模型
|
||||||
from app.models.inbound.buy import StockBuy
|
from app.models.inbound.buy import StockBuy
|
||||||
@ -24,6 +25,7 @@ bp = Blueprint('stock_ops', __name__)
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/all', methods=['GET'])
|
@bp.route('/all', methods=['GET'])
|
||||||
|
@permission_required('inventory_stocktake')
|
||||||
def get_all_stock():
|
def get_all_stock():
|
||||||
"""
|
"""
|
||||||
获取所有库存 > 0 的物品
|
获取所有库存 > 0 的物品
|
||||||
@ -63,6 +65,7 @@ def get_all_stock():
|
|||||||
# --- 草稿箱接口 ---
|
# --- 草稿箱接口 ---
|
||||||
|
|
||||||
@bp.route('/draft/list', methods=['GET'])
|
@bp.route('/draft/list', methods=['GET'])
|
||||||
|
@permission_required('inventory_stocktake')
|
||||||
def get_drafts():
|
def get_drafts():
|
||||||
"""获取当前用户的盘点进度"""
|
"""获取当前用户的盘点进度"""
|
||||||
user_id = request.args.get('user_id', 'admin')
|
user_id = request.args.get('user_id', 'admin')
|
||||||
@ -71,6 +74,7 @@ def get_drafts():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/draft/add', methods=['POST'])
|
@bp.route('/draft/add', methods=['POST'])
|
||||||
|
@permission_required('inventory_stocktake:operation')
|
||||||
def add_draft():
|
def add_draft():
|
||||||
"""扫码同步 (支持更新数量)"""
|
"""扫码同步 (支持更新数量)"""
|
||||||
try:
|
try:
|
||||||
@ -100,6 +104,7 @@ def add_draft():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/draft/clear', methods=['POST'])
|
@bp.route('/draft/clear', methods=['POST'])
|
||||||
|
@permission_required('inventory_stocktake:operation')
|
||||||
def clear_draft():
|
def clear_draft():
|
||||||
"""清空进度"""
|
"""清空进度"""
|
||||||
data = request.json
|
data = request.json
|
||||||
@ -113,6 +118,7 @@ def clear_draft():
|
|||||||
# --- 打印接口 ---
|
# --- 打印接口 ---
|
||||||
|
|
||||||
@bp.route('/print/selection', methods=['POST'])
|
@bp.route('/print/selection', methods=['POST'])
|
||||||
|
@permission_required('inventory_stocktake:operation')
|
||||||
def print_selection():
|
def print_selection():
|
||||||
try:
|
try:
|
||||||
data = request.json
|
data = request.json
|
||||||
@ -126,6 +132,7 @@ def print_selection():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route('/print/stocktake', methods=['POST'])
|
@bp.route('/print/stocktake', methods=['POST'])
|
||||||
|
@permission_required('inventory_stocktake:operation')
|
||||||
def print_stocktake():
|
def print_stocktake():
|
||||||
try:
|
try:
|
||||||
data = request.json
|
data = request.json
|
||||||
|
|||||||
@ -1,17 +1,80 @@
|
|||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from app.services.outbound_service import OutboundService
|
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
|
import traceback
|
||||||
|
|
||||||
outbound_bp = Blueprint('outbound', __name__, url_prefix='/outbound')
|
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. 扫码查询库存接口 (关联三个库存表)
|
# 1. 扫码查询库存接口 (关联三个库存表)
|
||||||
# GET /api/v1/outbound/scan?barcode=...
|
# GET /api/v1/outbound/scan?barcode=...
|
||||||
# --------------------------------------------------------
|
# --------------------------------------------------------
|
||||||
@outbound_bp.route('/scan', methods=['GET'])
|
@outbound_bp.route('/scan', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
|
@permission_required('outbound_selection')
|
||||||
def scan_barcode():
|
def scan_barcode():
|
||||||
barcode = request.args.get('barcode')
|
barcode = request.args.get('barcode')
|
||||||
if not barcode:
|
if not barcode:
|
||||||
@ -45,6 +108,19 @@ def scan_barcode():
|
|||||||
@outbound_bp.route('', methods=['POST'])
|
@outbound_bp.route('', methods=['POST'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
def create_outbound():
|
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()
|
data = request.get_json()
|
||||||
if not data:
|
if not data:
|
||||||
return jsonify({'code': 400, 'msg': '无有效数据'}), 400
|
return jsonify({'code': 400, 'msg': '无有效数据'}), 400
|
||||||
@ -89,6 +165,7 @@ def create_outbound():
|
|||||||
# --------------------------------------------------------
|
# --------------------------------------------------------
|
||||||
@outbound_bp.route('', methods=['GET'])
|
@outbound_bp.route('', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
|
@permission_required('outbound_list')
|
||||||
def get_outbound_list():
|
def get_outbound_list():
|
||||||
try:
|
try:
|
||||||
page = int(request.args.get('page', 1))
|
page = int(request.args.get('page', 1))
|
||||||
@ -99,6 +176,11 @@ def get_outbound_list():
|
|||||||
# ★ [修改] 调用分组查询服务
|
# ★ [修改] 调用分组查询服务
|
||||||
result = OutboundService.get_grouped_list(page, limit, keyword)
|
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({
|
return jsonify({
|
||||||
'code': 200,
|
'code': 200,
|
||||||
'msg': '获取成功',
|
'msg': '获取成功',
|
||||||
|
|||||||
48
inventory-backend/app/api/v1/permission.py
Normal file
48
inventory-backend/app/api/v1/permission.py
Normal 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
|
||||||
@ -1,14 +1,64 @@
|
|||||||
from flask import Blueprint, jsonify, request # .material -> .base refactor checked
|
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
|
from app.services.trans_service import TransService
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
trans_bp = Blueprint('transactions', __name__, url_prefix='/transactions')
|
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'])
|
@trans_bp.route('/borrow', methods=['POST'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
|
@permission_required('op_borrow:operation')
|
||||||
def create_borrow():
|
def create_borrow():
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
try:
|
try:
|
||||||
@ -21,6 +71,7 @@ def create_borrow():
|
|||||||
# --- 还库辅助:扫码查找借出记录 ---
|
# --- 还库辅助:扫码查找借出记录 ---
|
||||||
@trans_bp.route('/return/scan', methods=['GET'])
|
@trans_bp.route('/return/scan', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
|
@permission_required('op_return')
|
||||||
def scan_borrowed_item():
|
def scan_borrowed_item():
|
||||||
barcode = request.args.get('barcode')
|
barcode = request.args.get('barcode')
|
||||||
if not barcode:
|
if not barcode:
|
||||||
@ -36,6 +87,7 @@ def scan_borrowed_item():
|
|||||||
# --- 还库提交 ---
|
# --- 还库提交 ---
|
||||||
@trans_bp.route('/return', methods=['POST'])
|
@trans_bp.route('/return', methods=['POST'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
|
@permission_required('op_return:operation')
|
||||||
def submit_return():
|
def submit_return():
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
user = get_jwt_identity() # 库管
|
user = get_jwt_identity() # 库管
|
||||||
@ -49,10 +101,15 @@ def submit_return():
|
|||||||
# --- 记录列表 ---
|
# --- 记录列表 ---
|
||||||
@trans_bp.route('/records', methods=['GET'])
|
@trans_bp.route('/records', methods=['GET'])
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
|
@permission_required('op_records')
|
||||||
def get_records():
|
def get_records():
|
||||||
status = request.args.get('status', 'all')
|
status = request.args.get('status', 'all')
|
||||||
page = int(request.args.get('page', 1))
|
page = int(request.args.get('page', 1))
|
||||||
keyword = request.args.get('keyword', '')
|
keyword = request.args.get('keyword', '')
|
||||||
|
|
||||||
res = TransService.get_records(page=page, limit=10, status=status, keyword=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})
|
return jsonify({'code': 200, 'data': res})
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
# app/models/system.py
|
# inventory-backend/app/models/system.py
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 1. 系统用户表
|
||||||
|
# ==========================================
|
||||||
class SysUser(db.Model):
|
class SysUser(db.Model):
|
||||||
"""
|
"""
|
||||||
系统用户表
|
系统用户表
|
||||||
对应数据库: sys_user
|
对应数据库: sys_user
|
||||||
username 字段存储格式约定: "真实姓名/登录账号" (例如: 张三/zhangsan)
|
username 字段存储格式约定: "真实姓名/登录账号" (例如: 张三/zhangsan01)
|
||||||
"""
|
"""
|
||||||
__tablename__ = 'sys_user'
|
__tablename__ = 'sys_user'
|
||||||
|
|
||||||
@ -19,8 +22,7 @@ class SysUser(db.Model):
|
|||||||
role = db.Column(db.String(50))
|
role = db.Column(db.String(50))
|
||||||
status = db.Column(db.String(20), default='active')
|
status = db.Column(db.String(20), default='active')
|
||||||
password_hash = db.Column(db.Text)
|
password_hash = db.Column(db.Text)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||||
# created_at 已在数据库脚本中移除,此处不再定义
|
|
||||||
|
|
||||||
def set_password(self, password):
|
def set_password(self, password):
|
||||||
"""生成加密密码"""
|
"""生成加密密码"""
|
||||||
@ -45,23 +47,27 @@ class SysUser(db.Model):
|
|||||||
parts = raw_name.split('/')
|
parts = raw_name.split('/')
|
||||||
real_name = parts[0]
|
real_name = parts[0]
|
||||||
acc_id = parts[1]
|
acc_id = parts[1]
|
||||||
# 格式化为前端展示格式: 张三(zhangsan)
|
# 格式化为前端展示格式: 张三(zhangsan01)
|
||||||
display_name = f"{real_name}({acc_id})"
|
display_name = f"{real_name}({acc_id})"
|
||||||
# 单独提取账号ID (如果前端需要单独用)
|
# 单独提取账号ID (如果前端需要单独用)
|
||||||
account_id = acc_id
|
account_id = acc_id
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': self.id,
|
'id': self.id,
|
||||||
'username': display_name, # 列表显示: 张三(zhangsan)
|
'username': display_name, # 列表显示: 张三(zhangsan01)
|
||||||
'raw_username': self.username, # 原始数据
|
'raw_username': self.username, # 原始数据
|
||||||
'account_id': account_id, # 纯账号ID: zhangsan
|
'account_id': account_id, # 纯账号ID: zhangsan01
|
||||||
'email': self.email,
|
'email': self.email,
|
||||||
'department': self.department,
|
'department': self.department,
|
||||||
'role': self.role,
|
'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):
|
class SysLog(db.Model):
|
||||||
"""
|
"""
|
||||||
系统操作日志表
|
系统操作日志表
|
||||||
@ -89,3 +95,57 @@ class SysLog(db.Model):
|
|||||||
'action_type': self.action_type,
|
'action_type': self.action_type,
|
||||||
'description': self.description
|
'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'
|
||||||
@ -1,11 +1,10 @@
|
|||||||
# app/services/auth_service.py
|
# 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 app.extensions import db
|
||||||
from flask_jwt_extended import create_access_token
|
from flask_jwt_extended import create_access_token
|
||||||
from app.utils.constants import UserRole
|
from app.utils.constants import UserRole
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|
||||||
class AuthService:
|
class AuthService:
|
||||||
# 硬编码的超级管理员凭证
|
# 硬编码的超级管理员凭证
|
||||||
SUPER_ADMIN_USER = "IRIS"
|
SUPER_ADMIN_USER = "IRIS"
|
||||||
@ -212,3 +211,36 @@ class AuthService:
|
|||||||
db.session.delete(user)
|
db.session.delete(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return True
|
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
|
||||||
|
}
|
||||||
149
inventory-backend/app/services/permission_service.py
Normal file
149
inventory-backend/app/services/permission_service.py
Normal 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
|
||||||
@ -1,7 +1,8 @@
|
|||||||
# app/utils/decorators.py
|
# app/utils/decorators.py
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from flask_jwt_extended import get_jwt
|
from flask_jwt_extended import get_jwt, verify_jwt_in_request
|
||||||
from flask import jsonify
|
from flask import jsonify, g
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
def role_required(*roles):
|
def role_required(*roles):
|
||||||
@ -28,3 +29,60 @@ def role_required(*roles):
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
return wrapper
|
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
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||||
import { InfoFilled, SwitchButton, UserFilled } from '@element-plus/icons-vue'
|
import { InfoFilled, SwitchButton, UserFilled } from '@element-plus/icons-vue'
|
||||||
@ -14,6 +14,13 @@ const isLoginPage = computed(() => {
|
|||||||
return route.path === '/login'
|
return route.path === '/login'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 页面加载时刷新权限
|
||||||
|
onMounted(() => {
|
||||||
|
if (userStore.token) {
|
||||||
|
userStore.refreshUserPermissions()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// --- 退出登录逻辑 Start ---
|
// --- 退出登录逻辑 Start ---
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
ElMessageBox.confirm(
|
ElMessageBox.confirm(
|
||||||
@ -82,7 +89,7 @@ const handleLogout = () => {
|
|||||||
<footer v-if="!isLoginPage" class="app-footer">
|
<footer v-if="!isLoginPage" class="app-footer">
|
||||||
<span class="version-tag">
|
<span class="version-tag">
|
||||||
<el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon>
|
<el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon>
|
||||||
当前版本: 1.3 Beta (2.25权限管理版)
|
当前版本: 1.4 Beta (2.27权限管理版)
|
||||||
</span>
|
</span>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
26
inventory-web/src/api/system/permission.ts
Normal file
26
inventory-web/src/api/system/permission.ts
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
134
inventory-web/src/components/BaseTable/index.vue
Normal file
134
inventory-web/src/components/BaseTable/index.vue
Normal 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>
|
||||||
@ -195,9 +195,19 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
meta: {
|
meta: {
|
||||||
title: '账号开通',
|
title: '账号开通',
|
||||||
icon: 'User',
|
icon: 'User',
|
||||||
// 子路由也建议加上权限限制
|
|
||||||
roles: ['SUPER_ADMIN', 'SUPERVISOR']
|
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')
|
const token = userStore.token || localStorage.getItem('token')
|
||||||
|
|
||||||
// [修复] 优先从 user 对象获取,并统一转大写,防止大小写不一致导致权限失效
|
// [修复] 优先从 user 对象获取,并统一转大写,防止大小写不一致导致权限失效
|
||||||
// 注意:Store 中存储的可能是 user.role 或者直接是 role,根据你之前的 store 结构适配
|
|
||||||
const rawRole = userStore.user?.role || userStore.role || localStorage.getItem('role') || 'user'
|
const rawRole = userStore.user?.role || userStore.role || localStorage.getItem('role') || 'user'
|
||||||
const userRole = String(rawRole).toUpperCase()
|
const userRole = String(rawRole).toUpperCase()
|
||||||
|
|
||||||
// 调试日志:如果跳转有问题,请按 F12 查看控制台输出
|
// 调试日志
|
||||||
if (to.path.includes('/system')) {
|
if (to.path.includes('/system')) {
|
||||||
console.log(`路由守卫检查: Path=${to.path}, UserRole=${userRole}, Required=${to.meta.roles}`)
|
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)) {
|
if (to.meta.roles && Array.isArray(to.meta.roles)) {
|
||||||
// [修复] to.meta.roles 里已经是大写了,userRole 也转大写了,现在可以安全比对
|
|
||||||
if (to.meta.roles.includes(userRole)) {
|
if (to.meta.roles.includes(userRole)) {
|
||||||
next()
|
next()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
49
inventory-web/src/stores/permission.ts
Normal file
49
inventory-web/src/stores/permission.ts
Normal 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
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { login } from '@/api/auth'
|
import { login } from '@/api/auth'
|
||||||
|
import { getRolePermissions } from '@/api/system/permission'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
export const useUserStore = defineStore('user', () => {
|
export const useUserStore = defineStore('user', () => {
|
||||||
@ -7,6 +8,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
const token = ref(localStorage.getItem('token') || '')
|
const token = ref(localStorage.getItem('token') || '')
|
||||||
const role = ref(localStorage.getItem('role') || '')
|
const role = ref(localStorage.getItem('role') || '')
|
||||||
const username = ref(localStorage.getItem('username') || '')
|
const username = ref(localStorage.getItem('username') || '')
|
||||||
|
const permissions = ref<string[]>(JSON.parse(localStorage.getItem('permissions') || '[]'))
|
||||||
|
|
||||||
// 2. Actions
|
// 2. Actions
|
||||||
// 登录逻辑
|
// 登录逻辑
|
||||||
@ -44,6 +46,25 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
// 持久化存储 Token
|
// 持久化存储 Token
|
||||||
localStorage.setItem('token', data.access_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 表示登录成功
|
return true // 返回 true 表示登录成功
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,11 +74,36 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
token.value = ''
|
token.value = ''
|
||||||
role.value = ''
|
role.value = ''
|
||||||
username.value = ''
|
username.value = ''
|
||||||
|
permissions.value = []
|
||||||
|
|
||||||
// 2. 清空 LocalStorage (硬盘)
|
// 2. 清空 LocalStorage (硬盘)
|
||||||
localStorage.removeItem('token')
|
localStorage.removeItem('token')
|
||||||
localStorage.removeItem('role')
|
localStorage.removeItem('role')
|
||||||
localStorage.removeItem('username')
|
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
|
// 3. Getters / Helpers
|
||||||
@ -66,12 +112,20 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
return roles.includes(role.value)
|
return roles.includes(role.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 判断当前用户是否拥有某个权限(菜单或元素)
|
||||||
|
const hasPermission = (code: string) => {
|
||||||
|
return permissions.value.includes(code)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token,
|
token,
|
||||||
role,
|
role,
|
||||||
username,
|
username,
|
||||||
|
permissions,
|
||||||
handleLogin,
|
handleLogin,
|
||||||
logout,
|
logout,
|
||||||
hasRole
|
refreshUserPermissions,
|
||||||
|
hasRole,
|
||||||
|
hasPermission
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -52,11 +52,13 @@
|
|||||||
import { ref, reactive } from 'vue'
|
import { ref, reactive } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useUserStore } from '@/stores/user'
|
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'
|
import { User, Lock } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const permissionStore = usePermissionStore() // [新增]
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const loginFormRef = ref()
|
const loginFormRef = ref()
|
||||||
|
|
||||||
@ -74,23 +76,25 @@ const onLogin = async () => {
|
|||||||
if (valid) {
|
if (valid) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
// 执行登录请求
|
// 1. 执行登录请求
|
||||||
const success = await userStore.handleLogin(loginForm)
|
const success = await userStore.handleLogin(loginForm)
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
// 成功:跳转
|
// [新增] 2. 登录成功后,立即拉取当前用户的权限字典
|
||||||
|
// 这样进入 Dashboard 时,所有按钮/列的显示状态就已经确定了
|
||||||
|
await permissionStore.loadPermissions()
|
||||||
|
|
||||||
|
// 3. 跳转
|
||||||
router.push('/dashboard')
|
router.push('/dashboard')
|
||||||
} else {
|
} else {
|
||||||
// 失败(业务逻辑拒绝,如账号密码错):弹出模态框
|
// 失败(业务逻辑拒绝):弹出模态框
|
||||||
showLoginFailAlert('用户名或密码错误')
|
showLoginFailAlert('用户名或密码错误')
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// 失败(系统错误,如网络断开/500报错):弹出模态框
|
// 失败(系统错误):弹出模态框
|
||||||
// 优先取后端的报错信息,没有则显示默认
|
|
||||||
const msg = error.response?.data?.msg || error.message || '登录遇到未知错误'
|
const msg = error.response?.data?.msg || error.message || '登录遇到未知错误'
|
||||||
showLoginFailAlert(msg)
|
showLoginFailAlert(msg)
|
||||||
} finally {
|
} finally {
|
||||||
// 停止转圈,让用户可以看清弹窗
|
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -103,8 +107,6 @@ const showLoginFailAlert = (msg: string) => {
|
|||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
callback: () => {
|
callback: () => {
|
||||||
// 点击确定后,清空密码框,让用户重试
|
|
||||||
// 页面绝对不会刷新,光标还在
|
|
||||||
loginForm.password = ''
|
loginForm.password = ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -71,7 +71,7 @@
|
|||||||
<el-icon style="margin-right: 5px"><Download /></el-icon>导出库存统计
|
<el-icon style="margin-right: 5px"><Download /></el-icon>导出库存统计
|
||||||
</el-button>
|
</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-icon style="margin-right: 5px"><Plus /></el-icon>新增
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
@ -98,18 +98,18 @@
|
|||||||
<div style="font-weight: bold; margin-bottom: 5px; border-bottom: 1px solid #eee; padding-bottom: 5px">
|
<div style="font-weight: bold; margin-bottom: 5px; border-bottom: 1px solid #eee; padding-bottom: 5px">
|
||||||
列展示设置
|
列展示设置
|
||||||
</div>
|
</div>
|
||||||
<el-checkbox v-model="columns.id.visible" label="ID" />
|
<el-checkbox v-model="columns.id.visible" label="ID" :disabled="!userStore.hasPermission(permissionMap.id)" />
|
||||||
<el-checkbox v-model="columns.companyName.visible" label="所属公司" />
|
<el-checkbox v-model="columns.companyName.visible" label="所属公司" :disabled="!userStore.hasPermission(permissionMap.companyName)" />
|
||||||
<el-checkbox v-model="columns.name.visible" label="名称" />
|
<el-checkbox v-model="columns.name.visible" label="名称" :disabled="!userStore.hasPermission(permissionMap.name)" />
|
||||||
<el-checkbox v-model="columns.commonName.visible" label="俗名" />
|
<el-checkbox v-model="columns.commonName.visible" label="俗名" :disabled="!userStore.hasPermission(permissionMap.commonName)" />
|
||||||
<el-checkbox v-model="columns.category.visible" label="类别" />
|
<el-checkbox v-model="columns.category.visible" label="类别" :disabled="!userStore.hasPermission(permissionMap.category)" />
|
||||||
<el-checkbox v-model="columns.type.visible" label="类型" />
|
<el-checkbox v-model="columns.type.visible" label="类型" :disabled="!userStore.hasPermission(permissionMap.type)" />
|
||||||
<el-checkbox v-model="columns.spec.visible" label="规格型号" />
|
<el-checkbox v-model="columns.spec.visible" label="规格型号" :disabled="!userStore.hasPermission(permissionMap.spec)" />
|
||||||
<el-checkbox v-model="columns.unit.visible" label="单位" />
|
<el-checkbox v-model="columns.unit.visible" label="单位" :disabled="!userStore.hasPermission(permissionMap.unit)" />
|
||||||
<el-checkbox v-model="columns.inventory.visible" label="库存数" />
|
<el-checkbox v-model="columns.inventory.visible" label="库存数" :disabled="!userStore.hasPermission(permissionMap.inventory)" />
|
||||||
<el-checkbox v-model="columns.available.visible" label="可用数" />
|
<el-checkbox v-model="columns.available.visible" label="可用数" :disabled="!userStore.hasPermission(permissionMap.available)" />
|
||||||
<el-checkbox v-model="columns.files.visible" label="资料" />
|
<el-checkbox v-model="columns.files.visible" label="资料" :disabled="!userStore.hasPermission(permissionMap.files)" />
|
||||||
<el-checkbox v-model="columns.isEnabled.visible" label="状态" />
|
<el-checkbox v-model="columns.isEnabled.visible" label="状态" :disabled="!userStore.hasPermission(permissionMap.isEnabled)" />
|
||||||
</div>
|
</div>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
</div>
|
</div>
|
||||||
@ -210,14 +210,15 @@
|
|||||||
:active-value="1"
|
:active-value="1"
|
||||||
:inactive-value="0"
|
:inactive-value="0"
|
||||||
:loading="scope.row.statusLoading"
|
:loading="scope.row.statusLoading"
|
||||||
|
:disabled="!userStore.hasPermission('material_list:operation')"
|
||||||
@change="handleStatusChange(scope.row)"
|
@change="handleStatusChange(scope.row)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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">
|
<template #default="scope">
|
||||||
<el-button link type="primary" size="small" @click="handleEdit(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 link type="danger" size="small" @click="handleDelete(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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -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 { Plus, Document, Refresh, Setting, Rank, Camera, Link, Download } from '@element-plus/icons-vue';
|
||||||
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
|
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
|
||||||
import type { FormInstance, FormRules } from 'element-plus';
|
import type { FormInstance, FormRules } from 'element-plus';
|
||||||
|
import { useUserStore } from '@/stores/user';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
listMaterialBase,
|
listMaterialBase,
|
||||||
@ -432,6 +434,8 @@ import {
|
|||||||
import { uploadFile, deleteFile } from '@/api/common/upload';
|
import { uploadFile, deleteFile } from '@/api/common/upload';
|
||||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue';
|
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue';
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
// --- 类型定义 ---
|
// --- 类型定义 ---
|
||||||
interface MaterialBaseVO {
|
interface MaterialBaseVO {
|
||||||
id: number;
|
id: number;
|
||||||
@ -501,6 +505,39 @@ const columns = reactive({
|
|||||||
isEnabled: { visible: true }
|
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 companyOptions = ref<string[]>([]);
|
const companyOptions = ref<string[]>([]);
|
||||||
const categoryOptions = ref<string[]>([]);
|
const categoryOptions = ref<string[]>([]);
|
||||||
const typeOptions = ref<string[]>([]);
|
const typeOptions = ref<string[]>([]);
|
||||||
@ -988,6 +1025,8 @@ const handleCameraConfirm = async (file: File) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// 先根据权限初始化列显示状态
|
||||||
|
initColumnPermissions();
|
||||||
getList();
|
getList();
|
||||||
getOptionsList();
|
getOptionsList();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -8,14 +8,14 @@
|
|||||||
<span class="subtitle">(请添加需要出库的物品)</span>
|
<span class="subtitle">(请添加需要出库的物品)</span>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
||||||
<el-button type="warning" :icon="List" @click="openBomSelect">
|
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="warning" :icon="List" @click="openBomSelect">
|
||||||
按 BOM 套餐添加
|
按 BOM 套餐添加
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-divider direction="vertical" />
|
<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>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@ -71,6 +71,7 @@
|
|||||||
size="small"
|
size="small"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
controls-position="right"
|
controls-position="right"
|
||||||
|
:disabled="!userStore.hasPermission('outbound_selection:operation')"
|
||||||
@change="(val) => handleMainQuantityChange(val, row)"
|
@change="(val) => handleMainQuantityChange(val, row)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@ -78,7 +79,7 @@
|
|||||||
|
|
||||||
<el-table-column label="操作" width="80" align="center" fixed="right">
|
<el-table-column label="操作" width="80" align="center" fixed="right">
|
||||||
<template #default="{ $index }">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -133,6 +134,7 @@
|
|||||||
size="small"
|
size="small"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
|
:disabled="!userStore.hasPermission('outbound_selection:operation')"
|
||||||
@click.stop
|
@click.stop
|
||||||
@change="(val) => handleManualQuantityChange(val, row)"
|
@change="(val) => handleManualQuantityChange(val, row)"
|
||||||
/>
|
/>
|
||||||
@ -144,7 +146,7 @@
|
|||||||
已勾选 {{ tempSelection.length }} 项
|
已勾选 {{ tempSelection.length }} 项
|
||||||
</span>
|
</span>
|
||||||
<el-button @click="manualDialogVisible = false">取消</el-button>
|
<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>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
@ -161,7 +163,7 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="生产套数">
|
<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-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div style="margin-left: 100px; color: #909399; font-size: 12px;">
|
<div style="margin-left: 100px; color: #909399; font-size: 12px;">
|
||||||
@ -169,7 +171,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="bomSelectVisible = false">取消</el-button>
|
<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>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
@ -204,11 +206,11 @@
|
|||||||
<span class="dialog-footer">
|
<span class="dialog-footer">
|
||||||
<el-button @click="previewVisible = false">取消</el-button>
|
<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
|
导出 Excel
|
||||||
</el-button>
|
</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)
|
确认打印 (A4)
|
||||||
</el-button>
|
</el-button>
|
||||||
</span>
|
</span>
|
||||||
@ -283,6 +285,9 @@ import { Printer, Search, Plus, Download, List } from '@element-plus/icons-vue'
|
|||||||
import { ElMessage, ElTable, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElTable, ElMessageBox } from 'element-plus'
|
||||||
import { getAllStock, printSelectionList } from '@/api/inbound/stock'
|
import { getAllStock, printSelectionList } from '@/api/inbound/stock'
|
||||||
import { getBomList, getBomDetail } from '@/api/bom'
|
import { getBomList, getBomDetail } from '@/api/bom'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
// --- 状态变量 ---
|
// --- 状态变量 ---
|
||||||
const selectedItems = ref<any[]>([])
|
const selectedItems = ref<any[]>([])
|
||||||
|
|||||||
@ -17,10 +17,14 @@
|
|||||||
|
|
||||||
<div class="scan-section">
|
<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>
|
<el-icon :size="40" color="#409EFF"><CameraFilled /></el-icon>
|
||||||
<span class="text">点击开启全屏扫码</span>
|
<span class="text">点击开启全屏扫码</span>
|
||||||
</div>
|
</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">
|
<div class="input-box">
|
||||||
<el-input
|
<el-input
|
||||||
@ -30,12 +34,13 @@
|
|||||||
clearable
|
clearable
|
||||||
ref="barcodeRef"
|
ref="barcodeRef"
|
||||||
size="large"
|
size="large"
|
||||||
|
:disabled="!userStore.hasPermission('outbound_create:operation')"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<el-icon><Scissor /></el-icon>
|
<el-icon><Scissor /></el-icon>
|
||||||
</template>
|
</template>
|
||||||
<template #append>
|
<template #append>
|
||||||
<el-button @click="handleManualInput">添加</el-button>
|
<el-button @click="handleManualInput" :disabled="!userStore.hasPermission('outbound_create:operation')">添加</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</div>
|
</div>
|
||||||
@ -64,13 +69,14 @@
|
|||||||
:max="parseFloat(row.available_quantity)"
|
:max="parseFloat(row.available_quantity)"
|
||||||
size="small"
|
size="small"
|
||||||
style="width: 100px"
|
style="width: 100px"
|
||||||
|
:disabled="!userStore.hasPermission('outbound_create:operation')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column label="操作" width="60" align="center" fixed="right">
|
<el-table-column label="操作" width="60" align="center" fixed="right">
|
||||||
<template #default="{$index}">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -120,7 +126,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="电子签名确认" required>
|
<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">
|
<div v-if="signaturePreviewUrl" class="signed-img">
|
||||||
<img :src="signaturePreviewUrl" alt="签名" />
|
<img :src="signaturePreviewUrl" alt="签名" />
|
||||||
<span class="re-sign-tip">点击重签</span>
|
<span class="re-sign-tip">点击重签</span>
|
||||||
@ -130,11 +136,17 @@
|
|||||||
<span>点击此处进行全屏签名</span>
|
<span>点击此处进行全屏签名</span>
|
||||||
</div>
|
</div>
|
||||||
</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-item>
|
||||||
|
|
||||||
<div class="bottom-actions">
|
<div class="bottom-actions">
|
||||||
<el-button @click="clearAll" icon="Refresh">清空</el-button>
|
<el-button v-if="userStore.hasPermission('outbound_create:operation')" @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')" type="primary" size="large" :loading="loading" @click="submitForm" icon="Select">
|
||||||
确认出库
|
确认出库
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@ -205,6 +217,8 @@ import { getStockByBarcode, submitOutbound, getOutboundList } from '@/api/outbou
|
|||||||
import { uploadFile } from '@/api/common/upload'
|
import { uploadFile } from '@/api/common/upload'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
// --- 状态定义 ---
|
// --- 状态定义 ---
|
||||||
const barcodeInput = ref('')
|
const barcodeInput = ref('')
|
||||||
const cartItems = ref<any[]>([])
|
const cartItems = ref<any[]>([])
|
||||||
@ -212,7 +226,6 @@ const loading = ref(false)
|
|||||||
const showCamera = ref(false)
|
const showCamera = ref(false)
|
||||||
const barcodeRef = ref()
|
const barcodeRef = ref()
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
const userStore = useUserStore()
|
|
||||||
|
|
||||||
// 签名相关
|
// 签名相关
|
||||||
const showSignatureDialog = ref(false)
|
const showSignatureDialog = ref(false)
|
||||||
@ -292,6 +305,10 @@ const onScanSuccess = (code: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleManualInput = async () => {
|
const handleManualInput = async () => {
|
||||||
|
if (!userStore.hasPermission('outbound_create:operation')) {
|
||||||
|
ElMessage.warning('无操作权限')
|
||||||
|
return
|
||||||
|
}
|
||||||
const code = barcodeInput.value.trim()
|
const code = barcodeInput.value.trim()
|
||||||
if (!code) return
|
if (!code) return
|
||||||
|
|
||||||
@ -355,10 +372,18 @@ const handleManualInput = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const removeFromCart = (index: number) => {
|
const removeFromCart = (index: number) => {
|
||||||
|
if (!userStore.hasPermission('outbound_create:operation')) {
|
||||||
|
ElMessage.warning('无操作权限')
|
||||||
|
return
|
||||||
|
}
|
||||||
cartItems.value.splice(index, 1)
|
cartItems.value.splice(index, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearAll = () => {
|
const clearAll = () => {
|
||||||
|
if (!userStore.hasPermission('outbound_create:operation')) {
|
||||||
|
ElMessage.warning('无操作权限')
|
||||||
|
return
|
||||||
|
}
|
||||||
ElMessageBox.confirm('确定清空所有已选商品吗?', '提示', { type: 'warning' })
|
ElMessageBox.confirm('确定清空所有已选商品吗?', '提示', { type: 'warning' })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
cartItems.value = []
|
cartItems.value = []
|
||||||
@ -372,6 +397,10 @@ const clearAll = () => {
|
|||||||
|
|
||||||
// --- 提交逻辑 ---
|
// --- 提交逻辑 ---
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
|
if (!userStore.hasPermission('outbound_create:operation')) {
|
||||||
|
ElMessage.warning('无操作权限')
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!formRef.value) return
|
if (!formRef.value) return
|
||||||
if (cartItems.value.length === 0) return ElMessage.warning('请先添加商品')
|
if (cartItems.value.length === 0) return ElMessage.warning('请先添加商品')
|
||||||
|
|
||||||
@ -427,7 +456,13 @@ const submitForm = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- 签名逻辑 (Canvas) ---
|
// --- 签名逻辑 (Canvas) ---
|
||||||
const openSignatureDialog = () => { showSignatureDialog.value = true }
|
const openSignatureDialog = () => {
|
||||||
|
if (!userStore.hasPermission('outbound_create:operation')) {
|
||||||
|
ElMessage.warning('无签名权限')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showSignatureDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
const initCanvas = async () => {
|
const initCanvas = async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
value-format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
||||||
/>
|
/>
|
||||||
<el-button type="primary" class="filter-item" style="margin-left: 10px;" @click="fetchData">查询</el-button>
|
<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>
|
</div>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
@ -35,18 +35,18 @@
|
|||||||
<div style="padding: 10px 40px; background: #fafafa;">
|
<div style="padding: 10px 40px; background: #fafafa;">
|
||||||
<h4 style="margin: 0 0 10px 0; color: #409EFF;">商品明细 (按单价降序)</h4>
|
<h4 style="margin: 0 0 10px 0; color: #409EFF;">商品明细 (按单价降序)</h4>
|
||||||
<el-table :data="props.row.items" border size="small">
|
<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 v-if="hasColumnPermission('name')" prop="name" label="名称" min-width="150" show-overflow-tooltip />
|
||||||
<el-table-column prop="material_type" label="类型" width="120" show-overflow-tooltip />
|
<el-table-column v-if="hasColumnPermission('material_type')" prop="material_type" label="类型" width="120" show-overflow-tooltip />
|
||||||
<el-table-column prop="category" label="类别" width="120" show-overflow-tooltip />
|
<el-table-column v-if="hasColumnPermission('category')" 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('spec_model')" prop="spec_model" label="规格型号" min-width="150" show-overflow-tooltip />
|
||||||
|
|
||||||
<el-table-column prop="quantity" label="数量" width="100" />
|
<el-table-column v-if="hasColumnPermission('quantity')" prop="quantity" label="数量" width="100" />
|
||||||
<el-table-column prop="unit_price" label="单价" width="120">
|
<el-table-column v-if="hasColumnPermission('unit_price')" prop="unit_price" label="单价" width="120">
|
||||||
<template #default="{row}">¥{{ row.unit_price }}</template>
|
<template #default="{row}">¥{{ row.unit_price }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="subtotal" label="小计">
|
<el-table-column v-if="hasColumnPermission('subtotal')" prop="subtotal" label="小计">
|
||||||
<template #default="{row}">
|
<template #default="{row}">
|
||||||
<span style="color: #F56C6C; font-weight: bold;">¥{{ row.subtotal.toFixed(2) }}</span>
|
<span style="color: #F56C6C; font-weight: bold;">¥{{ row.subtotal.toFixed(2) }}</span>
|
||||||
</template>
|
</template>
|
||||||
@ -56,33 +56,33 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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 }">
|
<template #default="{ row }">
|
||||||
<span>{{ row.outbound_time ? row.outbound_time.substring(0, 16) : '' }}</span>
|
<span>{{ row.outbound_time ? row.outbound_time.substring(0, 16) : '' }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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 }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="getTagType(row.outbound_type)">{{ formatType(row.outbound_type) }}</el-tag>
|
<el-tag :type="getTagType(row.outbound_type)">{{ formatType(row.outbound_type) }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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 }">
|
<template #default="{ row }">
|
||||||
<span style="color: #F56C6C; font-weight: bold; font-size: 15px;">¥{{ row.total_amount }}</span>
|
<span style="color: #F56C6C; font-weight: bold; font-size: 15px;">¥{{ row.total_amount }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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 }">
|
<template #default="{ row }">
|
||||||
<div v-if="row.signature_path" class="signature-cell">
|
<div v-if="row.signature_path" class="signature-cell">
|
||||||
<el-image
|
<el-image
|
||||||
@ -121,6 +121,39 @@
|
|||||||
import { ref, onMounted, reactive } from 'vue'
|
import { ref, onMounted, reactive } from 'vue'
|
||||||
import { getOutboundList } from '@/api/outbound'
|
import { getOutboundList } from '@/api/outbound'
|
||||||
import { Picture } from '@element-plus/icons-vue'
|
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 list = ref([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
|
|||||||
@ -56,7 +56,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="right-actions">
|
<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-button :icon="Refresh" circle @click="fetchData" class="circle-btn" />
|
||||||
|
|
||||||
<el-popover placement="bottom-end" title="列配置" :width="500" trigger="click">
|
<el-popover placement="bottom-end" title="列配置" :width="500" trigger="click">
|
||||||
@ -67,13 +67,13 @@
|
|||||||
<div class="col-group-title">基础信息</div>
|
<div class="col-group-title">基础信息</div>
|
||||||
<el-row :gutter="10">
|
<el-row :gutter="10">
|
||||||
<el-col :span="12" v-for="c in baseColumns" :key="c.prop">
|
<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-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<div class="col-group-title" style="margin-top:10px">库存与商务</div>
|
<div class="col-group-title" style="margin-top:10px">库存与商务</div>
|
||||||
<el-row :gutter="10">
|
<el-row :gutter="10">
|
||||||
<el-col :span="12" v-for="c in stockColumns" :key="c.prop">
|
<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-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
@ -167,7 +167,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</template>
|
</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 }">
|
<template #default="{ row }">
|
||||||
<el-button link type="warning" size="default" @click="handlePrint(row)">
|
<el-button link type="warning" size="default" @click="handlePrint(row)">
|
||||||
<el-icon><Printer/></el-icon> 打印
|
<el-icon><Printer/></el-icon> 打印
|
||||||
@ -587,6 +587,7 @@ import {
|
|||||||
} from '@/api/inbound/buy'
|
} from '@/api/inbound/buy'
|
||||||
import {getLabelPreview, executePrint} from '@/api/common/print'
|
import {getLabelPreview, executePrint} from '@/api/common/print'
|
||||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// 自定义指令:v-loadmore (适配 Teleport 到 Body 的下拉框)
|
// 自定义指令:v-loadmore (适配 Teleport 到 Body 的下拉框)
|
||||||
@ -613,6 +614,7 @@ const vLoadmore = {
|
|||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// 状态与变量
|
// 状态与变量
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
|
const userStore = useUserStore()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
@ -701,6 +703,78 @@ const stockColumns = [
|
|||||||
{prop: 'inspection_report', label: '检测报告', minWidth: '100'}
|
{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 allColumns = [...baseColumns, ...stockColumns]
|
||||||
|
|
||||||
const defaultColumns = [
|
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)}` }
|
const formatMoney = (val: any, currency = '¥') => { const num = Number(val); return isNaN(num) ? '-' : `${currency} ${num.toFixed(2)}` }
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// 先根据权限初始化列显示状态
|
||||||
|
initColumnPermissions()
|
||||||
fetchData()
|
fetchData()
|
||||||
fetchOptions()
|
fetchOptions()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -41,13 +41,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="right-tools">
|
<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-button :icon="Refresh" @click="fetchData" class="action-btn">刷新</el-button>
|
||||||
<el-popover placement="bottom-end" title="列配置" :width="500" trigger="click">
|
<el-popover placement="bottom-end" title="列配置" :width="500" trigger="click">
|
||||||
<template #reference><el-button :icon="Setting" class="action-btn">表头</el-button></template>
|
<template #reference><el-button :icon="Setting" class="action-btn">表头</el-button></template>
|
||||||
<el-checkbox-group v-model="visibleColumnProps" class="column-selector">
|
<el-checkbox-group v-model="visibleColumnProps" class="column-selector">
|
||||||
<el-row :gutter="10">
|
<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-row>
|
||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
@ -137,7 +137,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</template>
|
</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 }">
|
<template #default="{ row }">
|
||||||
<el-button link type="warning" size="default" @click="handlePrint(row)">
|
<el-button link type="warning" size="default" @click="handlePrint(row)">
|
||||||
<el-icon><Printer/></el-icon>
|
<el-icon><Printer/></el-icon>
|
||||||
@ -432,6 +432,7 @@ import {
|
|||||||
import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
||||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
||||||
import { getLabelPreview, executePrint } from '@/api/common/print'
|
import { getLabelPreview, executePrint } from '@/api/common/print'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// v-loadmore
|
// v-loadmore
|
||||||
@ -455,6 +456,7 @@ const vLoadmore = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
@ -523,6 +525,69 @@ const allColumns = [
|
|||||||
{ prop: 'detail_link', label: '详情', minWidth: '100' }
|
{ 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 defaultVisibleCols = ['company_name', 'material_name', 'sku', 'serial_number', 'qty_stock', 'status', 'quality_status', 'product_photo', 'sale_price', 'order_id']
|
||||||
const visibleColumnProps = ref(defaultVisibleCols)
|
const visibleColumnProps = ref(defaultVisibleCols)
|
||||||
|
|
||||||
@ -834,6 +899,8 @@ const getStatusType = (s:string) => ({'在库':'success','出库':'info','借库
|
|||||||
const getQualityType = (s:string) => ({'合格':'success','不合格':'danger','待检':'info'}[s]||'info')
|
const getQualityType = (s:string) => ({'合格':'success','不合格':'danger','待检':'info'}[s]||'info')
|
||||||
const formatMoney = (val:any) => isNaN(Number(val)) ? '-' : `¥ ${Number(val).toFixed(2)}`
|
const formatMoney = (val:any) => isNaN(Number(val)) ? '-' : `¥ ${Number(val).toFixed(2)}`
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// 先根据权限初始化列显示状态
|
||||||
|
initColumnPermissions()
|
||||||
fetchData()
|
fetchData()
|
||||||
fetchOptions()
|
fetchOptions()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -69,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="right-tools">
|
<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-button :icon="Refresh" circle @click="fetchData" class="circle-btn" />
|
||||||
|
|
||||||
<el-popover placement="bottom-end" title="列配置" :width="500" trigger="click">
|
<el-popover placement="bottom-end" title="列配置" :width="500" trigger="click">
|
||||||
@ -80,13 +80,13 @@
|
|||||||
<div class="col-group-title">基础信息</div>
|
<div class="col-group-title">基础信息</div>
|
||||||
<el-row :gutter="10">
|
<el-row :gutter="10">
|
||||||
<el-col :span="12" v-for="c in baseColumns" :key="c.prop">
|
<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-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<div class="col-group-title" style="margin-top:10px">生产与库存</div>
|
<div class="col-group-title" style="margin-top:10px">生产与库存</div>
|
||||||
<el-row :gutter="10">
|
<el-row :gutter="10">
|
||||||
<el-col :span="12" v-for="c in stockColumns" :key="c.prop">
|
<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-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
@ -189,7 +189,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</template>
|
</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 }">
|
<template #default="{ row }">
|
||||||
<el-button link type="warning" size="default" @click="handlePrint(row)">
|
<el-button link type="warning" size="default" @click="handlePrint(row)">
|
||||||
<el-icon><Printer/></el-icon> 打印
|
<el-icon><Printer/></el-icon> 打印
|
||||||
@ -533,6 +533,7 @@ import {
|
|||||||
import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
||||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
||||||
import {getLabelPreview, executePrint} from '@/api/common/print'
|
import {getLabelPreview, executePrint} from '@/api/common/print'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// 自定义指令:v-loadmore (适配 Teleport 到 Body 的下拉框)
|
// 自定义指令:v-loadmore (适配 Teleport 到 Body 的下拉框)
|
||||||
@ -559,6 +560,7 @@ const vLoadmore = {
|
|||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
// 状态与变量
|
// 状态与变量
|
||||||
// ------------------------------------
|
// ------------------------------------
|
||||||
|
const userStore = useUserStore()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
@ -640,6 +642,78 @@ const stockColumns = [
|
|||||||
]
|
]
|
||||||
const allColumns = [...baseColumns, ...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 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)
|
const visibleColumnProps = ref(defaultColumns)
|
||||||
|
|
||||||
@ -985,6 +1059,8 @@ const getQualityType = (status: string) => { const map: any = { '合格': 'succe
|
|||||||
const formatMoney = (val: any) => { const num = Number(val); return isNaN(num) ? '-' : `¥ ${num.toFixed(2)}` }
|
const formatMoney = (val: any) => { const num = Number(val); return isNaN(num) ? '-' : `¥ ${num.toFixed(2)}` }
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// 先根据权限初始化列显示状态
|
||||||
|
initColumnPermissions()
|
||||||
fetchData()
|
fetchData()
|
||||||
fetchOptions()
|
fetchOptions()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -45,21 +45,21 @@
|
|||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||||
<el-button @click="resetSearch">重置</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-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table :data="tableData" border stripe style="width: 100%;" v-loading="loading">
|
<el-table :data="tableData" border stripe style="width: 100%;" v-loading="loading">
|
||||||
<el-table-column prop="sku" label="SKU" width="200" />
|
<el-table-column v-if="hasColumnPermission('sku')" prop="sku" label="SKU" width="200" />
|
||||||
<el-table-column prop="material_name" label="物料名称" />
|
<el-table-column v-if="hasColumnPermission('material_name')" prop="material_name" label="物料名称" />
|
||||||
<el-table-column prop="provider_name" label="服务商" width="150" />
|
<el-table-column v-if="hasColumnPermission('provider_name')" prop="provider_name" label="服务商" width="150" />
|
||||||
<el-table-column prop="sale_price" label="售价" width="120">
|
<el-table-column v-if="hasColumnPermission('sale_price')" prop="sale_price" label="售价" width="120">
|
||||||
<template #default="{row}">¥{{ row.sale_price.toFixed(2) }}</template>
|
<template #default="{row}">¥{{ row.sale_price.toFixed(2) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="description" label="简介" show-overflow-tooltip />
|
<el-table-column v-if="hasColumnPermission('description')" prop="description" label="简介" show-overflow-tooltip />
|
||||||
<el-table-column prop="created_at" label="创建时间" width="160" />
|
<el-table-column v-if="hasColumnPermission('created_at')" prop="created_at" label="创建时间" width="160" />
|
||||||
<el-table-column label="操作" width="180" fixed="right">
|
<el-table-column v-if="userStore.hasPermission('inbound_service:operation')" label="操作" width="180" fixed="right">
|
||||||
<template #default="{row}">
|
<template #default="{row}">
|
||||||
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
|
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
|
||||||
<el-button size="small" type="danger" @click="handleDelete(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 { InfoFilled, Box, House } from '@element-plus/icons-vue'
|
||||||
import type { FormInstance, FormRules } from 'element-plus'
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
import {
|
import {
|
||||||
getServiceList,
|
getServiceList,
|
||||||
createService,
|
createService,
|
||||||
@ -212,6 +213,27 @@ import {
|
|||||||
type MaterialBaseItem
|
type MaterialBaseItem
|
||||||
} from '@/api/inbound/service'
|
} 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 tableData = ref<ServiceItem[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|||||||
@ -10,12 +10,12 @@
|
|||||||
<p class="subtitle">单件自动确认,多件弹窗录入</p>
|
<p class="subtitle">单件自动确认,多件弹窗录入</p>
|
||||||
|
|
||||||
<div class="idle-actions">
|
<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>
|
||||||
|
|
||||||
<el-button
|
<el-button
|
||||||
v-if="serverDraftCount > 0"
|
v-if="serverDraftCount > 0 && userStore.hasPermission('inventory_stocktake:operation')"
|
||||||
type="warning"
|
type="warning"
|
||||||
plain
|
plain
|
||||||
size="large"
|
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-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>
|
<el-tag v-else type="danger" size="small" effect="dark" round>同步失败</el-tag>
|
||||||
</div>
|
</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>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@ -84,7 +84,7 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<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-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
@ -139,7 +139,7 @@
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<el-button @click="showQtyDialog = false">取消</el-button>
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@ -190,6 +190,7 @@
|
|||||||
<el-table-column label="操作" width="90" align="center" fixed="right">
|
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
<el-button
|
||||||
|
v-if="userStore.hasPermission('inventory_stocktake:operation')"
|
||||||
type="primary"
|
type="primary"
|
||||||
link
|
link
|
||||||
icon="Edit"
|
icon="Edit"
|
||||||
@ -237,7 +238,7 @@
|
|||||||
<el-button @click="showFinishDialog = false">返回修改</el-button>
|
<el-button @click="showFinishDialog = false">返回修改</el-button>
|
||||||
<div class="footer-right">
|
<div class="footer-right">
|
||||||
<el-button type="success" @click="exportToExcel" :icon="Download">导出Excel</el-button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
627
inventory-web/src/views/system/PermissionConfig.vue
Normal file
627
inventory-web/src/views/system/PermissionConfig.vue
Normal 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>
|
||||||
@ -13,10 +13,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="scan-section">
|
<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>
|
<el-icon :size="40" color="#409EFF"><CameraFilled /></el-icon>
|
||||||
<span class="text">点击开启全屏扫码</span>
|
<span class="text">点击开启全屏扫码</span>
|
||||||
</div>
|
</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">
|
<div class="input-box">
|
||||||
<el-input
|
<el-input
|
||||||
@ -26,12 +30,13 @@
|
|||||||
clearable
|
clearable
|
||||||
ref="barcodeRef"
|
ref="barcodeRef"
|
||||||
size="large"
|
size="large"
|
||||||
|
:disabled="!userStore.hasPermission('op_borrow:operation')"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<el-icon><Scissor /></el-icon>
|
<el-icon><Scissor /></el-icon>
|
||||||
</template>
|
</template>
|
||||||
<template #append>
|
<template #append>
|
||||||
<el-button @click="handleManualInput">添加</el-button>
|
<el-button @click="handleManualInput" :disabled="!userStore.hasPermission('op_borrow:operation')">添加</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</div>
|
</div>
|
||||||
@ -40,16 +45,16 @@
|
|||||||
<div class="cart-section">
|
<div class="cart-section">
|
||||||
<div v-if="cartItems.length > 0">
|
<div v-if="cartItems.length > 0">
|
||||||
<el-table :data="cartItems" border stripe style="width: 100%">
|
<el-table :data="cartItems" border stripe style="width: 100%">
|
||||||
<el-table-column prop="name" label="物品名称" min-width="120" show-overflow-tooltip />
|
<el-table-column v-if="hasColumnPermission('name')" 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('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}">
|
<template #default="{row}">
|
||||||
<el-tag type="info">{{ parseFloat(row.available_quantity) }}</el-tag>
|
<el-tag type="info">{{ parseFloat(row.available_quantity) }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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}">
|
<template #default="{row}">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="row.out_quantity"
|
v-model="row.out_quantity"
|
||||||
@ -57,11 +62,12 @@
|
|||||||
:max="parseFloat(row.available_quantity)"
|
:max="parseFloat(row.available_quantity)"
|
||||||
size="small"
|
size="small"
|
||||||
style="width: 100px"
|
style="width: 100px"
|
||||||
|
:disabled="!userStore.hasPermission('op_borrow:operation')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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}">
|
<template #default="{$index}">
|
||||||
<el-button type="danger" icon="Delete" circle size="small" @click="removeFromCart($index)" />
|
<el-button type="danger" icon="Delete" circle size="small" @click="removeFromCart($index)" />
|
||||||
</template>
|
</template>
|
||||||
@ -102,7 +108,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="领用人签名确认" required>
|
<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">
|
<div v-if="signaturePreviewUrl" class="signed-img">
|
||||||
<img :src="signaturePreviewUrl" alt="签名" />
|
<img :src="signaturePreviewUrl" alt="签名" />
|
||||||
<span class="re-sign-tip">点击重签</span>
|
<span class="re-sign-tip">点击重签</span>
|
||||||
@ -112,11 +118,17 @@
|
|||||||
<span>点击此处进行全屏签名</span>
|
<span>点击此处进行全屏签名</span>
|
||||||
</div>
|
</div>
|
||||||
</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-item>
|
||||||
|
|
||||||
<div class="bottom-actions">
|
<div class="bottom-actions">
|
||||||
<el-button @click="clearAll" icon="Refresh">清空</el-button>
|
<el-button v-if="userStore.hasPermission('op_borrow:operation')" @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')" type="primary" size="large" :loading="loading" @click="submitForm" icon="Select">
|
||||||
确认借出
|
确认借出
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@ -187,6 +199,27 @@ import QrScanner from '@/components/QrScanner/index.vue'
|
|||||||
import { getStockByBarcode } from '@/api/outbound'
|
import { getStockByBarcode } from '@/api/outbound'
|
||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
import { uploadFile } from '@/api/common/upload'
|
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('')
|
const barcodeInput = ref('')
|
||||||
|
|||||||
@ -19,12 +19,12 @@
|
|||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:row-class-name="tableRowClassName"
|
:row-class-name="tableRowClassName"
|
||||||
>
|
>
|
||||||
<el-table-column prop="borrow_no" label="单号" width="180" show-overflow-tooltip />
|
<el-table-column v-if="hasColumnPermission('borrow_no')" prop="borrow_no" label="单号" width="180" show-overflow-tooltip />
|
||||||
<el-table-column prop="borrower_name" label="借用人" width="100" />
|
<el-table-column v-if="hasColumnPermission('borrower_name')" prop="borrower_name" label="借用人" width="100" />
|
||||||
<el-table-column prop="sku" label="SKU" width="120" show-overflow-tooltip />
|
<el-table-column v-if="hasColumnPermission('sku')" 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_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}">
|
<template #default="{row}">
|
||||||
<div v-if="row.status === 'returned'">
|
<div v-if="row.status === 'returned'">
|
||||||
<el-tag type="success" size="small">实际</el-tag>
|
<el-tag type="success" size="small">实际</el-tag>
|
||||||
@ -40,7 +40,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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}">
|
<template #default="{row}">
|
||||||
<el-tag :type="row.status==='returned'?'success':'warning'">
|
<el-tag :type="row.status==='returned'?'success':'warning'">
|
||||||
{{ row.status==='returned'?'已还':'借出中' }}
|
{{ row.status==='returned'?'已还':'借出中' }}
|
||||||
@ -48,22 +48,22 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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}">
|
<template #default="{row}">
|
||||||
<span v-if="row.return_location">{{ row.return_location }}</span>
|
<span v-if="row.return_location">{{ row.return_location }}</span>
|
||||||
<span v-else style="color:#ccc">-</span>
|
<span v-else style="color:#ccc">-</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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}">
|
<template #default="{row}">
|
||||||
<div style="display:flex; justify-content: center; gap:10px">
|
<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>
|
<template #reference><el-tag size="small">借</el-tag></template>
|
||||||
<img :src="row.borrow_signature" style="width:200px; border:1px solid #eee" />
|
<img :src="row.borrow_signature" style="width:200px; border:1px solid #eee" />
|
||||||
</el-popover>
|
</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>
|
<template #reference><el-tag type="success" size="small">还</el-tag></template>
|
||||||
<img :src="row.return_signature" style="width:200px; border:1px solid #eee" />
|
<img :src="row.return_signature" style="width:200px; border:1px solid #eee" />
|
||||||
</el-popover>
|
</el-popover>
|
||||||
@ -88,6 +88,32 @@ import request from '@/utils/request'
|
|||||||
import dayjs from 'dayjs' // 建议使用 dayjs 处理日期,如果没有安装,可以用原生 Date
|
import dayjs from 'dayjs' // 建议使用 dayjs 处理日期,如果没有安装,可以用原生 Date
|
||||||
import 'dayjs/locale/zh-cn' // 导入中文包
|
import 'dayjs/locale/zh-cn' // 导入中文包
|
||||||
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 list = ref<any[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
|
|||||||
@ -13,10 +13,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="scan-section">
|
<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>
|
<el-icon :size="40" color="#409EFF"><CameraFilled /></el-icon>
|
||||||
<span class="text">点击开启全屏扫码</span>
|
<span class="text">点击开启全屏扫码</span>
|
||||||
</div>
|
</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">
|
<div class="input-box">
|
||||||
<el-input
|
<el-input
|
||||||
@ -26,12 +30,13 @@
|
|||||||
clearable
|
clearable
|
||||||
ref="barcodeRef"
|
ref="barcodeRef"
|
||||||
size="large"
|
size="large"
|
||||||
|
:disabled="!userStore.hasPermission('op_return:operation')"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<el-icon><Scissor /></el-icon>
|
<el-icon><Scissor /></el-icon>
|
||||||
</template>
|
</template>
|
||||||
<template #append>
|
<template #append>
|
||||||
<el-button @click="scanItem">识别</el-button>
|
<el-button @click="scanItem" :disabled="!userStore.hasPermission('op_return:operation')">识别</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</div>
|
</div>
|
||||||
@ -40,16 +45,17 @@
|
|||||||
<div class="cart-section">
|
<div class="cart-section">
|
||||||
<div v-if="returnList.length > 0">
|
<div v-if="returnList.length > 0">
|
||||||
<el-table :data="returnList" border stripe style="width: 100%">
|
<el-table :data="returnList" border stripe style="width: 100%">
|
||||||
<el-table-column prop="borrower_name" label="借用人" width="90" show-overflow-tooltip />
|
<el-table-column v-if="hasColumnPermission('borrower_name')" 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('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}">
|
<template #default="{row}">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="row.return_location"
|
v-model="row.return_location"
|
||||||
:placeholder="`原: ${row.current_location || '无'}`"
|
:placeholder="`原: ${row.current_location || '无'}`"
|
||||||
clearable
|
clearable
|
||||||
size="small"
|
size="small"
|
||||||
|
:disabled="!userStore.hasPermission('op_return:operation')"
|
||||||
>
|
>
|
||||||
<template #append v-if="row.return_location !== row.current_location">
|
<template #append v-if="row.return_location !== row.current_location">
|
||||||
<span style="color: #E6A23C; font-size: 12px;">变更</span>
|
<span style="color: #E6A23C; font-size: 12px;">变更</span>
|
||||||
@ -58,7 +64,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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}">
|
<template #default="{$index}">
|
||||||
<el-button type="danger" icon="Delete" circle size="small" @click="returnList.splice($index, 1)" />
|
<el-button type="danger" icon="Delete" circle size="small" @click="returnList.splice($index, 1)" />
|
||||||
</template>
|
</template>
|
||||||
@ -77,7 +83,7 @@
|
|||||||
|
|
||||||
<el-form label-position="top">
|
<el-form label-position="top">
|
||||||
<el-form-item required>
|
<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">
|
<div v-if="signaturePreviewUrl" class="signed-img">
|
||||||
<img :src="signaturePreviewUrl" alt="签名" />
|
<img :src="signaturePreviewUrl" alt="签名" />
|
||||||
<span class="re-sign-tip">点击重签</span>
|
<span class="re-sign-tip">点击重签</span>
|
||||||
@ -87,12 +93,18 @@
|
|||||||
<span>点击此处进行库管签名</span>
|
<span>点击此处进行库管签名</span>
|
||||||
</div>
|
</div>
|
||||||
</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-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<div class="bottom-actions">
|
<div class="bottom-actions">
|
||||||
<el-button @click="clearAll" icon="Refresh">清空</el-button>
|
<el-button v-if="userStore.hasPermission('op_return:operation')" @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')" type="success" size="large" :loading="loading" @click="preSubmitCheck" icon="Select">
|
||||||
确认归还
|
确认归还
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@ -161,6 +173,25 @@ import { uploadFile } from '@/api/common/upload'
|
|||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Scissor, EditPen, Delete, CameraFilled, Close, Refresh, Select } from '@element-plus/icons-vue'
|
import { Scissor, EditPen, Delete, CameraFilled, Close, Refresh, Select } from '@element-plus/icons-vue'
|
||||||
import QrScanner from '@/components/QrScanner/index.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('')
|
const barcode = ref('')
|
||||||
|
|||||||
Reference in New Issue
Block a user