Compare commits
88 Commits
1.0入库操作
...
16350842f8
| Author | SHA1 | Date | |
|---|---|---|---|
| 16350842f8 | |||
| d7dff943fc | |||
| 2f140e112f | |||
| 8264867b1c | |||
| d993e6796e | |||
| 4e05734865 | |||
| 7f19867139 | |||
| bcd39729f8 | |||
| 9cfbdc7d13 | |||
| d3510b0261 | |||
| 7b0082c6e0 | |||
| b08196c479 | |||
| 68ea351c99 | |||
| f001be9eef | |||
| 545cd86632 | |||
| b688480892 | |||
| 646804bb98 | |||
| 3daf7e4500 | |||
| e61c179d77 | |||
| f7cfb5a346 | |||
| 29fd397e4f | |||
| 54d83803c4 | |||
| 05fbb4e3b3 | |||
| fb56359f41 | |||
| 00ebffb9fd | |||
| 4b29912f6f | |||
| cc33108e88 | |||
| d78ef22251 | |||
| c3e2494b3e | |||
| fed85e51c5 | |||
| d2082c712b | |||
| b85f28fc72 | |||
| 8f6d0cd40b | |||
| 281a41c549 | |||
| dda54e829b | |||
| 5beb373677 | |||
| c1e4acc1d8 | |||
| a0993767fe | |||
| ad8bb5a75d | |||
| c414efc7a4 | |||
| 09a2af0b55 | |||
| 89620b2445 | |||
| a1df62238e | |||
| 3a056335bb | |||
| fbff519ac9 | |||
| 657c916703 | |||
| 3c1c822f88 | |||
| 4324e5a688 | |||
| 1fe00a8ba3 | |||
| afcf90a859 | |||
| 5bc3dab31c | |||
| 079987e7f3 | |||
| 00c45c72fb | |||
| 6fa5233ea6 | |||
| 3f83e8742b | |||
| 348e4dd024 | |||
| 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 - 借还/维修/报废)
|
||||
# ★★★ 关键修改:将前缀改为 /api/v1/transactions 以匹配前端请求 ★★★
|
||||
# -----------------------------------------------------
|
||||
try:
|
||||
from app.api.v1.transactions import trans_bp
|
||||
@ -90,8 +89,7 @@ def create_app():
|
||||
app.register_blueprint(trans_bp, url_prefix='/api/transactions', name='trans_legacy')
|
||||
print("✅ Transactions 模块注册成功")
|
||||
except ImportError as e:
|
||||
# 允许模块不存在时不崩溃,但在开发借还功能时这里报错说明 trans_bp 定义有问题
|
||||
print(f"⚠️ 提示: Transaction 模块导入失败 (请检查 app/api/v1/transactions.py): {e}")
|
||||
print(f"⚠️ 提示: Transaction 模块导入失败: {e}")
|
||||
|
||||
# -----------------------------------------------------
|
||||
# 2.5 注册出库模块 (Outbound)
|
||||
@ -119,6 +117,19 @@ def create_app():
|
||||
except ImportError as e:
|
||||
print(f"❌ 错误: BOM 模块导入失败: {e}")
|
||||
|
||||
# -----------------------------------------------------
|
||||
# 2.7 注册权限管理模块 (Permission) - [新增]
|
||||
# -----------------------------------------------------
|
||||
try:
|
||||
from app.api.v1.permission import permission_bp
|
||||
# 标准: /api/v1/permissions/tree
|
||||
app.register_blueprint(permission_bp, url_prefix='/api/v1/permissions')
|
||||
# 兼容: /api/permissions/tree
|
||||
app.register_blueprint(permission_bp, url_prefix='/api/permissions', name='permission_legacy')
|
||||
print("✅ Permission 模块注册成功")
|
||||
except ImportError as e:
|
||||
print(f"❌ 错误: Permission 模块导入失败 (请检查 app/api/v1/permission.py 是否存在): {e}")
|
||||
|
||||
# =========================================================
|
||||
# 3. 预加载数据模型
|
||||
# =========================================================
|
||||
@ -133,8 +144,8 @@ def create_app():
|
||||
# 出库模型
|
||||
from app.models.outbound import TransOutbound
|
||||
|
||||
# 系统与业务模型
|
||||
from app.models.system import SysUser, SysLog
|
||||
# 系统与业务模型 (SysRolePermission 等在 models.system 中)
|
||||
from app.models.system import SysUser, SysLog, SysMenu, SysElement, SysRolePermission
|
||||
# 确保借还模型被加载
|
||||
from app.models.transaction import TransBorrow, TransRepair, TransScrap
|
||||
|
||||
@ -146,4 +157,4 @@ def create_app():
|
||||
except Exception as e:
|
||||
print(f"⚠️ 模型预加载发生未知错误: {e}")
|
||||
|
||||
return app
|
||||
return app
|
||||
@ -2,10 +2,56 @@
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
from flask_jwt_extended import jwt_required, get_jwt
|
||||
from app.services.auth_service import AuthService
|
||||
from app.utils.decorators import permission_required
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 辅助函数:获取当前用户的完整权限列表(基于角色查询)
|
||||
# ==============================================================================
|
||||
def get_current_user_permissions():
|
||||
"""
|
||||
返回当前用户拥有的所有权限码列表(包括菜单和元素)
|
||||
此函数根据角色查询数据库得到权限。
|
||||
"""
|
||||
claims = get_jwt()
|
||||
user_role = claims.get('role')
|
||||
if not user_role:
|
||||
return []
|
||||
# 超级管理员返回所有字段权限 (忽略大小写)
|
||||
if user_role.upper() == 'SUPER_ADMIN':
|
||||
return ['system_user:*']
|
||||
perm_dict = AuthService.get_user_permissions(user_role)
|
||||
# 合并菜单和元素权限
|
||||
perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
|
||||
return perms
|
||||
|
||||
|
||||
def filter_item_by_permissions(item_dict, user_permissions):
|
||||
"""
|
||||
根据用户权限过滤 item 字典,无权限的字段值置为 None
|
||||
"""
|
||||
# 字段名到权限码的映射(与前端 permissionMap 保持一致)
|
||||
field_to_perm = {
|
||||
'id': 'system_user:id',
|
||||
'username': 'system_user:username',
|
||||
'account_id': 'system_user:account_id',
|
||||
'email': 'system_user:email',
|
||||
'department': 'system_user:department',
|
||||
'role': 'system_user:role',
|
||||
'status': 'system_user:status',
|
||||
'created_at': 'system_user:created_at',
|
||||
}
|
||||
# 如果用户是超级管理员且有 'system_user:*',则不过滤
|
||||
if 'system_user:*' in user_permissions:
|
||||
return item_dict
|
||||
for field, perm_code in field_to_perm.items():
|
||||
if field in item_dict and perm_code not in user_permissions:
|
||||
item_dict[field] = None
|
||||
return item_dict
|
||||
|
||||
|
||||
@auth_bp.route('/login', methods=['POST'])
|
||||
def login():
|
||||
try:
|
||||
@ -34,9 +80,36 @@ def login():
|
||||
|
||||
@auth_bp.route('/user/create', methods=['POST'])
|
||||
@jwt_required()
|
||||
@permission_required('system_user:operation')
|
||||
def create_user():
|
||||
try:
|
||||
data = request.get_json()
|
||||
# 数据清洗:移除用户没有权限的字段
|
||||
user_permissions = get_current_user_permissions()
|
||||
# 超级管理员不过滤
|
||||
if 'system_user:*' not in user_permissions:
|
||||
# 字段名到权限码的映射
|
||||
field_to_perm = {
|
||||
'cn_name': 'system_user:username',
|
||||
'username': 'system_user:username',
|
||||
'password': 'system_user:password',
|
||||
'department': 'system_user:department',
|
||||
'role': 'system_user:role',
|
||||
'email': 'system_user:email',
|
||||
}
|
||||
# 对于 password 字段,如果没有对应权限但用户有操作权限,可以保留(由装饰器保证)
|
||||
# 但如果连操作权限都没有,则不会进入此接口。
|
||||
for field in list(data.keys()):
|
||||
perm_code = field_to_perm.get(field)
|
||||
# 密码字段特殊处理:如果没有 password 权限但用户有操作权限,仍允许(不删除)
|
||||
if field == 'password':
|
||||
# 检查用户是否有操作权限,如果有则保留
|
||||
if 'system_user:operation' not in user_permissions:
|
||||
data.pop(field, None)
|
||||
continue
|
||||
if perm_code and perm_code not in user_permissions:
|
||||
data.pop(field, None)
|
||||
|
||||
claims = get_jwt()
|
||||
operator_role = claims.get('role')
|
||||
|
||||
@ -51,9 +124,34 @@ def create_user():
|
||||
# [新增] 更新用户
|
||||
@auth_bp.route('/user/<int:user_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@permission_required('system_user:operation')
|
||||
def update_user(user_id):
|
||||
try:
|
||||
data = request.get_json()
|
||||
# 数据清洗:移除用户没有权限的字段
|
||||
user_permissions = get_current_user_permissions()
|
||||
# 超级管理员不过滤
|
||||
if 'system_user:*' not in user_permissions:
|
||||
# 字段名到权限码的映射
|
||||
field_to_perm = {
|
||||
'cn_name': 'system_user:username',
|
||||
'username': 'system_user:username',
|
||||
'password': 'system_user:password',
|
||||
'department': 'system_user:department',
|
||||
'role': 'system_user:role',
|
||||
'email': 'system_user:email',
|
||||
}
|
||||
for field in list(data.keys()):
|
||||
perm_code = field_to_perm.get(field)
|
||||
# 密码字段特殊处理:如果没有 password 权限但用户有操作权限,仍允许(不删除)
|
||||
if field == 'password':
|
||||
# 检查用户是否有操作权限,如果有则保留
|
||||
if 'system_user:operation' not in user_permissions:
|
||||
data.pop(field, None)
|
||||
continue
|
||||
if perm_code and perm_code not in user_permissions:
|
||||
data.pop(field, None)
|
||||
|
||||
claims = get_jwt()
|
||||
operator_role = claims.get('role')
|
||||
|
||||
@ -67,10 +165,14 @@ def update_user(user_id):
|
||||
|
||||
@auth_bp.route('/users', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('system_user')
|
||||
def get_users():
|
||||
try:
|
||||
users = AuthService.get_all_users()
|
||||
return jsonify({'msg': '获取成功', 'data': users}), 200
|
||||
# 字段级脱敏
|
||||
user_permissions = get_current_user_permissions()
|
||||
filtered_users = [filter_item_by_permissions(user, user_permissions) for user in users]
|
||||
return jsonify({'msg': '获取成功', 'data': filtered_users}), 200
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Get Users Failed: {str(e)}")
|
||||
return jsonify({'msg': '获取用户列表失败'}), 500
|
||||
@ -78,6 +180,7 @@ def get_users():
|
||||
|
||||
@auth_bp.route('/user/<int:user_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@permission_required('system_user:operation')
|
||||
def delete_user(user_id):
|
||||
try:
|
||||
claims = get_jwt()
|
||||
@ -87,4 +190,21 @@ def delete_user(user_id):
|
||||
return jsonify({'msg': '删除成功'}), 200
|
||||
except Exception as 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
|
||||
|
||||
@ -3,15 +3,61 @@ from app.services.bom_service import BomService
|
||||
from app.models.base import MaterialBase
|
||||
from app.models.bom import BomTable
|
||||
from app.extensions import db
|
||||
from flask_jwt_extended import jwt_required
|
||||
from flask_jwt_extended import jwt_required, get_jwt
|
||||
from app.utils.decorators import permission_required
|
||||
from app.services.auth_service import AuthService
|
||||
|
||||
bom_bp = Blueprint('bom', __name__)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 辅助函数:获取当前用户的完整权限列表(基于角色查询)
|
||||
# ==============================================================================
|
||||
def get_current_user_permissions():
|
||||
"""
|
||||
返回当前用户拥有的所有权限码列表(包括菜单和元素)
|
||||
此函数根据角色查询数据库得到权限。
|
||||
"""
|
||||
claims = get_jwt()
|
||||
user_role = claims.get('role')
|
||||
if not user_role:
|
||||
return []
|
||||
# 超级管理员返回所有字段权限 (忽略大小写)
|
||||
if user_role.upper() == 'SUPER_ADMIN':
|
||||
return ['bom_manage:*']
|
||||
perm_dict = AuthService.get_user_permissions(user_role)
|
||||
# 合并菜单和元素权限
|
||||
perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
|
||||
return perms
|
||||
|
||||
|
||||
def filter_item_by_permissions(item_dict, user_permissions):
|
||||
"""
|
||||
根据用户权限过滤 item 字典,无权限的字段值置为 None
|
||||
"""
|
||||
# 字段名到权限码的映射(与前端 permissionMap 保持一致)
|
||||
field_to_perm = {
|
||||
'bom_no': 'bom_manage:bom_no',
|
||||
'parent_name': 'bom_manage:parent_name',
|
||||
'parent_spec': 'bom_manage:parent_spec',
|
||||
'version': 'bom_manage:version',
|
||||
'is_enabled': 'bom_manage:status',
|
||||
'child_count': 'bom_manage:child_count',
|
||||
}
|
||||
# 如果用户是超级管理员且有 'bom_manage:*',则不过滤
|
||||
if 'bom_manage:*' in user_permissions:
|
||||
return item_dict
|
||||
for field, perm_code in field_to_perm.items():
|
||||
if field in item_dict and perm_code not in user_permissions:
|
||||
item_dict[field] = None
|
||||
return item_dict
|
||||
|
||||
|
||||
# ==================== 新版 BOM 接口(基于 bom_no) ====================
|
||||
|
||||
@bom_bp.route('/list', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('bom_manage')
|
||||
def get_bom_list():
|
||||
"""获取所有 BOM 配方列表,支持 keyword 搜索和 active_only 过滤"""
|
||||
try:
|
||||
@ -20,6 +66,10 @@ def get_bom_list():
|
||||
active_only = request.args.get('active_only', 'false').lower() == 'true'
|
||||
|
||||
data = BomService.get_bom_list(keyword=keyword, active_only=active_only)
|
||||
# 字段级脱敏
|
||||
user_permissions = get_current_user_permissions()
|
||||
if isinstance(data, list):
|
||||
data = [filter_item_by_permissions(item, user_permissions) for item in data]
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': 'success',
|
||||
@ -30,8 +80,9 @@ def get_bom_list():
|
||||
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
|
||||
|
||||
|
||||
@bom_bp.route('/detail/<bom_no>', methods=['GET'])
|
||||
@bom_bp.route('/detail/<path:bom_no>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('bom_manage')
|
||||
def get_bom_detail(bom_no):
|
||||
"""
|
||||
根据 BOM 编号获取配方详情
|
||||
@ -42,6 +93,9 @@ def get_bom_detail(bom_no):
|
||||
data = BomService.get_bom_detail(bom_no, version=version)
|
||||
if not data:
|
||||
return jsonify({'code': 404, 'msg': 'BOM 不存在'}), 404
|
||||
# 字段级脱敏
|
||||
user_permissions = get_current_user_permissions()
|
||||
data = filter_item_by_permissions(data, user_permissions)
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': 'success',
|
||||
@ -54,10 +108,41 @@ def get_bom_detail(bom_no):
|
||||
|
||||
@bom_bp.route('/save', methods=['POST'])
|
||||
@jwt_required()
|
||||
@permission_required('bom_manage:operation')
|
||||
def save_bom():
|
||||
"""保存或更新 BOM 配方(支持自定义 bom_no 和 多版本)"""
|
||||
try:
|
||||
req_data = request.get_json()
|
||||
# 数据清洗:移除用户没有权限的字段
|
||||
user_permissions = get_current_user_permissions()
|
||||
# 超级管理员不过滤
|
||||
if 'bom_manage:*' not in user_permissions:
|
||||
# 字段名到权限码的映射
|
||||
field_to_perm = {
|
||||
'parent_id': 'bom_manage:parent_id',
|
||||
'version': 'bom_manage:version',
|
||||
'is_enabled': 'bom_manage:status',
|
||||
'bom_no': 'bom_manage:bom_no',
|
||||
}
|
||||
# 清洗顶级字段
|
||||
for field in list(req_data.keys()):
|
||||
perm_code = field_to_perm.get(field)
|
||||
if perm_code and perm_code not in user_permissions:
|
||||
req_data.pop(field, None)
|
||||
# 清洗 children 中的字段
|
||||
if 'children' in req_data and isinstance(req_data['children'], list):
|
||||
for child in req_data['children']:
|
||||
# 子件字段映射
|
||||
child_field_to_perm = {
|
||||
'child_id': 'bom_manage:child_id',
|
||||
'dosage': 'bom_manage:dosage',
|
||||
'remark': 'bom_manage:remark',
|
||||
}
|
||||
for field in list(child.keys()):
|
||||
perm_code = child_field_to_perm.get(field)
|
||||
if perm_code and perm_code not in user_permissions:
|
||||
child.pop(field, None)
|
||||
|
||||
# 必需字段校验
|
||||
if 'parent_id' not in req_data or 'children' not in req_data:
|
||||
return jsonify({'code': 400, 'msg': '缺少 parent_id 或 children 字段'}), 400
|
||||
@ -79,14 +164,18 @@ def save_bom():
|
||||
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
|
||||
|
||||
|
||||
@bom_bp.route('/stock/<bom_no>', methods=['GET'])
|
||||
@bom_bp.route('/stock/<path:bom_no>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('bom_manage')
|
||||
def get_bom_with_stock_by_no(bom_no):
|
||||
"""根据 BOM 编号获取配方详情及库存信息"""
|
||||
try:
|
||||
data = BomService.get_bom_with_stock_by_bom_no(bom_no)
|
||||
if not data:
|
||||
return jsonify({'code': 404, 'msg': 'BOM 不存在'}), 404
|
||||
# 字段级脱敏
|
||||
user_permissions = get_current_user_permissions()
|
||||
data = filter_item_by_permissions(data, user_permissions)
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': 'success',
|
||||
@ -99,8 +188,9 @@ def get_bom_with_stock_by_no(bom_no):
|
||||
|
||||
# ==================== 删除BOM接口 ====================
|
||||
|
||||
@bom_bp.route('/<bom_no>', methods=['DELETE'])
|
||||
@bom_bp.route('/<path:bom_no>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@permission_required('bom_manage:operation')
|
||||
def delete_bom(bom_no):
|
||||
"""
|
||||
根据 BOM 编号删除
|
||||
@ -133,9 +223,13 @@ def delete_bom(bom_no):
|
||||
|
||||
@bom_bp.route('/<int:parent_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('bom_manage')
|
||||
def get_bom(parent_id):
|
||||
try:
|
||||
data = BomService.get_bom_with_stock(parent_id)
|
||||
# 字段级脱敏
|
||||
user_permissions = get_current_user_permissions()
|
||||
data = filter_item_by_permissions(data, user_permissions)
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': 'success',
|
||||
@ -148,9 +242,40 @@ def get_bom(parent_id):
|
||||
|
||||
@bom_bp.route('', methods=['POST'])
|
||||
@jwt_required()
|
||||
@permission_required('bom_manage:operation')
|
||||
def save_bom_legacy():
|
||||
try:
|
||||
req_data = request.get_json()
|
||||
# 数据清洗:移除用户没有权限的字段
|
||||
user_permissions = get_current_user_permissions()
|
||||
# 超级管理员不过滤
|
||||
if 'bom_manage:*' not in user_permissions:
|
||||
# 字段名到权限码的映射
|
||||
field_to_perm = {
|
||||
'parent_id': 'bom_manage:parent_id',
|
||||
'version': 'bom_manage:version',
|
||||
'is_enabled': 'bom_manage:status',
|
||||
'bom_no': 'bom_manage:bom_no',
|
||||
}
|
||||
# 清洗顶级字段
|
||||
for field in list(req_data.keys()):
|
||||
perm_code = field_to_perm.get(field)
|
||||
if perm_code and perm_code not in user_permissions:
|
||||
req_data.pop(field, None)
|
||||
# 清洗 children 中的字段
|
||||
if 'children' in req_data and isinstance(req_data['children'], list):
|
||||
for child in req_data['children']:
|
||||
# 子件字段映射
|
||||
child_field_to_perm = {
|
||||
'child_id': 'bom_manage:child_id',
|
||||
'dosage': 'bom_manage:dosage',
|
||||
'remark': 'bom_manage:remark',
|
||||
}
|
||||
for field in list(child.keys()):
|
||||
perm_code = child_field_to_perm.get(field)
|
||||
if perm_code and perm_code not in user_permissions:
|
||||
child.pop(field, None)
|
||||
|
||||
parent_id = req_data.get('parent_id')
|
||||
child_list = req_data.get('children', [])
|
||||
if not parent_id or not isinstance(child_list, list):
|
||||
@ -169,11 +294,14 @@ def save_bom_legacy():
|
||||
|
||||
@bom_bp.route('/base/list', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('bom_manage')
|
||||
def get_material_base_list():
|
||||
"""获取所有基础物料列表,用于前端下拉框"""
|
||||
try:
|
||||
materials = MaterialBase.query.filter_by(is_enabled=True).order_by(MaterialBase.id.desc()).all()
|
||||
data = [item.to_dict() for item in materials]
|
||||
# 字段级脱敏 (如果需要,但此接口通常用于下拉选择,可能不需要脱敏)
|
||||
# 保持原样
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': 'success',
|
||||
@ -186,12 +314,16 @@ def get_material_base_list():
|
||||
|
||||
@bom_bp.route('/parents', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('bom_manage')
|
||||
def get_bom_parents():
|
||||
"""获取所有已定义BOM的父件物料列表(兼容旧版)"""
|
||||
try:
|
||||
subq = db.session.query(BomTable.parent_id).distinct().subquery()
|
||||
parents = MaterialBase.query.join(subq, MaterialBase.id == subq.c.parent_id).all()
|
||||
data = [item.to_dict() for item in parents]
|
||||
# 字段级脱敏 (如果需要)
|
||||
user_permissions = get_current_user_permissions()
|
||||
data = [filter_item_by_permissions(item, user_permissions) for item in data]
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': 'success',
|
||||
@ -199,4 +331,4 @@ def get_bom_parents():
|
||||
})
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'获取BOM父件列表失败: {str(e)}')
|
||||
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
|
||||
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
|
||||
|
||||
@ -1,22 +1,95 @@
|
||||
# 文件路径: app/api/v1/inbound/base.py
|
||||
|
||||
from flask import Blueprint, request, jsonify, send_file
|
||||
from flask import Blueprint, request, jsonify, send_file, g
|
||||
from app.services.inbound.base_service import MaterialBaseService
|
||||
from app.utils.decorators import login_required, permission_required
|
||||
import traceback
|
||||
import datetime
|
||||
|
||||
inbound_base_bp = Blueprint('stock_base', __name__)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 辅助函数:获取当前用户的完整权限列表(基于角色查询)
|
||||
# ==============================================================================
|
||||
def get_current_user_permissions():
|
||||
"""
|
||||
返回当前用户拥有的所有权限码列表(包括菜单和元素)
|
||||
此函数根据角色查询数据库得到权限。
|
||||
"""
|
||||
from flask_jwt_extended import get_jwt
|
||||
from app.services.auth_service import AuthService
|
||||
claims = get_jwt()
|
||||
user_role = claims.get('role')
|
||||
if not user_role:
|
||||
return []
|
||||
# 超级管理员返回所有字段权限 (忽略大小写)
|
||||
if user_role.upper() == 'SUPER_ADMIN':
|
||||
# 返回通配符权限(供列表脱敏使用)以及所有具体权限(供导出脱敏使用)
|
||||
return [
|
||||
'material_list:*',
|
||||
'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',
|
||||
'material_list:operation'
|
||||
]
|
||||
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
|
||||
"""
|
||||
# 如果用户拥有通配符权限,则不过滤
|
||||
if 'material_list:*' in user_permissions:
|
||||
return item_dict
|
||||
# 字段名到权限码的映射(与前端 permissionMap 保持一致)
|
||||
field_to_perm = {
|
||||
'id': 'material_list:id',
|
||||
'companyName': 'material_list:companyName',
|
||||
'name': 'material_list:name',
|
||||
'commonName': 'material_list:commonName',
|
||||
'category': 'material_list:category',
|
||||
'type': 'material_list:type',
|
||||
'spec': 'material_list:spec',
|
||||
'unit': 'material_list:unit',
|
||||
'inventoryCount': 'material_list:inventoryCount',
|
||||
'availableCount': 'material_list:availableCount',
|
||||
'generalManual': 'material_list:files',
|
||||
'generalImage': 'material_list:files',
|
||||
'isEnabled': 'material_list:isEnabled'
|
||||
}
|
||||
for field, perm_code in field_to_perm.items():
|
||||
if field in item_dict and perm_code not in user_permissions:
|
||||
item_dict[field] = None
|
||||
return item_dict
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 1. 搜索接口 (GET /api/v1/inbound/base/search)
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/search', methods=['GET'])
|
||||
@permission_required('material_list')
|
||||
def search_base():
|
||||
try:
|
||||
keyword = request.args.get('keyword', '')
|
||||
data = MaterialBaseService.search_material(keyword)
|
||||
return jsonify({"code": 200, "msg": "success", "data": data})
|
||||
# 字段级脱敏
|
||||
user_permissions = get_current_user_permissions()
|
||||
filtered_data = [filter_item_by_permissions(item, user_permissions) for item in data]
|
||||
return jsonify({"code": 200, "msg": "success", "data": filtered_data})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
@ -26,6 +99,7 @@ def search_base():
|
||||
# 2. 列表接口 (GET /api/v1/inbound/base/list)
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/list', methods=['GET'])
|
||||
@permission_required('material_list')
|
||||
def get_list():
|
||||
try:
|
||||
page = request.args.get('pageNum', 1, type=int)
|
||||
@ -37,10 +111,16 @@ def get_list():
|
||||
'company': request.args.get('company', ''),
|
||||
'category': request.args.get('category', ''),
|
||||
'type': request.args.get('type', ''),
|
||||
'isEnabled': request.args.get('isEnabled', None)
|
||||
'isEnabled': request.args.get('isEnabled', None),
|
||||
'orderByColumn': request.args.get('orderByColumn', ''),
|
||||
'isAsc': request.args.get('isAsc', None)
|
||||
}
|
||||
|
||||
result = MaterialBaseService.get_list(page, limit, filters)
|
||||
# 字段级脱敏
|
||||
user_permissions = get_current_user_permissions()
|
||||
if result.get('items'):
|
||||
result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']]
|
||||
return jsonify({"code": 200, "msg": "success", "data": result})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
@ -51,6 +131,7 @@ def get_list():
|
||||
# 2.1 选项接口 (GET /api/v1/inbound/base/options)
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/options', methods=['GET'])
|
||||
@permission_required('material_list')
|
||||
def get_options():
|
||||
try:
|
||||
data = MaterialBaseService.get_distinct_options()
|
||||
@ -64,6 +145,7 @@ def get_options():
|
||||
# 2.2 导出接口 (GET /api/v1/inbound/base/export)
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/export', methods=['GET'])
|
||||
@permission_required('material_list')
|
||||
def export_data():
|
||||
try:
|
||||
# 获取筛选条件
|
||||
@ -75,8 +157,11 @@ def export_data():
|
||||
'isEnabled': request.args.get('isEnabled', None)
|
||||
}
|
||||
|
||||
# 生成 Excel 文件流
|
||||
file_stream = MaterialBaseService.export_excel(filters)
|
||||
# 获取当前用户权限
|
||||
user_permissions = get_current_user_permissions()
|
||||
|
||||
# 生成 Excel 文件流(传入用户权限进行脱敏)
|
||||
file_stream = MaterialBaseService.export_excel(filters, user_permissions)
|
||||
|
||||
# 生成文件名:库存统计+年月日+时分秒 (北京时间 UTC+8)
|
||||
# 简单处理:UTC时间 + 8小时
|
||||
@ -101,13 +186,48 @@ def export_data():
|
||||
# 3. 新增接口 (POST /api/v1/inbound/base/)
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/', methods=['POST'])
|
||||
@permission_required('material_list:operation')
|
||||
def create():
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"code": 400, "msg": "No data provided"}), 400
|
||||
|
||||
MaterialBaseService.create_material(data)
|
||||
# 获取当前用户权限
|
||||
user_permissions = get_current_user_permissions()
|
||||
# 字段到权限码的映射(与 filter_item_by_permissions 一致)
|
||||
field_to_perm = {
|
||||
'id': 'material_list:id',
|
||||
'companyName': 'material_list:companyName',
|
||||
'name': 'material_list:name',
|
||||
'commonName': 'material_list:commonName',
|
||||
'category': 'material_list:category',
|
||||
'type': 'material_list:type',
|
||||
'spec': 'material_list:spec',
|
||||
'unit': 'material_list:unit',
|
||||
'inventoryCount': 'material_list:inventoryCount',
|
||||
'availableCount': 'material_list:availableCount',
|
||||
'generalManual': 'material_list:files',
|
||||
'generalImage': 'material_list:files',
|
||||
'isEnabled': 'material_list:isEnabled'
|
||||
}
|
||||
# 过滤用户没有权限的字段
|
||||
filtered_data = {}
|
||||
# 如果拥有通配符权限,则不过滤
|
||||
if 'material_list:*' in user_permissions:
|
||||
filtered_data = data
|
||||
else:
|
||||
for key, value in data.items():
|
||||
if key in field_to_perm:
|
||||
perm_code = field_to_perm[key]
|
||||
if perm_code in user_permissions:
|
||||
filtered_data[key] = value
|
||||
# 没有权限则跳过,不包含在 filtered_data 中
|
||||
else:
|
||||
# 不在映射中的字段,默认允许(例如 visibilityLevel)
|
||||
filtered_data[key] = value
|
||||
|
||||
MaterialBaseService.create_material(filtered_data)
|
||||
return jsonify({"code": 200, "msg": "新增成功"})
|
||||
except ValueError as e:
|
||||
# 捕获业务逻辑验证错误 (如名称为空)
|
||||
@ -122,10 +242,45 @@ def create():
|
||||
# 4. 修改接口 (PUT /api/v1/inbound/base/<id>)
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/<int:id>', methods=['PUT'])
|
||||
@permission_required('material_list:operation')
|
||||
def update(id):
|
||||
try:
|
||||
data = request.get_json()
|
||||
MaterialBaseService.update_material(id, data)
|
||||
# 获取当前用户权限
|
||||
user_permissions = get_current_user_permissions()
|
||||
# 字段到权限码的映射(与 filter_item_by_permissions 一致)
|
||||
field_to_perm = {
|
||||
'id': 'material_list:id',
|
||||
'companyName': 'material_list:companyName',
|
||||
'name': 'material_list:name',
|
||||
'commonName': 'material_list:commonName',
|
||||
'category': 'material_list:category',
|
||||
'type': 'material_list:type',
|
||||
'spec': 'material_list:spec',
|
||||
'unit': 'material_list:unit',
|
||||
'inventoryCount': 'material_list:inventoryCount',
|
||||
'availableCount': 'material_list:availableCount',
|
||||
'generalManual': 'material_list:files',
|
||||
'generalImage': 'material_list:files',
|
||||
'isEnabled': 'material_list:isEnabled'
|
||||
}
|
||||
# 过滤用户没有权限的字段
|
||||
filtered_data = {}
|
||||
# 如果拥有通配符权限,则不过滤
|
||||
if 'material_list:*' in user_permissions:
|
||||
filtered_data = data
|
||||
else:
|
||||
for key, value in data.items():
|
||||
if key in field_to_perm:
|
||||
perm_code = field_to_perm[key]
|
||||
if perm_code in user_permissions:
|
||||
filtered_data[key] = value
|
||||
# 没有权限则跳过,不包含在 filtered_data 中
|
||||
else:
|
||||
# 不在映射中的字段,默认允许(例如 visibilityLevel)
|
||||
filtered_data[key] = value
|
||||
# 使用过滤后的数据调用服务
|
||||
MaterialBaseService.update_material(id, filtered_data)
|
||||
return jsonify({"code": 200, "msg": "修改成功"})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
@ -136,10 +291,11 @@ def update(id):
|
||||
# 5. 删除接口 (DELETE /api/v1/inbound/base/<id>)
|
||||
# ==============================================================================
|
||||
@inbound_base_bp.route('/<int:id>', methods=['DELETE'])
|
||||
@permission_required('material_list:operation')
|
||||
def delete(id):
|
||||
try:
|
||||
MaterialBaseService.delete_material(id)
|
||||
return jsonify({"code": 200, "msg": "删除成功"})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
@ -1,14 +1,90 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from app.services.inbound.buy_service import BuyInboundService
|
||||
from app.utils.decorators import permission_required
|
||||
import traceback
|
||||
|
||||
inbound_buy_bp = Blueprint('stock_buy', __name__)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 辅助函数:获取当前用户的完整权限列表(基于角色查询)
|
||||
# ==============================================================================
|
||||
def get_current_user_permissions():
|
||||
"""
|
||||
返回当前用户拥有的所有权限码列表(包括菜单和元素)
|
||||
此函数根据角色查询数据库得到权限。
|
||||
"""
|
||||
from flask_jwt_extended import get_jwt
|
||||
from app.services.auth_service import AuthService
|
||||
claims = get_jwt()
|
||||
user_role = claims.get('role')
|
||||
if not user_role:
|
||||
return []
|
||||
# 超级管理员返回所有字段权限 (忽略大小写)
|
||||
if user_role.upper() == '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',
|
||||
'post_tax_unit_price': 'inbound_buy:post_tax_unit_price',
|
||||
'tax_rate': 'inbound_buy:tax_rate',
|
||||
'total_price': 'inbound_buy:total_price',
|
||||
'currency': 'inbound_buy:currency',
|
||||
'exchange_rate': 'inbound_buy:exchange_rate',
|
||||
'supplier_name': 'inbound_buy:supplier_name',
|
||||
'buyer_name': 'inbound_buy:buyer_name',
|
||||
'buyer_email': 'inbound_buy:buyer_email',
|
||||
'original_link': 'inbound_buy:original_link',
|
||||
'detail_link': 'inbound_buy:detail_link',
|
||||
'arrival_photo': 'inbound_buy:arrival_photo',
|
||||
'inspection_report': 'inbound_buy:inspection_report',
|
||||
'material_name': 'inbound_buy:material_name',
|
||||
'spec_model': 'inbound_buy:spec_model',
|
||||
'category': 'inbound_buy:category',
|
||||
'unit': 'inbound_buy:unit',
|
||||
'material_type': 'inbound_buy:material_type',
|
||||
'company_name': 'inbound_buy:company_name',
|
||||
}
|
||||
# 如果用户是超级管理员且有 'inbound_buy:*',则不过滤
|
||||
if 'inbound_buy:*' in user_permissions:
|
||||
return item_dict
|
||||
for field, perm_code in field_to_perm.items():
|
||||
if field in item_dict and perm_code not in user_permissions:
|
||||
item_dict[field] = None
|
||||
return item_dict
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 0. 基础物料搜索
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/search-base', methods=['GET'])
|
||||
@permission_required('inbound_buy')
|
||||
def search_base():
|
||||
try:
|
||||
keyword = request.args.get('keyword', '')
|
||||
@ -33,6 +109,7 @@ def search_base():
|
||||
# 1. 获取列表 (修改:接收 category 和 material_type)
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/list', methods=['GET'])
|
||||
@permission_required('inbound_buy')
|
||||
def get_list():
|
||||
try:
|
||||
page = request.args.get('page', 1, type=int)
|
||||
@ -42,12 +119,17 @@ def get_list():
|
||||
# 新增筛选参数
|
||||
category = request.args.get('category', '')
|
||||
material_type = request.args.get('material_type', '')
|
||||
company = request.args.get('company', '')
|
||||
|
||||
# 状态参数处理
|
||||
statuses_str = request.args.get('statuses', '')
|
||||
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, company)
|
||||
# 字段级脱敏
|
||||
user_permissions = get_current_user_permissions()
|
||||
if result.get('items'):
|
||||
result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']]
|
||||
return jsonify({"code": 200, "msg": "success", "data": result})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
@ -58,12 +140,59 @@ def get_list():
|
||||
# 2. 新增入库
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/submit', methods=['POST'])
|
||||
@permission_required('inbound_buy:operation')
|
||||
def submit():
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"code": 400, "msg": "No data"}), 400
|
||||
|
||||
# 数据清洗:移除用户没有权限的字段
|
||||
user_permissions = get_current_user_permissions()
|
||||
# 超级管理员不过滤
|
||||
if 'inbound_buy:*' not in user_permissions:
|
||||
# 字段名到权限码的映射(与前端 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',
|
||||
'post_tax_unit_price': 'inbound_buy:post_tax_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',
|
||||
}
|
||||
# 复制一份,避免遍历时修改字典
|
||||
for field in list(data.keys()):
|
||||
perm_code = field_to_perm.get(field)
|
||||
if perm_code and perm_code not in user_permissions:
|
||||
data.pop(field, None)
|
||||
|
||||
new_stock = BuyInboundService.handle_inbound(data)
|
||||
|
||||
return jsonify({
|
||||
@ -80,9 +209,55 @@ def submit():
|
||||
# 3. 更新入库
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/<int:id>', methods=['PUT'])
|
||||
@permission_required('inbound_buy:operation')
|
||||
def update_buy(id):
|
||||
try:
|
||||
data = request.get_json()
|
||||
# 数据清洗:移除用户没有权限的字段
|
||||
user_permissions = get_current_user_permissions()
|
||||
# 超级管理员不过滤
|
||||
if 'inbound_buy:*' not in user_permissions:
|
||||
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',
|
||||
'post_tax_unit_price': 'inbound_buy:post_tax_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',
|
||||
}
|
||||
# 复制一份,避免遍历时修改字典
|
||||
for field in list(data.keys()):
|
||||
perm_code = field_to_perm.get(field)
|
||||
if perm_code and perm_code not in user_permissions:
|
||||
data.pop(field, None)
|
||||
|
||||
BuyInboundService.update_inbound(id, data)
|
||||
return jsonify({"code": 200, "msg": "更新成功"})
|
||||
except Exception as e:
|
||||
@ -93,6 +268,7 @@ def update_buy(id):
|
||||
# 4. 删除
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/<int:id>', methods=['DELETE'])
|
||||
@permission_required('inbound_buy:operation')
|
||||
def delete_buy(id):
|
||||
try:
|
||||
BuyInboundService.delete_inbound(id)
|
||||
@ -105,6 +281,7 @@ def delete_buy(id):
|
||||
# 5. [新增] 获取筛选下拉选项 (修复404的关键)
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/options', methods=['GET'])
|
||||
@permission_required('inbound_buy')
|
||||
def get_options():
|
||||
try:
|
||||
data = BuyInboundService.get_filter_options()
|
||||
@ -117,6 +294,7 @@ def get_options():
|
||||
# 6. 获取关联的出库历史 (如果有)
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/<int:id>/history', methods=['GET'])
|
||||
@permission_required('inbound_buy')
|
||||
def get_history(id):
|
||||
# 如果没有出库模块,这个接口可能为空,但为保持兼容性保留
|
||||
return jsonify({"code": 200, "msg": "success", "data": []})
|
||||
@ -126,6 +304,7 @@ def get_history(id):
|
||||
# 7. 供应商建议
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/suggestions/suppliers', methods=['GET'])
|
||||
@permission_required('inbound_buy')
|
||||
def get_supplier_suggestions():
|
||||
base_id = request.args.get('base_id', type=int)
|
||||
if not base_id:
|
||||
@ -138,6 +317,7 @@ def get_supplier_suggestions():
|
||||
# 8. 采购人建议 (全局)
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/suggestions/users', methods=['GET'])
|
||||
@permission_required('inbound_buy')
|
||||
def get_user_suggestions():
|
||||
keyword = request.args.get('keyword', '')
|
||||
data = BuyInboundService.get_history_purchasers(keyword)
|
||||
@ -148,6 +328,7 @@ def get_user_suggestions():
|
||||
# 9. 链接建议
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/suggestions/links', methods=['GET'])
|
||||
@permission_required('inbound_buy')
|
||||
def get_link_suggestions():
|
||||
base_id = request.args.get('base_id', type=int)
|
||||
link_type = request.args.get('type', 'original') # original or detail
|
||||
@ -161,9 +342,10 @@ def get_link_suggestions():
|
||||
# 10. 库位建议
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_buy_bp.route('/suggestions/locations', methods=['GET'])
|
||||
@permission_required('inbound_buy')
|
||||
def get_location_suggestions():
|
||||
base_id = request.args.get('base_id', type=int)
|
||||
if not base_id:
|
||||
return jsonify({"code": 400, "msg": "base_id required"}), 400
|
||||
data = BuyInboundService.get_history_locations(base_id)
|
||||
return jsonify({"code": 200, "msg": "success", "data": data})
|
||||
return jsonify({"code": 200, "msg": "success", "data": data})
|
||||
|
||||
@ -1,109 +1,129 @@
|
||||
# inventory-backend/app/api/v1/inbound/product.py
|
||||
from flask import Blueprint, request, jsonify
|
||||
from app.services.inbound.product_service import ProductInboundService
|
||||
from app.utils.decorators import permission_required
|
||||
import traceback
|
||||
|
||||
# === 这一行非常关键,绝对不能丢!===
|
||||
inbound_product_bp = Blueprint('stock_product', __name__)
|
||||
|
||||
def get_current_user_permissions():
|
||||
from flask_jwt_extended import get_jwt
|
||||
from app.services.auth_service import AuthService
|
||||
claims = get_jwt()
|
||||
user_role = claims.get('role')
|
||||
if not user_role: return []
|
||||
if user_role.upper() == 'SUPER_ADMIN': return ['inbound_product:*']
|
||||
perm_dict = AuthService.get_user_permissions(user_role)
|
||||
return perm_dict.get('menus', []) + perm_dict.get('elements', [])
|
||||
|
||||
def filter_item_by_permissions(item_dict, user_permissions):
|
||||
field_to_perm = {
|
||||
'id': 'inbound_product:id', 'base_id': 'inbound_product:base_id', 'company_name': 'inbound_product:company_name',
|
||||
'material_name': 'inbound_product:material_name', 'category': 'inbound_product:category',
|
||||
'material_type': 'inbound_product:material_type', 'spec_model': 'inbound_product:spec_model',
|
||||
'unit': 'inbound_product:unit', 'sku': 'inbound_product:sku', 'inbound_date': 'inbound_product:inbound_date',
|
||||
'barcode': 'inbound_product:barcode', 'serial_number': 'inbound_product:serial_number',
|
||||
'status': 'inbound_product:status', 'quality_status': 'inbound_product:quality_status',
|
||||
'in_quantity': 'inbound_product:in_quantity', 'stock_quantity': 'inbound_product:stock_quantity',
|
||||
'available_quantity': 'inbound_product:available_quantity', 'warehouse_location': 'inbound_product:warehouse_location',
|
||||
'bom_code': 'inbound_product:bom_code', 'bom_version': 'inbound_product:bom_version',
|
||||
'work_order_code': 'inbound_product:work_order_code', 'order_id': 'inbound_product:order_id',
|
||||
'production_manager': 'inbound_product:production_manager', 'production_start_time': 'inbound_product:production_start_time',
|
||||
'production_end_time': 'inbound_product:production_end_time', 'raw_material_cost': 'inbound_product:raw_material_cost',
|
||||
'manual_cost': 'inbound_product:manual_cost', 'sale_price': 'inbound_product:sale_price',
|
||||
'product_photo': 'inbound_product:product_photo', 'quality_report_link': 'inbound_product:quality_report_link',
|
||||
'inspection_report_link': 'inbound_product:inspection_report_link', 'detail_link': 'inbound_product:detail_link',
|
||||
}
|
||||
if 'inbound_product:*' in user_permissions: return item_dict
|
||||
for field, perm_code in field_to_perm.items():
|
||||
if field in item_dict and perm_code not in user_permissions: item_dict[field] = None
|
||||
return item_dict
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 0. 基础物料搜索 (关键接口:配合 Service 实现自动回填)
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_product_bp.route('/search-base', methods=['GET'])
|
||||
@permission_required('inbound_product')
|
||||
def search_base():
|
||||
"""
|
||||
对应前端 API: /inbound/product/search-base
|
||||
功能: 模糊搜索基础物料,返回 spec, unit, category, type 等详细信息
|
||||
"""
|
||||
try:
|
||||
keyword = request.args.get('keyword', '')
|
||||
# 调用 Service 层已修复的 search_base_material 方法
|
||||
data = ProductInboundService.search_base_material(keyword)
|
||||
return jsonify({"code": 200, "msg": "success", "data": data})
|
||||
except Exception as e:
|
||||
# 捕获异常并打印堆栈,方便调试
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 0.5 [新增] BOM 搜索接口
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_product_bp.route('/search-bom', methods=['GET'])
|
||||
def search_bom():
|
||||
"""
|
||||
供前端下拉框远程搜索使用 (搜索BOM)
|
||||
"""
|
||||
try:
|
||||
keyword = request.args.get('keyword', '')
|
||||
data = ProductInboundService.search_bom_options(keyword)
|
||||
return jsonify({
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": data
|
||||
})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 1. 获取列表 (支持 status 多选筛选)
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_product_bp.route('/list', methods=['GET'])
|
||||
def get_list():
|
||||
try:
|
||||
page = request.args.get('page', 1, type=int)
|
||||
limit = request.args.get('pageSize', 15, type=int)
|
||||
keyword = request.args.get('keyword', '')
|
||||
|
||||
# 接收状态参数 (逗号分隔字符串 -> 列表)
|
||||
statuses_str = request.args.get('statuses', '')
|
||||
statuses = statuses_str.split(',') if statuses_str else []
|
||||
|
||||
result = ProductInboundService.get_list(page, limit, keyword, statuses)
|
||||
result = ProductInboundService.search_base_material(keyword, page)
|
||||
user_permissions = get_current_user_permissions()
|
||||
if result.get('items'):
|
||||
result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']]
|
||||
return jsonify({"code": 200, "msg": "success", "data": result})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 2. 新增入库
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_product_bp.route('/submit', methods=['POST'])
|
||||
def submit():
|
||||
@inbound_product_bp.route('/search-bom', methods=['GET'])
|
||||
@permission_required('inbound_product')
|
||||
def search_bom():
|
||||
try:
|
||||
# 调用 Service 处理入库,获取新创建的对象
|
||||
new_stock = ProductInboundService.handle_inbound(request.get_json())
|
||||
|
||||
# 返回成功信息以及新创建的数据(包含生成的ID和SKU),供前端自动打印使用
|
||||
return jsonify({
|
||||
"code": 200,
|
||||
"msg": "入库成功",
|
||||
"data": new_stock.to_dict()
|
||||
})
|
||||
keyword = request.args.get('keyword', '')
|
||||
data = ProductInboundService.search_bom_options(keyword)
|
||||
return jsonify({"code": 200, "msg": "success", "data": data})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
@inbound_product_bp.route('/list', methods=['GET'])
|
||||
@permission_required('inbound_product')
|
||||
def get_list():
|
||||
try:
|
||||
page = request.args.get('page', 1, type=int)
|
||||
limit = request.args.get('pageSize', 15, type=int)
|
||||
keyword = request.args.get('keyword', '')
|
||||
statuses_str = request.args.get('statuses', '')
|
||||
statuses = statuses_str.split(',') if statuses_str else []
|
||||
category = request.args.get('category', '')
|
||||
material_type = request.args.get('material_type', '')
|
||||
company = request.args.get('company', '')
|
||||
result = ProductInboundService.get_list(page, limit, keyword, statuses, category, material_type, company)
|
||||
user_permissions = get_current_user_permissions()
|
||||
if result.get('items'):
|
||||
result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']]
|
||||
return jsonify({"code": 200, "msg": "success", "data": result})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
@inbound_product_bp.route('/submit', methods=['POST'])
|
||||
@permission_required('inbound_product:operation')
|
||||
def submit():
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data: return jsonify({"code": 400, "msg": "No data"}), 400
|
||||
user_permissions = get_current_user_permissions()
|
||||
if 'inbound_product:*' not in user_permissions:
|
||||
field_to_perm = {'id': 'inbound_product:id', 'base_id': 'inbound_product:base_id', 'company_name': 'inbound_product:company_name', 'material_name': 'inbound_product:material_name', 'category': 'inbound_product:category', 'material_type': 'inbound_product:material_type', 'spec_model': 'inbound_product:spec_model', 'unit': 'inbound_product:unit', 'sku': 'inbound_product:sku', 'inbound_date': 'inbound_product:inbound_date', 'barcode': 'inbound_product:barcode', 'serial_number': 'inbound_product:serial_number', 'status': 'inbound_product:status', 'quality_status': 'inbound_product:quality_status', 'in_quantity': 'inbound_product:in_quantity', 'stock_quantity': 'inbound_product:stock_quantity', 'available_quantity': 'inbound_product:available_quantity', 'warehouse_location': 'inbound_product:warehouse_location', 'bom_code': 'inbound_product:bom_code', 'bom_version': 'inbound_product:bom_version', 'work_order_code': 'inbound_product:work_order_code', 'order_id': 'inbound_product:order_id', 'production_manager': 'inbound_product:production_manager', 'production_start_time': 'inbound_product:production_start_time', 'production_end_time': 'inbound_product:production_end_time', 'raw_material_cost': 'inbound_product:raw_material_cost', 'manual_cost': 'inbound_product:manual_cost', 'sale_price': 'inbound_product:sale_price', 'product_photo': 'inbound_product:product_photo', 'quality_report_link': 'inbound_product:quality_report_link', 'inspection_report_link': 'inbound_product:inspection_report_link', 'detail_link': 'inbound_product:detail_link'}
|
||||
for field in list(data.keys()):
|
||||
perm_code = field_to_perm.get(field)
|
||||
if perm_code and perm_code not in user_permissions: data.pop(field, None)
|
||||
new_stock = ProductInboundService.handle_inbound(data)
|
||||
return jsonify({"code": 200, "msg": "入库成功", "data": new_stock.to_dict()})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 3. 更新入库
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_product_bp.route('/<int:id>', methods=['PUT'])
|
||||
@permission_required('inbound_product:operation')
|
||||
def update(id):
|
||||
try:
|
||||
ProductInboundService.update_inbound(id, request.get_json())
|
||||
data = request.get_json()
|
||||
user_permissions = get_current_user_permissions()
|
||||
if 'inbound_product:*' not in user_permissions:
|
||||
field_to_perm = {'id': 'inbound_product:id', 'base_id': 'inbound_product:base_id', 'company_name': 'inbound_product:company_name', 'material_name': 'inbound_product:material_name', 'category': 'inbound_product:category', 'material_type': 'inbound_product:material_type', 'spec_model': 'inbound_product:spec_model', 'unit': 'inbound_product:unit', 'sku': 'inbound_product:sku', 'inbound_date': 'inbound_product:inbound_date', 'barcode': 'inbound_product:barcode', 'serial_number': 'inbound_product:serial_number', 'status': 'inbound_product:status', 'quality_status': 'inbound_product:quality_status', 'in_quantity': 'inbound_product:in_quantity', 'stock_quantity': 'inbound_product:stock_quantity', 'available_quantity': 'inbound_product:available_quantity', 'warehouse_location': 'inbound_product:warehouse_location', 'bom_code': 'inbound_product:bom_code', 'bom_version': 'inbound_product:bom_version', 'work_order_code': 'inbound_product:work_order_code', 'order_id': 'inbound_product:order_id', 'production_manager': 'inbound_product:production_manager', 'production_start_time': 'inbound_product:production_start_time', 'production_end_time': 'inbound_product:production_end_time', 'raw_material_cost': 'inbound_product:raw_material_cost', 'manual_cost': 'inbound_product:manual_cost', 'sale_price': 'inbound_product:sale_price', 'product_photo': 'inbound_product:product_photo', 'quality_report_link': 'inbound_product:quality_report_link', 'inspection_report_link': 'inbound_product:inspection_report_link', 'detail_link': 'inbound_product:detail_link'}
|
||||
for field in list(data.keys()):
|
||||
perm_code = field_to_perm.get(field)
|
||||
if perm_code and perm_code not in user_permissions: data.pop(field, None)
|
||||
ProductInboundService.update_inbound(id, data)
|
||||
return jsonify({"code": 200, "msg": "更新成功"})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 4. 删除
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_product_bp.route('/<int:id>', methods=['DELETE'])
|
||||
@permission_required('inbound_product:operation')
|
||||
def delete(id):
|
||||
try:
|
||||
ProductInboundService.delete_inbound(id)
|
||||
@ -112,11 +132,8 @@ def delete(id):
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 5. 获取出库历史
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_product_bp.route('/<int:id>/history', methods=['GET'])
|
||||
@permission_required('inbound_product')
|
||||
def get_history(id):
|
||||
try:
|
||||
data = ProductInboundService.get_outbound_history(id)
|
||||
@ -125,24 +142,29 @@ def get_history(id):
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 6. 系统用户建议
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_product_bp.route('/suggestions/users', methods=['GET'])
|
||||
@permission_required('inbound_product')
|
||||
def get_user_suggestions():
|
||||
keyword = request.args.get('keyword', '')
|
||||
data = ProductInboundService.search_system_users(keyword)
|
||||
return jsonify({"code": 200, "msg": "success", "data": data})
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 7. 获取筛选选项
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_product_bp.route('/options', methods=['GET'])
|
||||
@permission_required('inbound_product')
|
||||
def get_options():
|
||||
try:
|
||||
data = ProductInboundService.get_filter_options()
|
||||
return jsonify({"code": 200, "msg": "success", "data": data})
|
||||
except Exception as e:
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
@inbound_product_bp.route('/suggestions/managers', methods=['GET'])
|
||||
@permission_required('inbound_product')
|
||||
def get_manager_history():
|
||||
keyword = request.args.get('keyword', '')
|
||||
try:
|
||||
data = ProductInboundService.get_history_managers(keyword)
|
||||
return jsonify({"code": 200, "msg": "success", "data": data})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
@ -1,120 +1,126 @@
|
||||
# inventory-backend/app/api/v1/inbound/semi.py
|
||||
from flask import Blueprint, request, jsonify
|
||||
from app.services.inbound.semi_service import SemiInboundService
|
||||
from app.utils.decorators import permission_required
|
||||
import traceback
|
||||
|
||||
# 定义蓝图
|
||||
# === 这一行非常关键,绝对不能丢!===
|
||||
inbound_semi_bp = Blueprint('stock_semi', __name__)
|
||||
|
||||
def get_current_user_permissions():
|
||||
from flask_jwt_extended import get_jwt
|
||||
from app.services.auth_service import AuthService
|
||||
claims = get_jwt()
|
||||
user_role = claims.get('role')
|
||||
if not user_role: return []
|
||||
if user_role.upper() == 'SUPER_ADMIN': return ['inbound_semi:*']
|
||||
perm_dict = AuthService.get_user_permissions(user_role)
|
||||
return perm_dict.get('menus', []) + perm_dict.get('elements', [])
|
||||
|
||||
def filter_item_by_permissions(item_dict, user_permissions):
|
||||
field_to_perm = {
|
||||
'id': 'inbound_semi:id', 'base_id': 'inbound_semi:base_id', 'company_name': 'inbound_semi:company_name',
|
||||
'material_name': 'inbound_semi:material_name', 'category': 'inbound_semi:category',
|
||||
'material_type': 'inbound_semi:material_type', 'spec_model': 'inbound_semi:spec_model',
|
||||
'unit': 'inbound_semi:unit', 'sku': 'inbound_semi:sku', 'inbound_date': 'inbound_semi:inbound_date',
|
||||
'barcode': 'inbound_semi:barcode', 'serial_number': 'inbound_semi:serial_number',
|
||||
'batch_number': 'inbound_semi:batch_number', 'status': 'inbound_semi:status',
|
||||
'quality_status': 'inbound_semi:quality_status', 'in_quantity': 'inbound_semi:in_quantity',
|
||||
'stock_quantity': 'inbound_semi:stock_quantity', 'available_quantity': 'inbound_semi:available_quantity',
|
||||
'warehouse_location': 'inbound_semi:warehouse_location', 'bom_code': 'inbound_semi:bom_code',
|
||||
'bom_version': 'inbound_semi:bom_version', 'work_order_code': 'inbound_semi:work_order_code',
|
||||
'raw_material_cost': 'inbound_semi:raw_material_cost', 'manual_cost': 'inbound_semi:manual_cost',
|
||||
'unit_total_cost': 'inbound_semi:unit_total_cost', 'production_manager': 'inbound_semi:production_manager',
|
||||
'production_start_time': 'inbound_semi:production_start_time', 'production_end_time': 'inbound_semi:production_end_time',
|
||||
'arrival_photo': 'inbound_semi:arrival_photo', 'quality_report_link': 'inbound_semi:quality_report_link',
|
||||
'detail_link': 'inbound_semi:detail_link',
|
||||
}
|
||||
if 'inbound_semi:*' in user_permissions: return item_dict
|
||||
for field, perm_code in field_to_perm.items():
|
||||
if field in item_dict and perm_code not in user_permissions: item_dict[field] = None
|
||||
return item_dict
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 0. 基础物料搜索 (复用逻辑)
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_semi_bp.route('/search-base', methods=['GET'])
|
||||
@permission_required('inbound_semi')
|
||||
def search_base():
|
||||
"""
|
||||
供前端下拉框远程搜索使用 (搜索半成品类型的基础物料)
|
||||
Query Param: keyword (名称或规格)
|
||||
"""
|
||||
try:
|
||||
keyword = request.args.get('keyword', '')
|
||||
# 这里复用 Service 中的搜索逻辑
|
||||
data = SemiInboundService.search_base_material(keyword)
|
||||
return jsonify({
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": data
|
||||
})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 0.5 [新增] BOM 搜索接口
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_semi_bp.route('/search-bom', methods=['GET'])
|
||||
def search_bom():
|
||||
"""
|
||||
供前端下拉框远程搜索使用 (搜索BOM)
|
||||
Query Param: keyword (编号或父件规格)
|
||||
"""
|
||||
try:
|
||||
keyword = request.args.get('keyword', '')
|
||||
data = SemiInboundService.search_bom_options(keyword)
|
||||
return jsonify({
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": data
|
||||
})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 1. 获取半成品列表
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_semi_bp.route('/list', methods=['GET'])
|
||||
def get_list():
|
||||
try:
|
||||
page = request.args.get('page', 1, type=int)
|
||||
limit = request.args.get('pageSize', 15, type=int)
|
||||
# 支持按关键字搜索:BOM号、工单号、SN、批号等
|
||||
keyword = request.args.get('keyword', '')
|
||||
|
||||
# [修改] 获取状态列表参数
|
||||
statuses_str = request.args.get('statuses', '')
|
||||
statuses = statuses_str.split(',') if statuses_str else []
|
||||
|
||||
result = SemiInboundService.get_list(page, limit, keyword, statuses)
|
||||
result = SemiInboundService.search_base_material(keyword, page)
|
||||
user_permissions = get_current_user_permissions()
|
||||
if result.get('items'):
|
||||
result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']]
|
||||
return jsonify({"code": 200, "msg": "success", "data": result})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 2. 新增半成品入库 (修改:返回创建的对象数据)
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_semi_bp.route('/submit', methods=['POST'])
|
||||
def submit():
|
||||
@inbound_semi_bp.route('/search-bom', methods=['GET'])
|
||||
@permission_required('inbound_semi')
|
||||
def search_bom():
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"code": 400, "msg": "No data"}), 400
|
||||
|
||||
# 修改:调用 Service 处理入库,获取新创建的对象
|
||||
new_stock = SemiInboundService.handle_inbound(data)
|
||||
|
||||
# 修改:返回成功信息以及新创建的数据(包含生成的ID和SKU),供前端打印使用
|
||||
return jsonify({
|
||||
"code": 200,
|
||||
"msg": "入库成功",
|
||||
"data": new_stock.to_dict()
|
||||
})
|
||||
keyword = request.args.get('keyword', '')
|
||||
data = SemiInboundService.search_bom_options(keyword)
|
||||
return jsonify({"code": 200, "msg": "success", "data": data})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
@inbound_semi_bp.route('/list', methods=['GET'])
|
||||
@permission_required('inbound_semi')
|
||||
def get_list():
|
||||
try:
|
||||
page = request.args.get('page', 1, type=int)
|
||||
limit = request.args.get('pageSize', 15, type=int)
|
||||
keyword = request.args.get('keyword', '')
|
||||
statuses_str = request.args.get('statuses', '')
|
||||
statuses = statuses_str.split(',') if statuses_str else []
|
||||
result = SemiInboundService.get_list(page, limit, keyword, statuses)
|
||||
user_permissions = get_current_user_permissions()
|
||||
if result.get('items'):
|
||||
result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']]
|
||||
return jsonify({"code": 200, "msg": "success", "data": result})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
@inbound_semi_bp.route('/submit', methods=['POST'])
|
||||
@permission_required('inbound_semi:operation')
|
||||
def submit():
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data: return jsonify({"code": 400, "msg": "No data"}), 400
|
||||
user_permissions = get_current_user_permissions()
|
||||
if 'inbound_semi:*' not in user_permissions:
|
||||
field_to_perm = {'id': 'inbound_semi:id', 'base_id': 'inbound_semi:base_id', 'company_name': 'inbound_semi:company_name', 'material_name': 'inbound_semi:material_name', 'category': 'inbound_semi:category', 'material_type': 'inbound_semi:material_type', 'spec_model': 'inbound_semi:spec_model', 'unit': 'inbound_semi:unit', 'sku': 'inbound_semi:sku', 'inbound_date': 'inbound_semi:inbound_date', 'barcode': 'inbound_semi:barcode', 'serial_number': 'inbound_semi:serial_number', 'batch_number': 'inbound_semi:batch_number', 'status': 'inbound_semi:status', 'quality_status': 'inbound_semi:quality_status', 'in_quantity': 'inbound_semi:in_quantity', 'stock_quantity': 'inbound_semi:stock_quantity', 'available_quantity': 'inbound_semi:available_quantity', 'warehouse_location': 'inbound_semi:warehouse_location', 'bom_code': 'inbound_semi:bom_code', 'bom_version': 'inbound_semi:bom_version', 'work_order_code': 'inbound_semi:work_order_code', 'raw_material_cost': 'inbound_semi:raw_material_cost', 'manual_cost': 'inbound_semi:manual_cost', 'unit_total_cost': 'inbound_semi:unit_total_cost', 'production_manager': 'inbound_semi:production_manager', 'production_start_time': 'inbound_semi:production_start_time', 'production_end_time': 'inbound_semi:production_end_time', 'arrival_photo': 'inbound_semi:arrival_photo', 'quality_report_link': 'inbound_semi:quality_report_link', 'detail_link': 'inbound_semi:detail_link'}
|
||||
for field in list(data.keys()):
|
||||
perm_code = field_to_perm.get(field)
|
||||
if perm_code and perm_code not in user_permissions: data.pop(field, None)
|
||||
new_stock = SemiInboundService.handle_inbound(data)
|
||||
return jsonify({"code": 200, "msg": "入库成功", "data": new_stock.to_dict()})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 3. 更新半成品入库信息
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_semi_bp.route('/<int:id>', methods=['PUT'])
|
||||
@permission_required('inbound_semi:operation')
|
||||
def update_semi(id):
|
||||
try:
|
||||
data = request.get_json()
|
||||
user_permissions = get_current_user_permissions()
|
||||
if 'inbound_semi:*' not in user_permissions:
|
||||
field_to_perm = {'id': 'inbound_semi:id', 'base_id': 'inbound_semi:base_id', 'company_name': 'inbound_semi:company_name', 'material_name': 'inbound_semi:material_name', 'category': 'inbound_semi:category', 'material_type': 'inbound_semi:material_type', 'spec_model': 'inbound_semi:spec_model', 'unit': 'inbound_semi:unit', 'sku': 'inbound_semi:sku', 'inbound_date': 'inbound_semi:inbound_date', 'barcode': 'inbound_semi:barcode', 'serial_number': 'inbound_semi:serial_number', 'batch_number': 'inbound_semi:batch_number', 'status': 'inbound_semi:status', 'quality_status': 'inbound_semi:quality_status', 'in_quantity': 'inbound_semi:in_quantity', 'stock_quantity': 'inbound_semi:stock_quantity', 'available_quantity': 'inbound_semi:available_quantity', 'warehouse_location': 'inbound_semi:warehouse_location', 'bom_code': 'inbound_semi:bom_code', 'bom_version': 'inbound_semi:bom_version', 'work_order_code': 'inbound_semi:work_order_code', 'raw_material_cost': 'inbound_semi:raw_material_cost', 'manual_cost': 'inbound_semi:manual_cost', 'unit_total_cost': 'inbound_semi:unit_total_cost', 'production_manager': 'inbound_semi:production_manager', 'production_start_time': 'inbound_semi:production_start_time', 'production_end_time': 'inbound_semi:production_end_time', 'arrival_photo': 'inbound_semi:arrival_photo', 'quality_report_link': 'inbound_semi:quality_report_link', 'detail_link': 'inbound_semi:detail_link'}
|
||||
for field in list(data.keys()):
|
||||
perm_code = field_to_perm.get(field)
|
||||
if perm_code and perm_code not in user_permissions: data.pop(field, None)
|
||||
SemiInboundService.update_inbound(id, data)
|
||||
return jsonify({"code": 200, "msg": "更新成功"})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 4. 删除半成品入库记录
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_semi_bp.route('/<int:id>', methods=['DELETE'])
|
||||
@permission_required('inbound_semi:operation')
|
||||
def delete_semi(id):
|
||||
try:
|
||||
SemiInboundService.delete_inbound(id)
|
||||
@ -123,41 +129,39 @@ def delete_semi(id):
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 5. [新增] 获取关联出库历史
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_semi_bp.route('/<int:id>/history', methods=['GET'])
|
||||
@permission_required('inbound_semi')
|
||||
def get_history(id):
|
||||
try:
|
||||
data = SemiInboundService.get_outbound_history(id)
|
||||
return jsonify({
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": data
|
||||
})
|
||||
return jsonify({"code": 200, "msg": "success", "data": data})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 6. 系统用户建议
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_semi_bp.route('/suggestions/users', methods=['GET'])
|
||||
@permission_required('inbound_semi')
|
||||
def get_user_suggestions():
|
||||
keyword = request.args.get('keyword', '')
|
||||
data = SemiInboundService.search_system_users(keyword)
|
||||
return jsonify({"code": 200, "msg": "success", "data": data})
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 7. 获取筛选选项
|
||||
# ------------------------------------------------------------------
|
||||
@inbound_semi_bp.route('/options', methods=['GET'])
|
||||
@permission_required('inbound_semi')
|
||||
def get_options():
|
||||
try:
|
||||
data = SemiInboundService.get_filter_options()
|
||||
return jsonify({"code": 200, "msg": "success", "data": data})
|
||||
except Exception as e:
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
|
||||
@inbound_semi_bp.route('/suggestions/managers', methods=['GET'])
|
||||
@permission_required('inbound_semi')
|
||||
def get_manager_history():
|
||||
keyword = request.args.get('keyword', '')
|
||||
try:
|
||||
data = SemiInboundService.get_history_managers(keyword)
|
||||
return jsonify({"code": 200, "msg": "success", "data": data})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({"code": 500, "msg": str(e)}), 500
|
||||
@ -3,21 +3,73 @@ from flask import request, jsonify, current_app
|
||||
from flask_jwt_extended import jwt_required
|
||||
from . import inbound_bp
|
||||
from app.services.inbound.service_service import ServiceService
|
||||
from app.utils.decorators import role_required
|
||||
from app.utils.decorators import role_required, permission_required
|
||||
import traceback
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 辅助函数:获取当前用户的完整权限列表(基于角色查询)
|
||||
# ==============================================================================
|
||||
def get_current_user_permissions():
|
||||
"""
|
||||
返回当前用户拥有的所有权限码列表(包括菜单和元素)
|
||||
此函数根据角色查询数据库得到权限。
|
||||
"""
|
||||
from flask_jwt_extended import get_jwt
|
||||
from app.services.auth_service import AuthService
|
||||
claims = get_jwt()
|
||||
user_role = claims.get('role')
|
||||
if not user_role:
|
||||
return []
|
||||
# 超级管理员返回所有字段权限 (忽略大小写)
|
||||
if user_role.upper() == 'SUPER_ADMIN':
|
||||
return ['inbound_service:*']
|
||||
perm_dict = AuthService.get_user_permissions(user_role)
|
||||
# 合并菜单和元素权限
|
||||
perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
|
||||
return perms
|
||||
|
||||
|
||||
def filter_item_by_permissions(item_dict, user_permissions):
|
||||
"""
|
||||
根据用户权限过滤 item 字典,无权限的字段值置为 None
|
||||
"""
|
||||
# 字段名到权限码的映射(与前端 permissionMap 保持一致)
|
||||
field_to_perm = {
|
||||
'id': 'inbound_service:id',
|
||||
'base_id': 'inbound_service:base_id',
|
||||
'sku': 'inbound_service:sku',
|
||||
'material_name': 'inbound_service:material_name',
|
||||
'provider_name': 'inbound_service:provider_name',
|
||||
'sale_price': 'inbound_service:sale_price',
|
||||
'description': 'inbound_service:description',
|
||||
'created_at': 'inbound_service:created_at',
|
||||
'material_type': 'inbound_service:material_type',
|
||||
'category': 'inbound_service:category',
|
||||
'spec_model': 'inbound_service:spec_model',
|
||||
'unit': 'inbound_service:unit',
|
||||
}
|
||||
if 'inbound_service:*' in user_permissions:
|
||||
return item_dict
|
||||
for field, perm_code in field_to_perm.items():
|
||||
if field in item_dict and perm_code not in user_permissions:
|
||||
item_dict[field] = None
|
||||
return item_dict
|
||||
|
||||
|
||||
@inbound_bp.route('/service/search-base', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('inbound_service')
|
||||
def search_base():
|
||||
"""搜索基础物料"""
|
||||
keyword = request.args.get('keyword', '')
|
||||
try:
|
||||
data = ServiceService.search_base_material(keyword)
|
||||
user_permissions = get_current_user_permissions()
|
||||
filtered_data = [filter_item_by_permissions(item, user_permissions) for item in data]
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': 'success',
|
||||
'data': data
|
||||
'data': filtered_data
|
||||
})
|
||||
except Exception as e:
|
||||
current_app.logger.error(f'搜索基础物料失败: {str(e)}')
|
||||
@ -25,7 +77,7 @@ def search_base():
|
||||
|
||||
|
||||
@inbound_bp.route('/service', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('inbound_service')
|
||||
def get_service_list():
|
||||
"""获取服务权益列表"""
|
||||
page = request.args.get('page', 1, type=int)
|
||||
@ -44,6 +96,9 @@ def get_service_list():
|
||||
end_date=end_date,
|
||||
provider_name=provider_name
|
||||
)
|
||||
user_permissions = get_current_user_permissions()
|
||||
if result.get('items'):
|
||||
result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']]
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': 'success',
|
||||
@ -56,14 +111,38 @@ def get_service_list():
|
||||
|
||||
|
||||
@inbound_bp.route('/service', methods=['POST'])
|
||||
@jwt_required()
|
||||
@role_required('admin,manager')
|
||||
@permission_required('inbound_service:operation')
|
||||
def create_service():
|
||||
"""创建服务权益"""
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'code': 400, 'msg': '请求数据为空'}), 400
|
||||
|
||||
# 数据清洗:移除用户没有权限的字段
|
||||
user_permissions = get_current_user_permissions()
|
||||
# 超级管理员不过滤
|
||||
if 'inbound_service:*' not in user_permissions:
|
||||
# 字段名到权限码的映射(与前端 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',
|
||||
}
|
||||
# 复制一份,避免遍历时修改字典
|
||||
for field in list(data.keys()):
|
||||
perm_code = field_to_perm.get(field)
|
||||
if perm_code and perm_code not in user_permissions:
|
||||
data.pop(field, None)
|
||||
|
||||
# 基础校验
|
||||
if not data.get('base_id'):
|
||||
return jsonify({'code': 400, 'msg': '请选择基础物料'}), 400
|
||||
@ -72,10 +151,12 @@ def create_service():
|
||||
|
||||
try:
|
||||
service = ServiceService.create_service(data)
|
||||
user_permissions = get_current_user_permissions()
|
||||
filtered_data = filter_item_by_permissions(service.to_dict(), user_permissions)
|
||||
return jsonify({
|
||||
'code': 201,
|
||||
'msg': '创建成功',
|
||||
'data': service.to_dict()
|
||||
'data': filtered_data
|
||||
}), 201
|
||||
except ValueError as e:
|
||||
return jsonify({'code': 400, 'msg': str(e)}), 400
|
||||
@ -86,15 +167,17 @@ def create_service():
|
||||
|
||||
|
||||
@inbound_bp.route('/service/<int:service_id>', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('inbound_service')
|
||||
def get_service(service_id):
|
||||
"""获取单个服务权益详情"""
|
||||
try:
|
||||
service = ServiceService.get_service(service_id)
|
||||
user_permissions = get_current_user_permissions()
|
||||
filtered_data = filter_item_by_permissions(service.to_dict(), user_permissions)
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': 'success',
|
||||
'data': service.to_dict()
|
||||
'data': filtered_data
|
||||
})
|
||||
except ValueError as e:
|
||||
return jsonify({'code': 404, 'msg': str(e)}), 404
|
||||
@ -104,14 +187,38 @@ def get_service(service_id):
|
||||
|
||||
|
||||
@inbound_bp.route('/service/<int:service_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
@role_required('admin,manager')
|
||||
@permission_required('inbound_service:operation')
|
||||
def update_service(service_id):
|
||||
"""更新服务权益"""
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'code': 400, 'msg': '请求数据为空'}), 400
|
||||
|
||||
# 数据清洗:移除用户没有权限的字段
|
||||
user_permissions = get_current_user_permissions()
|
||||
# 超级管理员不过滤
|
||||
if 'inbound_service:*' not in user_permissions:
|
||||
# 字段名到权限码的映射(与前端 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',
|
||||
}
|
||||
# 复制一份,避免遍历时修改字典
|
||||
for field in list(data.keys()):
|
||||
perm_code = field_to_perm.get(field)
|
||||
if perm_code and perm_code not in user_permissions:
|
||||
data.pop(field, None)
|
||||
|
||||
# 允许更新的字段
|
||||
allowed_fields = {
|
||||
'sale_price', 'provider_name', 'description',
|
||||
@ -124,10 +231,12 @@ def update_service(service_id):
|
||||
|
||||
try:
|
||||
service = ServiceService.update_service(service_id, filtered_data)
|
||||
user_permissions = get_current_user_permissions()
|
||||
filtered_service = filter_item_by_permissions(service.to_dict(), user_permissions)
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': '更新成功',
|
||||
'data': service.to_dict()
|
||||
'data': filtered_service
|
||||
})
|
||||
except ValueError as e:
|
||||
return jsonify({'code': 404, 'msg': str(e)}), 404
|
||||
@ -137,8 +246,7 @@ def update_service(service_id):
|
||||
|
||||
|
||||
@inbound_bp.route('/service/<int:service_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@role_required('admin,manager')
|
||||
@permission_required('inbound_service:operation')
|
||||
def delete_service(service_id):
|
||||
"""删除服务权益"""
|
||||
try:
|
||||
@ -155,7 +263,7 @@ def delete_service(service_id):
|
||||
|
||||
|
||||
@inbound_bp.route('/service/suggestions/providers', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('inbound_service')
|
||||
def get_provider_suggestions():
|
||||
base_id = request.args.get('base_id', type=int)
|
||||
if not base_id:
|
||||
@ -165,7 +273,7 @@ def get_provider_suggestions():
|
||||
|
||||
|
||||
@inbound_bp.route('/service/suggestions/users', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('inbound_service')
|
||||
def get_user_suggestions():
|
||||
keyword = request.args.get('keyword', '')
|
||||
data = ServiceService.search_system_users(keyword)
|
||||
@ -173,10 +281,10 @@ def get_user_suggestions():
|
||||
|
||||
|
||||
@inbound_bp.route('/service/options', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('inbound_service')
|
||||
def get_options():
|
||||
try:
|
||||
data = ServiceService.get_filter_options()
|
||||
return jsonify({'code': 200, 'msg': 'success', 'data': data})
|
||||
except Exception as e:
|
||||
return jsonify({'code': 500, 'msg': str(e)}), 500
|
||||
return jsonify({'code': 500, 'msg': str(e)}), 500
|
||||
|
||||
@ -2,6 +2,7 @@ from flask import Blueprint, jsonify, request
|
||||
from app.extensions import db
|
||||
# ★★★ 修复点:必须引入 datetime,否则下方更新时间时会报错 500 ★★★
|
||||
from datetime import datetime
|
||||
from app.utils.decorators import permission_required
|
||||
|
||||
# 导入模型
|
||||
from app.models.inbound.buy import StockBuy
|
||||
@ -24,6 +25,7 @@ bp = Blueprint('stock_ops', __name__)
|
||||
|
||||
|
||||
@bp.route('/all', methods=['GET'])
|
||||
@permission_required('inventory_stocktake')
|
||||
def get_all_stock():
|
||||
"""
|
||||
获取所有库存 > 0 的物品
|
||||
@ -63,6 +65,7 @@ def get_all_stock():
|
||||
# --- 草稿箱接口 ---
|
||||
|
||||
@bp.route('/draft/list', methods=['GET'])
|
||||
@permission_required('inventory_stocktake')
|
||||
def get_drafts():
|
||||
"""获取当前用户的盘点进度"""
|
||||
user_id = request.args.get('user_id', 'admin')
|
||||
@ -71,6 +74,7 @@ def get_drafts():
|
||||
|
||||
|
||||
@bp.route('/draft/add', methods=['POST'])
|
||||
@permission_required('inventory_stocktake:operation')
|
||||
def add_draft():
|
||||
"""扫码同步 (支持更新数量)"""
|
||||
try:
|
||||
@ -100,6 +104,7 @@ def add_draft():
|
||||
|
||||
|
||||
@bp.route('/draft/clear', methods=['POST'])
|
||||
@permission_required('inventory_stocktake:operation')
|
||||
def clear_draft():
|
||||
"""清空进度"""
|
||||
data = request.json
|
||||
@ -110,9 +115,26 @@ def clear_draft():
|
||||
return jsonify({"message": "Cleared"}), 200
|
||||
|
||||
|
||||
@bp.route('/borrowed-quantities', methods=['POST'])
|
||||
@permission_required('inventory_stocktake')
|
||||
def get_borrowed_quantities():
|
||||
"""批量获取借出未还数量"""
|
||||
from app.models.transaction import TransBorrow
|
||||
data = request.json.get('items', [])
|
||||
result = {}
|
||||
for item in data:
|
||||
source = item.get('source_table')
|
||||
stock_id = item.get('stock_id')
|
||||
if source and stock_id is not None:
|
||||
qty = TransBorrow.get_borrowed_quantity(source, stock_id)
|
||||
result[f"{source}_{stock_id}"] = qty
|
||||
return jsonify(result), 200
|
||||
|
||||
|
||||
# --- 打印接口 ---
|
||||
|
||||
@bp.route('/print/selection', methods=['POST'])
|
||||
@permission_required('inventory_stocktake:operation')
|
||||
def print_selection():
|
||||
try:
|
||||
data = request.json
|
||||
@ -126,6 +148,7 @@ def print_selection():
|
||||
|
||||
|
||||
@bp.route('/print/stocktake', methods=['POST'])
|
||||
@permission_required('inventory_stocktake:operation')
|
||||
def print_stocktake():
|
||||
try:
|
||||
data = request.json
|
||||
@ -133,4 +156,4 @@ def print_stocktake():
|
||||
success, msg = printer.print_stocktake_report(data)
|
||||
return jsonify({"message": "盘点报告已发送" if success else msg}), 200 if success else 500
|
||||
except Exception as e:
|
||||
return jsonify({"message": str(e)}), 500
|
||||
return jsonify({"message": str(e)}), 500
|
||||
|
||||
@ -1,17 +1,80 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from app.services.outbound_service import OutboundService
|
||||
from flask_jwt_extended import jwt_required, get_jwt_identity
|
||||
from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt
|
||||
from app.utils.decorators import permission_required
|
||||
from app.services.auth_service import AuthService
|
||||
import traceback
|
||||
|
||||
outbound_bp = Blueprint('outbound', __name__, url_prefix='/outbound')
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 辅助函数:获取当前用户的完整权限列表(基于角色查询)
|
||||
# ==============================================================================
|
||||
def get_current_user_permissions():
|
||||
"""
|
||||
返回当前用户拥有的所有权限码列表(包括菜单和元素)
|
||||
此函数根据角色查询数据库得到权限。
|
||||
"""
|
||||
from flask_jwt_extended import get_jwt
|
||||
from app.services.auth_service import AuthService
|
||||
claims = get_jwt()
|
||||
user_role = claims.get('role')
|
||||
if not user_role:
|
||||
return []
|
||||
# 超级管理员返回所有字段权限 (忽略大小写)
|
||||
if user_role.upper() == 'SUPER_ADMIN':
|
||||
return ['outbound_list:*']
|
||||
perm_dict = AuthService.get_user_permissions(user_role)
|
||||
# 合并菜单和元素权限
|
||||
perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
|
||||
return perms
|
||||
|
||||
|
||||
def filter_item_by_permissions(item_dict, user_permissions):
|
||||
"""
|
||||
根据用户权限过滤 item 字典,无权限的字段值置为 None
|
||||
"""
|
||||
# 字段名到权限码的映射(与前端 permissionMap 保持一致)
|
||||
field_to_perm = {
|
||||
'outbound_no': 'outbound_list:outbound_no',
|
||||
'outbound_time': 'outbound_list:outbound_time',
|
||||
'outbound_type': 'outbound_list:outbound_type',
|
||||
'total_amount': 'outbound_list:total_amount',
|
||||
'consumer_name': 'outbound_list:consumer_name',
|
||||
'operator_name': 'outbound_list:operator_name',
|
||||
'remark': 'outbound_list:remark',
|
||||
'signature_path': 'outbound_list:signature_path',
|
||||
# 明细字段
|
||||
'sku': 'outbound_list:sku',
|
||||
'name': 'outbound_list:name',
|
||||
'material_type': 'outbound_list:material_type',
|
||||
'category': 'outbound_list:category',
|
||||
'spec_model': 'outbound_list:spec_model',
|
||||
'quantity': 'outbound_list:quantity',
|
||||
'unit_price': 'outbound_list:unit_price',
|
||||
'subtotal': 'outbound_list:subtotal',
|
||||
}
|
||||
# 如果用户是超级管理员且有 'outbound_list:*',则不过滤
|
||||
if 'outbound_list:*' in user_permissions:
|
||||
return item_dict
|
||||
for field, perm_code in field_to_perm.items():
|
||||
if field in item_dict and perm_code not in user_permissions:
|
||||
item_dict[field] = None
|
||||
# 如果 item_dict 中包含 items 列表,递归处理每个子项
|
||||
if 'items' in item_dict and isinstance(item_dict['items'], list):
|
||||
for sub_item in item_dict['items']:
|
||||
filter_item_by_permissions(sub_item, user_permissions)
|
||||
return item_dict
|
||||
|
||||
|
||||
# --------------------------------------------------------
|
||||
# 1. 扫码查询库存接口 (关联三个库存表)
|
||||
# GET /api/v1/outbound/scan?barcode=...
|
||||
# --------------------------------------------------------
|
||||
@outbound_bp.route('/scan', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('outbound_selection')
|
||||
def scan_barcode():
|
||||
barcode = request.args.get('barcode')
|
||||
if not barcode:
|
||||
@ -45,6 +108,19 @@ def scan_barcode():
|
||||
@outbound_bp.route('', methods=['POST'])
|
||||
@jwt_required()
|
||||
def create_outbound():
|
||||
# 权限检查:需要 outbound_create:operation 或 outbound_selection:operation 之一
|
||||
claims = get_jwt()
|
||||
user_role = claims.get('role')
|
||||
if not user_role:
|
||||
return jsonify({'code': 403, 'msg': '未授权'}), 403
|
||||
|
||||
# 超级管理员直接放行
|
||||
if user_role.upper() != 'SUPER_ADMIN':
|
||||
perm_dict = AuthService.get_user_permissions(user_role)
|
||||
perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
|
||||
if ('outbound_create:operation' not in perms) and ('outbound_selection:operation' not in perms):
|
||||
return jsonify({'code': 403, 'msg': '权限不足'}), 403
|
||||
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({'code': 400, 'msg': '无有效数据'}), 400
|
||||
@ -67,6 +143,44 @@ def create_outbound():
|
||||
if not data.get('consumer_name') or not data.get('signature_path'):
|
||||
return jsonify({'code': 400, 'msg': '领用人及签名信息缺失'}), 400
|
||||
|
||||
# 数据清洗:移除用户没有权限的字段
|
||||
user_permissions = get_current_user_permissions()
|
||||
# 超级管理员不过滤
|
||||
if 'outbound_list:*' not in user_permissions:
|
||||
# 字段名到权限码的映射(与前端 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',
|
||||
'price': 'outbound_list:unit_price', # 兼容 price 字段
|
||||
'subtotal': 'outbound_list:subtotal',
|
||||
}
|
||||
# 清洗顶层字段
|
||||
for field in list(data.keys()):
|
||||
perm_code = field_to_perm.get(field)
|
||||
if perm_code and perm_code not in user_permissions:
|
||||
data.pop(field, None)
|
||||
# 清洗 items 中的每个商品字段
|
||||
if 'items' in data and isinstance(data['items'], list):
|
||||
for item in data['items']:
|
||||
for field in list(item.keys()):
|
||||
perm_code = field_to_perm.get(field)
|
||||
if perm_code and perm_code not in user_permissions:
|
||||
item.pop(field, None)
|
||||
|
||||
try:
|
||||
# ★ [修改] 调用批量创建服务
|
||||
outbound_no = OutboundService.create_outbound_batch(data, operator_name=final_operator)
|
||||
@ -89,6 +203,7 @@ def create_outbound():
|
||||
# --------------------------------------------------------
|
||||
@outbound_bp.route('', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('outbound_list')
|
||||
def get_outbound_list():
|
||||
try:
|
||||
page = int(request.args.get('page', 1))
|
||||
@ -99,6 +214,11 @@ def get_outbound_list():
|
||||
# ★ [修改] 调用分组查询服务
|
||||
result = OutboundService.get_grouped_list(page, limit, keyword)
|
||||
|
||||
# 字段级脱敏
|
||||
user_permissions = get_current_user_permissions()
|
||||
if result.get('items'):
|
||||
result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']]
|
||||
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': '获取成功',
|
||||
@ -106,4 +226,4 @@ def get_outbound_list():
|
||||
})
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return jsonify({'code': 500, 'msg': str(e)}), 500
|
||||
return jsonify({'code': 500, 'msg': str(e)}), 500
|
||||
|
||||
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,16 +1,86 @@
|
||||
from flask import Blueprint, jsonify, request # .material -> .base refactor checked
|
||||
from flask_jwt_extended import jwt_required, get_jwt_identity
|
||||
from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt
|
||||
from app.utils.decorators import permission_required
|
||||
from app.services.auth_service import AuthService
|
||||
from app.services.trans_service import TransService
|
||||
import traceback
|
||||
|
||||
trans_bp = Blueprint('transactions', __name__, url_prefix='/transactions')
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 辅助函数:获取当前用户的完整权限列表(基于角色查询)
|
||||
# ==============================================================================
|
||||
def get_current_user_permissions():
|
||||
"""
|
||||
返回当前用户拥有的所有权限码列表(包括菜单和元素)
|
||||
此函数根据角色查询数据库得到权限。
|
||||
"""
|
||||
claims = get_jwt()
|
||||
user_role = claims.get('role')
|
||||
if not user_role:
|
||||
return []
|
||||
# 超级管理员返回所有字段权限 (忽略大小写)
|
||||
if user_role.upper() == 'SUPER_ADMIN':
|
||||
return ['*']
|
||||
perm_dict = AuthService.get_user_permissions(user_role)
|
||||
# 合并菜单和元素权限
|
||||
perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
|
||||
return perms
|
||||
|
||||
|
||||
def filter_item_by_permissions(item_dict, user_permissions, prefix='op_records'):
|
||||
"""
|
||||
根据用户权限过滤 item 字典,无权限的字段值置为 None
|
||||
"""
|
||||
# 字段名到权限码的映射(与前端 permissionMap 保持一致)
|
||||
field_to_perm = {
|
||||
'borrow_no': f'{prefix}:borrow_no',
|
||||
'borrower_name': f'{prefix}:borrower_name',
|
||||
'sku': f'{prefix}:sku',
|
||||
'borrow_time': f'{prefix}:borrow_time',
|
||||
'return_time': f'{prefix}:return_time',
|
||||
'status': f'{prefix}:status',
|
||||
'expected_return_time': f'{prefix}:expected_return_time',
|
||||
'return_location': f'{prefix}:return_location',
|
||||
'borrow_signature': f'{prefix}:borrow_signature',
|
||||
'return_signature': f'{prefix}:return_signature',
|
||||
}
|
||||
# 如果用户是超级管理员且有 '*',则不过滤
|
||||
if '*' in user_permissions:
|
||||
return item_dict
|
||||
for field, perm_code in field_to_perm.items():
|
||||
if field in item_dict and perm_code not in user_permissions:
|
||||
item_dict[field] = None
|
||||
return item_dict
|
||||
|
||||
|
||||
# --- 借库接口 ---
|
||||
@trans_bp.route('/borrow', methods=['POST'])
|
||||
@jwt_required()
|
||||
@permission_required('op_borrow:operation')
|
||||
def create_borrow():
|
||||
data = request.get_json()
|
||||
# 数据清洗:移除用户没有权限的字段
|
||||
user_permissions = get_current_user_permissions()
|
||||
# 超级管理员不过滤
|
||||
if '*' not in user_permissions:
|
||||
field_to_perm = {
|
||||
'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',
|
||||
}
|
||||
for field in list(data.keys()):
|
||||
perm_code = field_to_perm.get(field)
|
||||
if perm_code and perm_code not in user_permissions:
|
||||
data.pop(field, None)
|
||||
try:
|
||||
no = TransService.create_borrow(data)
|
||||
return jsonify({'code': 200, 'msg': '借用成功', 'data': {'borrow_no': no}})
|
||||
@ -21,6 +91,7 @@ def create_borrow():
|
||||
# --- 还库辅助:扫码查找借出记录 ---
|
||||
@trans_bp.route('/return/scan', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('op_return')
|
||||
def scan_borrowed_item():
|
||||
barcode = request.args.get('barcode')
|
||||
if not barcode:
|
||||
@ -36,8 +107,29 @@ def scan_borrowed_item():
|
||||
# --- 还库提交 ---
|
||||
@trans_bp.route('/return', methods=['POST'])
|
||||
@jwt_required()
|
||||
@permission_required('op_return:operation')
|
||||
def submit_return():
|
||||
data = request.get_json()
|
||||
# 数据清洗:移除用户没有权限的字段
|
||||
user_permissions = get_current_user_permissions()
|
||||
# 超级管理员不过滤
|
||||
if '*' not in user_permissions:
|
||||
field_to_perm = {
|
||||
'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',
|
||||
}
|
||||
for field in list(data.keys()):
|
||||
perm_code = field_to_perm.get(field)
|
||||
if perm_code and perm_code not in user_permissions:
|
||||
data.pop(field, None)
|
||||
user = get_jwt_identity() # 库管
|
||||
try:
|
||||
TransService.process_return(data, operator_name=user)
|
||||
@ -49,10 +141,15 @@ def submit_return():
|
||||
# --- 记录列表 ---
|
||||
@trans_bp.route('/records', methods=['GET'])
|
||||
@jwt_required()
|
||||
@permission_required('op_records')
|
||||
def get_records():
|
||||
status = request.args.get('status', 'all')
|
||||
page = int(request.args.get('page', 1))
|
||||
keyword = request.args.get('keyword', '')
|
||||
|
||||
res = TransService.get_records(page=page, limit=10, status=status, keyword=keyword)
|
||||
# 字段级脱敏
|
||||
user_permissions = get_current_user_permissions()
|
||||
if res.get('items'):
|
||||
res['items'] = [filter_item_by_permissions(item, user_permissions, 'op_records') for item in res['items']]
|
||||
return jsonify({'code': 200, 'data': res})
|
||||
|
||||
@ -33,7 +33,8 @@ class StockBuy(db.Model):
|
||||
available_quantity = db.Column(db.Numeric(19, 4), default=0)
|
||||
|
||||
# 财务与商务
|
||||
unit_price = db.Column(db.Numeric(19, 4), default=0) # 现意为:不含税单价
|
||||
pre_tax_unit_price = db.Column(db.Numeric(19, 4), default=0) # 现意为:不含税单价
|
||||
post_tax_unit_price = db.Column(db.Numeric(19, 4), default=0) # 税后单价
|
||||
total_price = db.Column(db.Numeric(19, 4), default=0) # 总价
|
||||
# [新增] 税率
|
||||
tax_rate = db.Column(db.Numeric(5, 2), default=0)
|
||||
@ -97,7 +98,8 @@ class StockBuy(db.Model):
|
||||
'available_quantity': float(self.available_quantity or 0),
|
||||
'qty_available': float(self.available_quantity or 0),
|
||||
|
||||
'unit_price': float(self.unit_price or 0),
|
||||
'unit_price': float(self.pre_tax_unit_price or 0),
|
||||
'post_tax_unit_price': float(self.post_tax_unit_price or 0),
|
||||
'total_price': float(self.total_price or 0),
|
||||
# [新增] 税率
|
||||
'tax_rate': float(self.tax_rate or 0),
|
||||
@ -116,4 +118,4 @@ class StockBuy(db.Model):
|
||||
|
||||
'global_print_id': self.global_print_id,
|
||||
'global_print_id_str': f"{self.global_print_id:010d}" if self.global_print_id else ""
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
# app/models/system.py
|
||||
# inventory-backend/app/models/system.py
|
||||
from app.extensions import db
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 1. 系统用户表
|
||||
# ==========================================
|
||||
class SysUser(db.Model):
|
||||
"""
|
||||
系统用户表
|
||||
对应数据库: sys_user
|
||||
username 字段存储格式约定: "真实姓名/登录账号" (例如: 张三/zhangsan)
|
||||
username 字段存储格式约定: "真实姓名/登录账号" (例如: 张三/zhangsan01)
|
||||
"""
|
||||
__tablename__ = 'sys_user'
|
||||
|
||||
@ -19,8 +22,7 @@ class SysUser(db.Model):
|
||||
role = db.Column(db.String(50))
|
||||
status = db.Column(db.String(20), default='active')
|
||||
password_hash = db.Column(db.Text)
|
||||
|
||||
# created_at 已在数据库脚本中移除,此处不再定义
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
|
||||
def set_password(self, password):
|
||||
"""生成加密密码"""
|
||||
@ -45,23 +47,27 @@ class SysUser(db.Model):
|
||||
parts = raw_name.split('/')
|
||||
real_name = parts[0]
|
||||
acc_id = parts[1]
|
||||
# 格式化为前端展示格式: 张三(zhangsan)
|
||||
# 格式化为前端展示格式: 张三(zhangsan01)
|
||||
display_name = f"{real_name}({acc_id})"
|
||||
# 单独提取账号ID (如果前端需要单独用)
|
||||
account_id = acc_id
|
||||
|
||||
return {
|
||||
'id': self.id,
|
||||
'username': display_name, # 列表显示: 张三(zhangsan)
|
||||
'username': display_name, # 列表显示: 张三(zhangsan01)
|
||||
'raw_username': self.username, # 原始数据
|
||||
'account_id': account_id, # 纯账号ID: zhangsan
|
||||
'account_id': account_id, # 纯账号ID: zhangsan01
|
||||
'email': self.email,
|
||||
'department': self.department,
|
||||
'role': self.role,
|
||||
'status': self.status
|
||||
'status': self.status,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 2. 系统日志表
|
||||
# ==========================================
|
||||
class SysLog(db.Model):
|
||||
"""
|
||||
系统操作日志表
|
||||
@ -88,4 +94,58 @@ class SysLog(db.Model):
|
||||
'module_name': self.module_name,
|
||||
'action_type': self.action_type,
|
||||
'description': self.description
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 3. 权限管理模型 (RBAC) - [新增]
|
||||
# ==========================================
|
||||
|
||||
class SysMenu(db.Model):
|
||||
"""系统菜单/页面表"""
|
||||
__tablename__ = 'sys_menu'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
parent_id = db.Column(db.Integer, default=0)
|
||||
name = db.Column(db.String(50), nullable=False)
|
||||
code = db.Column(db.String(100), unique=True, nullable=False)
|
||||
path = db.Column(db.String(200))
|
||||
sort_order = db.Column(db.Integer, default=0)
|
||||
is_visible = db.Column(db.Boolean, default=True)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'code': self.code,
|
||||
'path': self.path,
|
||||
'type': 'menu' # 前端树形控件图标判断用
|
||||
}
|
||||
|
||||
|
||||
class SysElement(db.Model):
|
||||
"""页面元素/列定义表"""
|
||||
__tablename__ = 'sys_element'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
menu_code = db.Column(db.String(100), db.ForeignKey('sys_menu.code'))
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
code = db.Column(db.String(100), nullable=False) # 如: unit_price
|
||||
element_type = db.Column(db.String(20), default='column')
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'code': self.code,
|
||||
'menu_code': self.menu_code,
|
||||
'type': 'element',
|
||||
'element_type': self.element_type
|
||||
}
|
||||
|
||||
|
||||
class SysRolePermission(db.Model):
|
||||
"""角色权限关联表"""
|
||||
__tablename__ = 'sys_role_permission'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
role_code = db.Column(db.String(50), nullable=False)
|
||||
target_code = db.Column(db.String(100), nullable=False) # menu_code 或 element_code
|
||||
type = db.Column(db.String(20), nullable=False) # 'menu' 或 'element'
|
||||
@ -1,5 +1,6 @@
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
from sqlalchemy import func
|
||||
|
||||
|
||||
class TransBorrow(db.Model):
|
||||
@ -46,6 +47,19 @@ class TransBorrow(db.Model):
|
||||
'remark': self.remark,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_borrowed_quantity(cls, source_table, stock_id):
|
||||
"""
|
||||
获取指定库存记录(source_table 和 stock_id)的借出未还数量总和。
|
||||
返回浮点数,若无借出记录则返回 0.0。
|
||||
"""
|
||||
result = db.session.query(func.sum(cls.quantity)).filter(
|
||||
cls.source_table == source_table,
|
||||
cls.stock_id == stock_id,
|
||||
cls.is_returned == False
|
||||
).scalar()
|
||||
return float(result) if result is not None else 0.0
|
||||
|
||||
|
||||
class TransRepair(db.Model):
|
||||
__tablename__ = 'trans_repair'
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
# 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 sqlalchemy import func
|
||||
from flask_jwt_extended import create_access_token
|
||||
from app.utils.constants import UserRole
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class AuthService:
|
||||
# 硬编码的超级管理员凭证
|
||||
SUPER_ADMIN_USER = "IRIS"
|
||||
@ -52,9 +52,10 @@ class AuthService:
|
||||
if user.status != 'active':
|
||||
raise ValueError("账号已被禁用,请联系管理员")
|
||||
|
||||
user_role = user.role
|
||||
user_role = user.role.upper() if user.role else None
|
||||
user_id = user.id
|
||||
user_info = user.to_dict()
|
||||
user_info['role'] = user_role
|
||||
|
||||
# 3. 生成 Token
|
||||
# Token 中 identity 存数据库ID,claims 存登录账号ID
|
||||
@ -81,7 +82,9 @@ class AuthService:
|
||||
创建新用户
|
||||
data 包含: cn_name(张三), username(zhangsan), ...
|
||||
"""
|
||||
if operator_role not in [UserRole.SUPER_ADMIN, UserRole.SUPERVISOR]:
|
||||
# 标准化操作者角色为全大写
|
||||
operator_role_upper = operator_role.upper() if operator_role else None
|
||||
if operator_role_upper not in [UserRole.SUPER_ADMIN, UserRole.SUPERVISOR]:
|
||||
raise Exception("权限不足:只有超级管理员或主管可以创建新用户")
|
||||
|
||||
cn_name = data.get('cn_name')
|
||||
@ -90,7 +93,8 @@ class AuthService:
|
||||
if not cn_name or not pinyin_base:
|
||||
raise Exception("姓名和账号不能为空")
|
||||
|
||||
role = data.get('role')
|
||||
role_raw = data.get('role')
|
||||
role = role_raw.upper() if role_raw else None
|
||||
|
||||
# 验证角色合法性
|
||||
valid_roles = [
|
||||
@ -101,7 +105,7 @@ class AuthService:
|
||||
if role not in valid_roles:
|
||||
raise Exception(f"角色无效")
|
||||
|
||||
if operator_role == UserRole.SUPERVISOR and role == UserRole.SUPER_ADMIN:
|
||||
if operator_role_upper == UserRole.SUPERVISOR and role == UserRole.SUPER_ADMIN:
|
||||
raise Exception("权限不足:主管无法创建超级管理员")
|
||||
|
||||
email = data.get('email', '')
|
||||
@ -150,7 +154,9 @@ class AuthService:
|
||||
更新用户信息
|
||||
注意: 这里暂时不允许修改用户名/账号,因为涉及 split 逻辑较复杂,且通常账号不开通后不改
|
||||
"""
|
||||
if operator_role not in [UserRole.SUPER_ADMIN, UserRole.SUPERVISOR]:
|
||||
# 标准化操作者角色为全大写
|
||||
operator_role_upper = operator_role.upper() if operator_role else None
|
||||
if operator_role_upper not in [UserRole.SUPER_ADMIN, UserRole.SUPERVISOR]:
|
||||
raise Exception("权限不足")
|
||||
|
||||
user = SysUser.query.get(user_id)
|
||||
@ -163,10 +169,11 @@ class AuthService:
|
||||
v for k, v in UserRole.__dict__.items()
|
||||
if not k.startswith('__') and isinstance(v, str)
|
||||
]
|
||||
new_role = data['role']
|
||||
new_role_raw = data['role']
|
||||
new_role = new_role_raw.upper() if new_role_raw else None
|
||||
if new_role not in valid_roles:
|
||||
raise Exception(f"角色无效")
|
||||
if operator_role == UserRole.SUPERVISOR and new_role == UserRole.SUPER_ADMIN:
|
||||
if operator_role_upper == UserRole.SUPERVISOR and new_role == UserRole.SUPER_ADMIN:
|
||||
raise Exception("权限不足")
|
||||
user.role = new_role
|
||||
|
||||
@ -202,7 +209,9 @@ class AuthService:
|
||||
@staticmethod
|
||||
def delete_user(user_id, operator_role):
|
||||
"""删除用户"""
|
||||
if operator_role != UserRole.SUPER_ADMIN:
|
||||
# 标准化操作者角色为全大写
|
||||
operator_role_upper = operator_role.upper() if operator_role else None
|
||||
if operator_role_upper != UserRole.SUPER_ADMIN:
|
||||
raise Exception("权限不足:只有超级管理员可以删除用户")
|
||||
|
||||
user = SysUser.query.get(user_id)
|
||||
@ -211,4 +220,46 @@ class AuthService:
|
||||
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
return True
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_user_permissions(role_code):
|
||||
"""
|
||||
获取指定角色的所有权限代码列表
|
||||
返回格式: {
|
||||
'menus': ['inbound_buy', 'system_user'],
|
||||
'elements': ['inbound_buy:unit_price', ...]
|
||||
}
|
||||
"""
|
||||
# 超级管理员返回所有权限(通配符)
|
||||
from app.utils.constants import UserRole
|
||||
if role_code and role_code.upper() == UserRole.SUPER_ADMIN:
|
||||
# 返回通配符,表示拥有所有菜单和元素权限
|
||||
return {
|
||||
'menus': ['*'],
|
||||
'elements': ['*']
|
||||
}
|
||||
|
||||
# 1. 查菜单权限
|
||||
menu_perms = SysRolePermission.query.filter(
|
||||
func.upper(SysRolePermission.role_code) == role_code.upper(),
|
||||
SysRolePermission.type == 'menu'
|
||||
).all()
|
||||
menu_codes = [p.target_code for p in menu_perms]
|
||||
|
||||
# 2. 查元素(列)权限
|
||||
# 注意:这里我们只返回用户拥有的。前端逻辑是:"如果列配置了Key且用户没这个Key,则隐藏"
|
||||
element_perms = SysRolePermission.query.filter(
|
||||
func.upper(SysRolePermission.role_code) == role_code.upper(),
|
||||
SysRolePermission.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
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ from app.models.inbound.buy import StockBuy
|
||||
from app.models.inbound.semi import StockSemi
|
||||
from app.models.inbound.product import StockProduct
|
||||
# from app.models.inbound.service import StockService
|
||||
from sqlalchemy import or_, and_
|
||||
from sqlalchemy import or_, and_, func
|
||||
import traceback
|
||||
import json
|
||||
import io
|
||||
@ -14,6 +14,7 @@ import datetime
|
||||
# 需要 pip install openpyxl
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
class MaterialBaseService:
|
||||
@ -114,7 +115,41 @@ class MaterialBaseService:
|
||||
获取基础信息列表 (带分页和筛选)
|
||||
"""
|
||||
try:
|
||||
query = MaterialBase.query
|
||||
# 构建聚合子查询
|
||||
buy_sub = db.session.query(
|
||||
StockBuy.base_id,
|
||||
func.sum(StockBuy.stock_quantity).label('buy_inv'),
|
||||
func.sum(StockBuy.available_quantity).label('buy_avail')
|
||||
).group_by(StockBuy.base_id).subquery()
|
||||
|
||||
semi_sub = db.session.query(
|
||||
StockSemi.base_id,
|
||||
func.sum(StockSemi.stock_quantity).label('semi_inv'),
|
||||
func.sum(StockSemi.available_quantity).label('semi_avail')
|
||||
).group_by(StockSemi.base_id).subquery()
|
||||
|
||||
prod_sub = db.session.query(
|
||||
StockProduct.base_id,
|
||||
func.sum(StockProduct.stock_quantity).label('prod_inv'),
|
||||
func.sum(StockProduct.available_quantity).label('prod_avail')
|
||||
).group_by(StockProduct.base_id).subquery()
|
||||
|
||||
# 总库存和可用数的 SQL 表达式
|
||||
total_inv = func.coalesce(buy_sub.c.buy_inv, 0) + \
|
||||
func.coalesce(semi_sub.c.semi_inv, 0) + \
|
||||
func.coalesce(prod_sub.c.prod_inv, 0)
|
||||
total_avail = func.coalesce(buy_sub.c.buy_avail, 0) + \
|
||||
func.coalesce(semi_sub.c.semi_avail, 0) + \
|
||||
func.coalesce(prod_sub.c.prod_avail, 0)
|
||||
|
||||
# 主查询,关联聚合子查询
|
||||
query = db.session.query(
|
||||
MaterialBase,
|
||||
total_inv.label('total_inv'),
|
||||
total_avail.label('total_avail')
|
||||
).outerjoin(buy_sub, MaterialBase.id == buy_sub.c.base_id) \
|
||||
.outerjoin(semi_sub, MaterialBase.id == semi_sub.c.base_id) \
|
||||
.outerjoin(prod_sub, MaterialBase.id == prod_sub.c.base_id)
|
||||
|
||||
if filters:
|
||||
# 1. 关键词模糊搜索
|
||||
@ -127,37 +162,47 @@ class MaterialBaseService:
|
||||
))
|
||||
|
||||
# 2. 精确筛选
|
||||
# 公司筛选
|
||||
if filters.get('company'):
|
||||
query = query.filter_by(company_name=filters['company'])
|
||||
company = filters.get('company')
|
||||
if company is not None and company != '':
|
||||
query = query.filter(MaterialBase.company_name.ilike(company.strip()))
|
||||
|
||||
if filters.get('category'):
|
||||
query = query.filter_by(category=filters['category'])
|
||||
category = filters.get('category')
|
||||
if category is not None and category != '':
|
||||
query = query.filter(MaterialBase.category.ilike(category.strip()))
|
||||
|
||||
if filters.get('type'):
|
||||
query = query.filter_by(material_type=filters['type'])
|
||||
type_val = filters.get('type')
|
||||
if type_val is not None and type_val != '':
|
||||
query = query.filter(MaterialBase.material_type.ilike(type_val.strip()))
|
||||
|
||||
if filters.get('isEnabled') is not None:
|
||||
is_active = bool(int(filters['isEnabled']))
|
||||
query = query.filter_by(is_enabled=is_active)
|
||||
|
||||
# [修改3] 默认排序方式改为按 spec_model 排序
|
||||
pagination = query.order_by(MaterialBase.spec_model.asc()).paginate(page=page, per_page=limit,
|
||||
error_out=False)
|
||||
# 排序处理
|
||||
order_by_column = filters.get('orderByColumn', '')
|
||||
is_asc = filters.get('isAsc', None)
|
||||
if order_by_column == 'inventoryCount':
|
||||
if is_asc == 'asc':
|
||||
query = query.order_by(total_inv.asc())
|
||||
else:
|
||||
query = query.order_by(total_inv.desc())
|
||||
elif order_by_column == 'availableCount':
|
||||
if is_asc == 'asc':
|
||||
query = query.order_by(total_avail.asc())
|
||||
else:
|
||||
query = query.order_by(total_avail.desc())
|
||||
else:
|
||||
# 默认排序:优先按总库存数降序,当库存相同时,再按规格型号升序
|
||||
query = query.order_by(total_inv.desc(), MaterialBase.spec_model.asc())
|
||||
|
||||
# 分页
|
||||
pagination = query.paginate(page=page, per_page=limit, error_out=False)
|
||||
|
||||
items_list = []
|
||||
for item in pagination.items:
|
||||
for item, inv, avail in pagination.items:
|
||||
item_dict = item.to_dict()
|
||||
|
||||
# 聚合库存
|
||||
buy_inv, buy_avail = MaterialBaseService._get_stock_counts(item.stock_buys)
|
||||
semi_inv, semi_avail = MaterialBaseService._get_stock_counts(item.stock_semis)
|
||||
prod_inv, prod_avail = MaterialBaseService._get_stock_counts(item.stock_products)
|
||||
serv_inv, serv_avail = MaterialBaseService._get_stock_counts(getattr(item, 'stock_services', []))
|
||||
|
||||
item_dict['inventoryCount'] = buy_inv + semi_inv + prod_inv + serv_inv
|
||||
item_dict['availableCount'] = buy_avail + semi_avail + prod_avail + serv_avail
|
||||
|
||||
item_dict['inventoryCount'] = float(inv) if inv is not None else 0.0
|
||||
item_dict['availableCount'] = float(avail) if avail is not None else 0.0
|
||||
items_list.append(item_dict)
|
||||
|
||||
return {"total": pagination.total, "items": items_list}
|
||||
@ -217,7 +262,6 @@ class MaterialBaseService:
|
||||
raise ValueError(f"已存在相同名称和规格的数据 (ID: {exist.id})")
|
||||
|
||||
new_material = MaterialBase(
|
||||
# [修改] 移除了 'IRIS' 默认值
|
||||
company_name=data.get('companyName'),
|
||||
name=data['name'],
|
||||
common_name=data.get('commonName'),
|
||||
@ -307,13 +351,13 @@ class MaterialBaseService:
|
||||
raise e
|
||||
|
||||
# ==============================================================================
|
||||
# [核心修改] 统一资产统计导出
|
||||
# [核心修改] 统一资产统计导出(增加最高单价计算逻辑)
|
||||
# ==============================================================================
|
||||
@staticmethod
|
||||
def export_excel(filters=None):
|
||||
def export_excel(filters=None, user_permissions=None):
|
||||
"""
|
||||
全口径资产统计报表:
|
||||
逻辑:先查库存表 (Buy, Semi, Product),关联基础信息,只导出实际存在的库存记录。
|
||||
根据基础信息列表和库存表,计算出每个物料的最高历史单价并进行导出
|
||||
"""
|
||||
try:
|
||||
# 1. 构造基础信息的筛选条件 (用于过滤库存)
|
||||
@ -327,12 +371,15 @@ class MaterialBaseService:
|
||||
MaterialBase.spec_model.ilike(kw),
|
||||
MaterialBase.company_name.ilike(kw)
|
||||
))
|
||||
if filters.get('company'):
|
||||
filter_conditions.append(MaterialBase.company_name == filters['company'])
|
||||
if filters.get('category'):
|
||||
filter_conditions.append(MaterialBase.category == filters['category'])
|
||||
if filters.get('type'):
|
||||
filter_conditions.append(MaterialBase.material_type == filters['type'])
|
||||
company = filters.get('company')
|
||||
if company is not None and company != '':
|
||||
filter_conditions.append(MaterialBase.company_name.ilike(company.strip()))
|
||||
category = filters.get('category')
|
||||
if category is not None and category != '':
|
||||
filter_conditions.append(MaterialBase.category.ilike(category.strip()))
|
||||
type_val = filters.get('type')
|
||||
if type_val is not None and type_val != '':
|
||||
filter_conditions.append(MaterialBase.material_type.ilike(type_val.strip()))
|
||||
if filters.get('isEnabled') is not None:
|
||||
is_active = bool(int(filters['isEnabled']))
|
||||
filter_conditions.append(MaterialBase.is_enabled == is_active)
|
||||
@ -362,21 +409,56 @@ class MaterialBaseService:
|
||||
query_product = query_product.filter(cond)
|
||||
list_product = query_product.all()
|
||||
|
||||
# ====================================================
|
||||
# [核心新增] 预先计算每个 base_id 的全局最高历史单价
|
||||
# 优先级:采购件 > 半成品 > 成品
|
||||
# ====================================================
|
||||
buy_max_prices = {}
|
||||
for stock, base in list_buy:
|
||||
price = float(stock.pre_tax_unit_price or 0)
|
||||
if price > buy_max_prices.get(base.id, 0):
|
||||
buy_max_prices[base.id] = price
|
||||
|
||||
semi_max_prices = {}
|
||||
for stock, base in list_semi:
|
||||
# 半成品的单价直接取自 manual_cost 字段(单件总成本)
|
||||
price = float(stock.manual_cost or 0)
|
||||
|
||||
if price > semi_max_prices.get(base.id, 0):
|
||||
semi_max_prices[base.id] = price
|
||||
|
||||
product_max_prices = {}
|
||||
for stock, base in list_product:
|
||||
# 成品的单价直接取自 manual_cost 字段(单件总成本)
|
||||
price = float(stock.manual_cost or 0)
|
||||
|
||||
if price > product_max_prices.get(base.id, 0):
|
||||
product_max_prices[base.id] = price
|
||||
|
||||
# 构造获取某个物料最高价的闭包函数
|
||||
def get_highest_price(base_id):
|
||||
if base_id in buy_max_prices and buy_max_prices[base_id] > 0:
|
||||
return buy_max_prices[base_id]
|
||||
if base_id in semi_max_prices and semi_max_prices[base_id] > 0:
|
||||
return semi_max_prices[base_id]
|
||||
if base_id in product_max_prices and product_max_prices[base_id] > 0:
|
||||
return product_max_prices[base_id]
|
||||
return 0.0
|
||||
|
||||
# 3. 数据整合
|
||||
all_rows = []
|
||||
|
||||
# 处理采购件
|
||||
for stock, base in list_buy:
|
||||
# 价格计算
|
||||
unit_price = float(stock.unit_price or 0)
|
||||
tax_rate = float(stock.tax_rate or 0)
|
||||
price_incl = unit_price * (1 + tax_rate / 100.0)
|
||||
qty = float(stock.stock_quantity or 0)
|
||||
# 使用该物料的全局最高单价作为不含税单价
|
||||
highest_excl_price = get_highest_price(base.id)
|
||||
tax_rate = float(stock.tax_rate or 0)
|
||||
|
||||
# 计算不含税总价 = 数量 * 不含税单价
|
||||
total_val_excl = qty * unit_price
|
||||
# 计算含税总价 = 数量 * 含税单价
|
||||
total_val_incl = qty * price_incl
|
||||
# 计算含税单价和总额
|
||||
highest_incl_price = highest_excl_price * (1 + tax_rate / 100.0)
|
||||
total_val_excl = qty * highest_excl_price
|
||||
total_val_incl = qty * highest_incl_price
|
||||
|
||||
ident = stock.batch_number or stock.serial_number or stock.barcode or stock.sku
|
||||
|
||||
@ -389,22 +471,21 @@ class MaterialBaseService:
|
||||
"date": stock.in_date,
|
||||
"qty": qty,
|
||||
"avail": float(stock.available_quantity or 0),
|
||||
"price_excl": unit_price,
|
||||
"total_val_excl": total_val_excl, # [新增]
|
||||
"price_excl": highest_excl_price,
|
||||
"total_val_excl": total_val_excl,
|
||||
"tax": tax_rate,
|
||||
"price_incl": price_incl,
|
||||
"price_incl": highest_incl_price,
|
||||
"total_val": total_val_incl
|
||||
})
|
||||
|
||||
# 处理半成品
|
||||
for stock, base in list_semi:
|
||||
cost = float(stock.raw_material_cost or 0) + float(stock.manual_cost or 0)
|
||||
qty = float(stock.stock_quantity or 0)
|
||||
# 半成品的单价直接取自 manual_cost 字段(单件总成本)
|
||||
unit_cost = float(stock.manual_cost or 0)
|
||||
|
||||
# 半成品不含税总价 = 数量 * 成本
|
||||
total_val_excl = qty * cost
|
||||
# 含税总价同上 (税率0)
|
||||
total_val_incl = qty * cost
|
||||
total_val_excl = qty * unit_cost
|
||||
total_val_incl = qty * unit_cost # 半成品无税
|
||||
|
||||
ident = stock.batch_number or stock.serial_number or stock.barcode or stock.sku
|
||||
|
||||
@ -417,20 +498,21 @@ class MaterialBaseService:
|
||||
"date": stock.production_date,
|
||||
"qty": qty,
|
||||
"avail": float(stock.available_quantity or 0),
|
||||
"price_excl": cost,
|
||||
"total_val_excl": total_val_excl, # [新增]
|
||||
"price_excl": unit_cost,
|
||||
"total_val_excl": total_val_excl,
|
||||
"tax": 0.0,
|
||||
"price_incl": cost,
|
||||
"price_incl": unit_cost,
|
||||
"total_val": total_val_incl
|
||||
})
|
||||
|
||||
# 处理成品
|
||||
for stock, base in list_product:
|
||||
cost = float(stock.raw_material_cost or 0) + float(stock.manual_cost or 0)
|
||||
qty = float(stock.stock_quantity or 0)
|
||||
# 成品的单价直接取自 manual_cost 字段(单件总成本)
|
||||
unit_cost = float(stock.manual_cost or 0)
|
||||
|
||||
total_val_excl = qty * cost
|
||||
total_val_incl = qty * cost
|
||||
total_val_excl = qty * unit_cost
|
||||
total_val_incl = qty * unit_cost
|
||||
|
||||
ident = stock.serial_number or stock.barcode or stock.sku
|
||||
|
||||
@ -443,10 +525,10 @@ class MaterialBaseService:
|
||||
"date": stock.production_date,
|
||||
"qty": qty,
|
||||
"avail": float(stock.available_quantity or 0),
|
||||
"price_excl": cost,
|
||||
"total_val_excl": total_val_excl, # [新增]
|
||||
"price_excl": unit_cost,
|
||||
"total_val_excl": total_val_excl,
|
||||
"tax": 0.0,
|
||||
"price_incl": cost,
|
||||
"price_incl": unit_cost,
|
||||
"total_val": total_val_incl
|
||||
})
|
||||
|
||||
@ -463,7 +545,7 @@ class MaterialBaseService:
|
||||
ws = wb.active
|
||||
ws.title = "库存统计"
|
||||
|
||||
# 表头 [修改] 增加 "资产总额 (不含税)"
|
||||
# 表头 (严格对应你的图 5)
|
||||
headers = [
|
||||
"所属公司", "资产名称", "规格型号", "物料类型",
|
||||
"类别一级", "类别二级", "类别三级", "类别四级", "类别五级",
|
||||
@ -475,6 +557,36 @@ class MaterialBaseService:
|
||||
]
|
||||
ws.append(headers)
|
||||
|
||||
# 确定各字段在表头中的列索引
|
||||
col_idx = {}
|
||||
for idx, header in enumerate(headers):
|
||||
if header == "所属公司":
|
||||
col_idx['companyName'] = idx
|
||||
elif header == "资产名称":
|
||||
col_idx['name'] = idx
|
||||
elif header == "规格型号":
|
||||
col_idx['spec'] = idx
|
||||
elif header == "物料类型":
|
||||
col_idx['type'] = idx
|
||||
elif header in ("类别一级", "类别二级", "类别三级", "类别四级", "类别五级"):
|
||||
col_idx.setdefault('category_cols', []).append(idx)
|
||||
elif header == "计量单位":
|
||||
col_idx['unit'] = idx
|
||||
elif header == "库存数量":
|
||||
col_idx['inventoryCount'] = idx
|
||||
elif header == "可用数量":
|
||||
col_idx['availableCount'] = idx
|
||||
elif header == "单价/成本 (不含税)":
|
||||
col_idx['price_excl'] = idx
|
||||
elif header == "资产总额 (不含税)":
|
||||
col_idx['total_val_excl'] = idx
|
||||
elif header == "税率 (%)":
|
||||
col_idx['tax'] = idx
|
||||
elif header == "单价/成本 (含税)":
|
||||
col_idx['price_incl'] = idx
|
||||
elif header == "资产总额 (含税)":
|
||||
col_idx['total_val'] = idx
|
||||
|
||||
# 样式
|
||||
header_fill = PatternFill(start_color="D7E4BC", end_color="D7E4BC", fill_type="solid")
|
||||
border_style = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'),
|
||||
@ -486,7 +598,19 @@ class MaterialBaseService:
|
||||
cell.fill = header_fill
|
||||
cell.border = border_style
|
||||
|
||||
# 写入数据
|
||||
# 字段到权限码的映射
|
||||
field_to_perm = {
|
||||
'companyName': 'material_list:companyName',
|
||||
'name': 'material_list:name',
|
||||
'spec': 'material_list:spec',
|
||||
'type': 'material_list:type',
|
||||
'unit': 'material_list:unit',
|
||||
'category': 'material_list:category',
|
||||
'inventoryCount': 'material_list:inventoryCount',
|
||||
'availableCount': 'material_list:availableCount'
|
||||
}
|
||||
|
||||
# 写入数据,并脱敏
|
||||
for r in all_rows:
|
||||
base = r['base']
|
||||
# 类别拆分
|
||||
@ -512,11 +636,56 @@ class MaterialBaseService:
|
||||
r['qty'],
|
||||
r['avail'],
|
||||
r['price_excl'],
|
||||
r['total_val_excl'], # [新增] 对应列
|
||||
r['total_val_excl'],
|
||||
r['tax'],
|
||||
r['price_incl'],
|
||||
r['total_val']
|
||||
]
|
||||
|
||||
# 根据用户权限脱敏
|
||||
if user_permissions is not None:
|
||||
for field, perm_code in field_to_perm.items():
|
||||
if perm_code not in user_permissions:
|
||||
if field == 'category':
|
||||
for cat_idx in col_idx.get('category_cols', []):
|
||||
row_val[cat_idx] = ''
|
||||
elif field in col_idx:
|
||||
row_val[col_idx[field]] = ''
|
||||
|
||||
# 联动脱敏:根据数据来源,校验对应模块的价格/成本权限
|
||||
if user_permissions is not None:
|
||||
# 超级管理员拥有所有权限,跳过价格脱敏
|
||||
if 'material_list:*' in user_permissions:
|
||||
# 拥有通配符权限,不隐藏价格列
|
||||
pass
|
||||
else:
|
||||
has_price_perm = True
|
||||
row_type = r['type_name']
|
||||
|
||||
# 根据数据来源检查对应模块的权限
|
||||
if row_type == '采购件':
|
||||
# 校验采购模块的价格权限
|
||||
has_price_perm = any(p in user_permissions for p in
|
||||
['inbound_buy:postTaxUnitPrice', 'inbound_buy:preTaxUnitPrice',
|
||||
'inbound_buy:totalAmount'])
|
||||
elif row_type == '半成品':
|
||||
# 校验半成品模块的成本权限
|
||||
has_price_perm = any(p in user_permissions for p in
|
||||
['inbound_semi:rawMaterialCost', 'inbound_semi:manualCost'])
|
||||
elif row_type == '成品':
|
||||
# 校验成品模块的成本权限
|
||||
has_price_perm = any(p in user_permissions for p in
|
||||
['inbound_product:rawMaterialCost', 'inbound_product:manualCost'])
|
||||
else:
|
||||
# 未知类型,默认隐藏价格列
|
||||
has_price_perm = False
|
||||
|
||||
# 如果没有对应模块的价格查看权限,则清空涉密的5个列
|
||||
if not has_price_perm:
|
||||
for p_col in ['price_excl', 'total_val_excl', 'tax', 'price_incl', 'total_val']:
|
||||
if p_col in col_idx:
|
||||
row_val[col_idx[p_col]] = ''
|
||||
|
||||
ws.append(row_val)
|
||||
|
||||
# 列宽调整
|
||||
@ -535,4 +704,4 @@ class MaterialBaseService:
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
raise e
|
||||
raise e
|
||||
|
||||
@ -117,7 +117,13 @@ class BuyInboundService:
|
||||
|
||||
in_qty = float(data.get('in_quantity') or 0)
|
||||
u_price = float(data.get('unit_price') or 0)
|
||||
tax_rate = float(data.get('tax_rate') or 0) # [新增]
|
||||
tax_rate = float(data.get('tax_rate') or 0)
|
||||
|
||||
# 计算税后单价
|
||||
post_tax_price = float(data.get('post_tax_unit_price') or 0)
|
||||
if post_tax_price == 0 and u_price > 0:
|
||||
tax_multiplier = 1 + (tax_rate / 100)
|
||||
post_tax_price = u_price * tax_multiplier
|
||||
|
||||
try:
|
||||
seq_sql = text("SELECT nextval('global_print_seq')")
|
||||
@ -137,8 +143,9 @@ class BuyInboundService:
|
||||
warehouse_location=data.get('warehouse_location'),
|
||||
|
||||
# 价格信息
|
||||
unit_price=u_price,
|
||||
tax_rate=tax_rate, # [新增]
|
||||
pre_tax_unit_price=u_price,
|
||||
post_tax_unit_price=post_tax_price,
|
||||
tax_rate=tax_rate,
|
||||
total_price=in_qty * u_price,
|
||||
currency=data.get('currency', 'CNY'),
|
||||
exchange_rate=data.get('exchange_rate', 1.0),
|
||||
@ -182,8 +189,22 @@ class BuyInboundService:
|
||||
if 'arrival_photo' in data: stock.arrival_photo = json.dumps(data['arrival_photo'])
|
||||
if 'inspection_report' in data: stock.inspection_report = json.dumps(data['inspection_report'])
|
||||
|
||||
# [新增] 更新税率
|
||||
if 'tax_rate' in data: stock.tax_rate = float(data['tax_rate'])
|
||||
# 更新税率
|
||||
if 'tax_rate' in data:
|
||||
stock.tax_rate = float(data['tax_rate'])
|
||||
|
||||
# 更新税前单价
|
||||
if 'unit_price' in data:
|
||||
stock.pre_tax_unit_price = float(data['unit_price'])
|
||||
|
||||
# 更新税后单价
|
||||
if 'post_tax_unit_price' in data:
|
||||
stock.post_tax_unit_price = float(data['post_tax_unit_price'])
|
||||
else:
|
||||
# 如果税后单价没有提供,根据税前单价和税率计算
|
||||
if 'unit_price' in data or 'tax_rate' in data:
|
||||
tax_multiplier = 1 + (float(data.get('tax_rate', stock.tax_rate or 0)) / 100)
|
||||
stock.post_tax_unit_price = float(stock.pre_tax_unit_price) * tax_multiplier
|
||||
|
||||
if 'in_quantity' in data:
|
||||
diff = float(data['in_quantity']) - float(stock.in_quantity)
|
||||
@ -192,9 +213,8 @@ class BuyInboundService:
|
||||
stock.stock_quantity = float(stock.stock_quantity) + diff
|
||||
stock.available_quantity = float(stock.available_quantity) + diff
|
||||
|
||||
if 'unit_price' in data: stock.unit_price = float(data['unit_price'])
|
||||
|
||||
stock.total_price = float(stock.in_quantity) * float(stock.unit_price)
|
||||
# 重新计算总价
|
||||
stock.total_price = float(stock.in_quantity) * float(stock.pre_tax_unit_price)
|
||||
db.session.commit()
|
||||
return stock
|
||||
except Exception as e:
|
||||
@ -322,4 +342,4 @@ class BuyInboundService:
|
||||
@staticmethod
|
||||
def get_history_locations(base_id):
|
||||
return [r[0] for r in
|
||||
db.session.query(StockBuy.warehouse_location).filter(StockBuy.base_id == base_id).distinct().all()]
|
||||
db.session.query(StockBuy.warehouse_location).filter(StockBuy.base_id == base_id).distinct().all()]
|
||||
|
||||
@ -10,9 +10,6 @@ import json
|
||||
|
||||
class ProductInboundService:
|
||||
|
||||
# ============================================================
|
||||
# 0. 辅助:唯一性校验
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def _check_unique(serial_number, exclude_id=None):
|
||||
from app.models.inbound.product import StockProduct
|
||||
@ -25,11 +22,8 @@ class ProductInboundService:
|
||||
occupied_name = exists.base.name if (hasattr(exists, 'base') and exists.base) else "未知物料"
|
||||
raise ValueError(f"序列号【{serial_number}】已存在!被成品 [{occupied_name}] 占用,请核查。")
|
||||
|
||||
# ============================================================
|
||||
# 1. 基础物料搜索
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def search_base_material(keyword):
|
||||
def search_base_material(keyword, page=1, limit=50):
|
||||
try:
|
||||
query = MaterialBase.query.filter(MaterialBase.is_enabled == True)
|
||||
if keyword:
|
||||
@ -38,15 +32,16 @@ class ProductInboundService:
|
||||
or_(
|
||||
MaterialBase.name.ilike(kw),
|
||||
MaterialBase.spec_model.ilike(kw),
|
||||
MaterialBase.company_name.ilike(kw) # [新增]
|
||||
MaterialBase.company_name.ilike(kw)
|
||||
)
|
||||
)
|
||||
query = query.order_by(MaterialBase.id.desc()).limit(20)
|
||||
query = query.order_by(MaterialBase.id.desc())
|
||||
pagination = query.paginate(page=page, per_page=limit, error_out=False)
|
||||
results = []
|
||||
for item in query.all():
|
||||
for item in pagination.items:
|
||||
results.append({
|
||||
'id': item.id,
|
||||
'company_name': item.company_name, # [新增]
|
||||
'company_name': item.company_name,
|
||||
'name': item.name,
|
||||
'spec': item.spec_model,
|
||||
'category': item.category,
|
||||
@ -54,14 +49,16 @@ class ProductInboundService:
|
||||
'type': item.material_type,
|
||||
'status': '启用'
|
||||
})
|
||||
return results
|
||||
return {
|
||||
"items": results,
|
||||
"total": pagination.total,
|
||||
"page": page,
|
||||
"has_next": pagination.has_next
|
||||
}
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
return []
|
||||
return {"items": [], "total": 0, "page": 1, "has_next": False}
|
||||
|
||||
# ============================================================
|
||||
# 1.5 BOM 搜索逻辑
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def search_bom_options(keyword):
|
||||
from app.models.bom import BomTable
|
||||
@ -98,9 +95,6 @@ class ProductInboundService:
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
# ============================================================
|
||||
# 2. 新增入库逻辑
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def handle_inbound(data):
|
||||
from app.models.inbound.product import StockProduct
|
||||
@ -132,6 +126,11 @@ class ProductInboundService:
|
||||
in_date_val = current_time
|
||||
|
||||
in_qty = float(data.get('in_quantity') or 0)
|
||||
raw_cost = float(data.get('raw_material_cost') or 0)
|
||||
manual_cost = 0.0 # 字段已弃用,保持向后兼容
|
||||
unit_total_cost = float(data.get('unit_total_cost') or raw_cost or 0)
|
||||
total_price = unit_total_cost * in_qty
|
||||
|
||||
p_start = data.get('production_start_time', '')
|
||||
p_end = data.get('production_end_time', '')
|
||||
time_range = f"{p_start} ~ {p_end}" if p_start or p_end else None
|
||||
@ -170,8 +169,8 @@ class ProductInboundService:
|
||||
work_order_code=data.get('work_order_code'),
|
||||
production_manager=data.get('production_manager'),
|
||||
production_time_range=time_range,
|
||||
raw_material_cost=float(data.get('raw_material_cost') or 0),
|
||||
manual_cost=float(data.get('manual_cost') or 0),
|
||||
raw_material_cost=raw_cost,
|
||||
manual_cost=unit_total_cost,
|
||||
quality_status=data.get('quality_status', '合格'),
|
||||
product_photo=json.dumps(photo_list),
|
||||
quality_report_link=json.dumps(quality_list),
|
||||
@ -188,9 +187,6 @@ class ProductInboundService:
|
||||
db.session.rollback()
|
||||
raise e
|
||||
|
||||
# ============================================================
|
||||
# 3. 更新逻辑
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def update_inbound(stock_id, data):
|
||||
from app.models.inbound.product import StockProduct
|
||||
@ -225,7 +221,7 @@ class ProductInboundService:
|
||||
|
||||
if 'sale_price' in data: stock.sale_price = float(data['sale_price'])
|
||||
if 'raw_material_cost' in data: stock.raw_material_cost = float(data['raw_material_cost'])
|
||||
if 'manual_cost' in data: stock.manual_cost = float(data['manual_cost'])
|
||||
if 'unit_total_cost' in data: stock.manual_cost = float(data['unit_total_cost']) # 映射到 manual_cost 物理字段
|
||||
|
||||
if 'in_quantity' in data:
|
||||
new_qty = float(data['in_quantity'])
|
||||
@ -234,6 +230,7 @@ class ProductInboundService:
|
||||
stock.stock_quantity = float(stock.stock_quantity) + diff
|
||||
stock.available_quantity = float(stock.available_quantity) + diff
|
||||
|
||||
|
||||
if 'production_start_time' in data or 'production_end_time' in data:
|
||||
old_range = stock.production_time_range or " ~ "
|
||||
parts = old_range.split(' ~ ')
|
||||
@ -249,9 +246,6 @@ class ProductInboundService:
|
||||
db.session.rollback()
|
||||
raise e
|
||||
|
||||
# ============================================================
|
||||
# 4. 删除逻辑
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def delete_inbound(stock_id):
|
||||
from app.models.inbound.product import StockProduct
|
||||
@ -265,9 +259,6 @@ class ProductInboundService:
|
||||
db.session.rollback()
|
||||
raise e
|
||||
|
||||
# ============================================================
|
||||
# 5. 出库历史
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def get_outbound_history(stock_id):
|
||||
try:
|
||||
@ -278,9 +269,6 @@ class ProductInboundService:
|
||||
except:
|
||||
return []
|
||||
|
||||
# ============================================================
|
||||
# 6. 获取列表
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def get_list(page, limit, keyword=None, statuses=None, category=None, material_type=None, company=None):
|
||||
from app.models.inbound.product import StockProduct
|
||||
@ -291,7 +279,7 @@ class ProductInboundService:
|
||||
query = query.filter(or_(
|
||||
MaterialBase.name.ilike(kw),
|
||||
MaterialBase.spec_model.ilike(kw),
|
||||
MaterialBase.company_name.ilike(kw), # [新增]
|
||||
MaterialBase.company_name.ilike(kw),
|
||||
StockProduct.serial_number.ilike(kw),
|
||||
StockProduct.work_order_code.ilike(kw),
|
||||
StockProduct.order_id.ilike(kw),
|
||||
@ -302,7 +290,6 @@ class ProductInboundService:
|
||||
if material_type and material_type.strip():
|
||||
query = query.filter(MaterialBase.material_type == material_type.strip())
|
||||
|
||||
# [新增]
|
||||
if company and company.strip():
|
||||
query = query.filter(MaterialBase.company_name == company.strip())
|
||||
|
||||
@ -330,7 +317,9 @@ class ProductInboundService:
|
||||
|
||||
items = []
|
||||
for item in current_items:
|
||||
items.append(item.to_dict()) # 使用 Model to_dict
|
||||
item_dict = item.to_dict()
|
||||
item_dict['unit_total_cost'] = float(item.manual_cost or 0)
|
||||
items.append(item_dict)
|
||||
return {"total": pagination.total, "items": items}
|
||||
except:
|
||||
traceback.print_exc()
|
||||
@ -358,29 +347,20 @@ class ProductInboundService:
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
# ============================================================
|
||||
# 7. 获取筛选项
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def get_filter_options():
|
||||
try:
|
||||
from app.models.base import MaterialBase
|
||||
# 类别
|
||||
categories = db.session.query(MaterialBase.category) \
|
||||
.filter(MaterialBase.category != None, MaterialBase.category != '') \
|
||||
.distinct().all()
|
||||
categories = db.session.query(MaterialBase.category).filter(MaterialBase.category != None,
|
||||
MaterialBase.category != '').distinct().all()
|
||||
sorted_categories = sorted([r[0] for r in categories])
|
||||
|
||||
# 类型
|
||||
types = db.session.query(MaterialBase.material_type) \
|
||||
.filter(MaterialBase.material_type != None, MaterialBase.material_type != '') \
|
||||
.distinct().all()
|
||||
types = db.session.query(MaterialBase.material_type).filter(MaterialBase.material_type != None,
|
||||
MaterialBase.material_type != '').distinct().all()
|
||||
sorted_types = sorted([r[0] for r in types])
|
||||
|
||||
# [新增] 公司
|
||||
companies = db.session.query(MaterialBase.company_name) \
|
||||
.filter(MaterialBase.company_name != None, MaterialBase.company_name != '') \
|
||||
.distinct().all()
|
||||
companies = db.session.query(MaterialBase.company_name).filter(MaterialBase.company_name != None,
|
||||
MaterialBase.company_name != '').distinct().all()
|
||||
sorted_companies = sorted([r[0] for r in companies])
|
||||
|
||||
return {
|
||||
@ -391,4 +371,71 @@ class ProductInboundService:
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {"categories": [], "types": [], "companies": []}
|
||||
return {"categories": [], "types": [], "companies": []}
|
||||
|
||||
@staticmethod
|
||||
def get_history_managers(keyword=None):
|
||||
from app.models.inbound.product import StockProduct
|
||||
try:
|
||||
query = db.session.query(StockProduct.production_manager).filter(
|
||||
StockProduct.production_manager.isnot(None),
|
||||
StockProduct.production_manager != ''
|
||||
)
|
||||
if keyword:
|
||||
query = query.filter(StockProduct.production_manager.ilike(f'%{keyword}%'))
|
||||
records = query.distinct().all()
|
||||
return [r[0] for r in records if r[0]]
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
# ============================================================
|
||||
# 9. BOM 原材料成本自动核算 (新增)
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def calculate_bom_cost(bom_no, bom_version):
|
||||
"""
|
||||
根据 BOM 编号和版本计算原材料总成本
|
||||
遍历 BOM 子件,使用原生 SQL 查物理表 bom_table,取每个子件在采购、半成品、成品三个表中的最高单价,乘以用量后累加
|
||||
"""
|
||||
from app.models.inbound.buy import StockBuy
|
||||
from app.models.inbound.semi import StockSemi
|
||||
from app.models.inbound.product import StockProduct
|
||||
from sqlalchemy import func, text
|
||||
try:
|
||||
# 使用原生 SQL 精准查询 bom_table,避免模型映射错误
|
||||
sql = text("""
|
||||
SELECT child_id, dosage
|
||||
FROM bom_table
|
||||
WHERE bom_no = :bom_no AND version = :version
|
||||
""")
|
||||
bom_lines = db.session.execute(sql, {'bom_no': bom_no, 'version': bom_version}).fetchall()
|
||||
|
||||
total_cost = 0.0
|
||||
for line in bom_lines:
|
||||
component_base_id = line[0] # child_id
|
||||
usage_qty = float(line[1] or 1.0) # dosage
|
||||
|
||||
# 1. 查采购表最高价 (不含税)
|
||||
buy_price = db.session.query(func.max(StockBuy.pre_tax_unit_price)).filter(
|
||||
StockBuy.base_id == component_base_id
|
||||
).scalar() or 0.0
|
||||
|
||||
# 2. 查半成品表最高价 (单件成本映射存在 manual_cost 里了)
|
||||
semi_price = db.session.query(func.max(StockSemi.manual_cost)).filter(
|
||||
StockSemi.base_id == component_base_id
|
||||
).scalar() or 0.0
|
||||
|
||||
# 3. 查成品表最高价 (同样存储在 manual_cost 字段里)
|
||||
product_price = db.session.query(func.max(StockProduct.manual_cost)).filter(
|
||||
StockProduct.base_id == component_base_id
|
||||
).scalar() or 0.0
|
||||
|
||||
# 4. 取三个表中的最大值,乘以用量 (dosage)
|
||||
max_price = max(float(buy_price), float(semi_price), float(product_price))
|
||||
total_cost += max_price * usage_qty
|
||||
|
||||
return round(total_cost, 2)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
raise e
|
||||
|
||||
@ -10,9 +10,6 @@ import json
|
||||
|
||||
class SemiInboundService:
|
||||
|
||||
# ============================================================
|
||||
# 0. 辅助:唯一性校验
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def _check_unique(base_id, serial_number, batch_number, exclude_id=None):
|
||||
from app.models.inbound.semi import StockSemi
|
||||
@ -35,11 +32,8 @@ class SemiInboundService:
|
||||
if query.first():
|
||||
raise ValueError(f"该物料已存在批号【{batch_number}】,请勿重复建单,建议在原批次上追加库存。")
|
||||
|
||||
# ============================================================
|
||||
# 1. 基础物料搜索
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def search_base_material(keyword):
|
||||
def search_base_material(keyword, page=1, limit=50):
|
||||
try:
|
||||
query = MaterialBase.query.filter(MaterialBase.is_enabled == True)
|
||||
if keyword:
|
||||
@ -48,15 +42,16 @@ class SemiInboundService:
|
||||
or_(
|
||||
MaterialBase.name.ilike(kw),
|
||||
MaterialBase.spec_model.ilike(kw),
|
||||
MaterialBase.company_name.ilike(kw) # [新增] 支持搜公司
|
||||
MaterialBase.company_name.ilike(kw)
|
||||
)
|
||||
)
|
||||
query = query.order_by(MaterialBase.id.desc()).limit(20)
|
||||
query = query.order_by(MaterialBase.id.desc())
|
||||
pagination = query.paginate(page=page, per_page=limit, error_out=False)
|
||||
results = []
|
||||
for item in query.all():
|
||||
for item in pagination.items:
|
||||
results.append({
|
||||
'id': item.id,
|
||||
'company_name': item.company_name, # [新增]
|
||||
'company_name': item.company_name,
|
||||
'name': item.name,
|
||||
'spec': item.spec_model,
|
||||
'category': item.category,
|
||||
@ -64,14 +59,11 @@ class SemiInboundService:
|
||||
'type': item.material_type,
|
||||
'status': '启用'
|
||||
})
|
||||
return results
|
||||
return {"items": results, "total": pagination.total, "page": page, "has_next": pagination.has_next}
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return []
|
||||
return {"items": [], "total": 0, "page": 1, "has_next": False}
|
||||
|
||||
# ============================================================
|
||||
# 1.5 BOM 搜索逻辑
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def search_bom_options(keyword):
|
||||
from app.models.bom import BomTable
|
||||
@ -108,9 +100,6 @@ class SemiInboundService:
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
# ============================================================
|
||||
# 2. 新增入库逻辑
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def handle_inbound(data):
|
||||
from app.models.inbound.semi import StockSemi
|
||||
@ -167,9 +156,9 @@ class SemiInboundService:
|
||||
|
||||
in_qty = float(data.get('in_quantity') or 0)
|
||||
raw_cost = float(data.get('raw_material_cost') or 0)
|
||||
manual_cost = float(data.get('manual_cost') or 0)
|
||||
unit_total_cost = raw_cost + manual_cost
|
||||
total_value = unit_total_cost * in_qty
|
||||
# 【重要修改】:把前端的 unit_total_cost(单件成本)存入原数据库的 manual_cost 字段中
|
||||
unit_cost = float(data.get('unit_total_cost') or raw_cost)
|
||||
total_value = unit_cost * in_qty
|
||||
|
||||
next_global_id = 0
|
||||
try:
|
||||
@ -215,7 +204,7 @@ class SemiInboundService:
|
||||
production_end_time=p_end,
|
||||
production_time_range=time_range_str,
|
||||
raw_material_cost=raw_cost,
|
||||
manual_cost=manual_cost,
|
||||
manual_cost=unit_cost, # 映射到 manual_cost 物理字段
|
||||
total_price=total_value,
|
||||
arrival_photo=json.dumps(arrival_list),
|
||||
quality_report_link=json.dumps(quality_report_list),
|
||||
@ -231,9 +220,6 @@ class SemiInboundService:
|
||||
traceback.print_exc()
|
||||
raise e
|
||||
|
||||
# ============================================================
|
||||
# 3. 更新逻辑
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def update_inbound(stock_id, data):
|
||||
from app.models.inbound.semi import StockSemi
|
||||
@ -307,7 +293,6 @@ class SemiInboundService:
|
||||
stock.production_time_range = raw_range
|
||||
|
||||
qty_changed = False
|
||||
cost_changed = False
|
||||
if 'in_quantity' in data:
|
||||
new_qty = float(data['in_quantity'])
|
||||
diff = new_qty - float(stock.in_quantity)
|
||||
@ -316,15 +301,16 @@ class SemiInboundService:
|
||||
stock.stock_quantity = float(stock.stock_quantity) + diff
|
||||
stock.available_quantity = float(stock.available_quantity) + diff
|
||||
qty_changed = True
|
||||
|
||||
if 'raw_material_cost' in data:
|
||||
stock.raw_material_cost = float(data['raw_material_cost'])
|
||||
cost_changed = True
|
||||
if 'manual_cost' in data:
|
||||
stock.manual_cost = float(data['manual_cost'])
|
||||
cost_changed = True
|
||||
if cost_changed or qty_changed:
|
||||
unit_total = float(stock.raw_material_cost) + float(stock.manual_cost)
|
||||
stock.total_price = float(stock.in_quantity) * unit_total
|
||||
if 'unit_total_cost' in data:
|
||||
stock.manual_cost = float(data['unit_total_cost']) # 映射到 manual_cost 物理字段
|
||||
|
||||
if 'unit_total_cost' in data or qty_changed:
|
||||
qty = float(stock.in_quantity or 1)
|
||||
# 使用存入 manual_cost 的单价计算总价
|
||||
stock.total_price = float(stock.manual_cost or 0) * qty
|
||||
|
||||
db.session.commit()
|
||||
return stock
|
||||
@ -332,9 +318,6 @@ class SemiInboundService:
|
||||
db.session.rollback()
|
||||
raise e
|
||||
|
||||
# ============================================================
|
||||
# 4. 删除逻辑
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def delete_inbound(stock_id):
|
||||
from app.models.inbound.semi import StockSemi
|
||||
@ -349,9 +332,6 @@ class SemiInboundService:
|
||||
db.session.rollback()
|
||||
raise e
|
||||
|
||||
# ============================================================
|
||||
# 5. 出库历史
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def get_outbound_history(stock_id):
|
||||
try:
|
||||
@ -362,9 +342,6 @@ class SemiInboundService:
|
||||
except:
|
||||
return []
|
||||
|
||||
# ============================================================
|
||||
# 6. 获取列表
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def get_list(page, limit, keyword=None, statuses=None, category=None, material_type=None, company=None):
|
||||
from app.models.inbound.semi import StockSemi
|
||||
@ -376,7 +353,7 @@ class SemiInboundService:
|
||||
or_(
|
||||
MaterialBase.name.ilike(kw),
|
||||
MaterialBase.spec_model.ilike(kw),
|
||||
MaterialBase.company_name.ilike(kw), # [新增]
|
||||
MaterialBase.company_name.ilike(kw),
|
||||
StockSemi.batch_number.ilike(kw),
|
||||
StockSemi.serial_number.ilike(kw),
|
||||
StockSemi.sku.ilike(kw),
|
||||
@ -389,7 +366,6 @@ class SemiInboundService:
|
||||
if material_type and material_type.strip():
|
||||
query = query.filter(MaterialBase.material_type == material_type.strip())
|
||||
|
||||
# [新增] 公司筛选
|
||||
if company and company.strip():
|
||||
query = query.filter(MaterialBase.company_name == company.strip())
|
||||
|
||||
@ -406,18 +382,12 @@ class SemiInboundService:
|
||||
)
|
||||
pagination = query.order_by(StockSemi.production_date.desc()).paginate(page=page, per_page=limit,
|
||||
error_out=False)
|
||||
current_items = pagination.items
|
||||
|
||||
def parse_img(json_str):
|
||||
if not json_str: return []
|
||||
try:
|
||||
return json.loads(json_str) if json_str.startswith('[') else [json_str]
|
||||
except:
|
||||
return []
|
||||
|
||||
items = []
|
||||
for item in current_items:
|
||||
items.append(item.to_dict()) # 直接使用 Model 的 to_dict (已包含 company_name)
|
||||
for item in pagination.items:
|
||||
# 把 manual_cost 伪装成 unit_total_cost 返回给前端
|
||||
item_dict = item.to_dict()
|
||||
item_dict['unit_total_cost'] = float(item.manual_cost or 0)
|
||||
items.append(item_dict)
|
||||
return {"total": pagination.total, "items": items}
|
||||
except Exception as e:
|
||||
print(f"List Error: {e}")
|
||||
@ -446,29 +416,18 @@ class SemiInboundService:
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
# ============================================================
|
||||
# 7. 获取筛选项 (排序)
|
||||
# ============================================================
|
||||
@staticmethod
|
||||
def get_filter_options():
|
||||
try:
|
||||
from app.models.base import MaterialBase
|
||||
# 类别
|
||||
categories = db.session.query(MaterialBase.category) \
|
||||
.filter(MaterialBase.category != None, MaterialBase.category != '') \
|
||||
.distinct().all()
|
||||
categories = db.session.query(MaterialBase.category).filter(MaterialBase.category != None,
|
||||
MaterialBase.category != '').distinct().all()
|
||||
sorted_categories = sorted([r[0] for r in categories])
|
||||
|
||||
# 类型
|
||||
types = db.session.query(MaterialBase.material_type) \
|
||||
.filter(MaterialBase.material_type != None, MaterialBase.material_type != '') \
|
||||
.distinct().all()
|
||||
types = db.session.query(MaterialBase.material_type).filter(MaterialBase.material_type != None,
|
||||
MaterialBase.material_type != '').distinct().all()
|
||||
sorted_types = sorted([r[0] for r in types])
|
||||
|
||||
# [新增] 公司
|
||||
companies = db.session.query(MaterialBase.company_name) \
|
||||
.filter(MaterialBase.company_name != None, MaterialBase.company_name != '') \
|
||||
.distinct().all()
|
||||
companies = db.session.query(MaterialBase.company_name).filter(MaterialBase.company_name != None,
|
||||
MaterialBase.company_name != '').distinct().all()
|
||||
sorted_companies = sorted([r[0] for r in companies])
|
||||
|
||||
return {
|
||||
@ -477,6 +436,69 @@ class SemiInboundService:
|
||||
"companies": sorted_companies
|
||||
}
|
||||
except Exception:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {"categories": [], "types": [], "companies": []}
|
||||
return {"categories": [], "types": [], "companies": []}
|
||||
|
||||
@staticmethod
|
||||
def get_history_managers(keyword=None):
|
||||
from app.models.inbound.semi import StockSemi
|
||||
try:
|
||||
query = db.session.query(StockSemi.production_manager).filter(
|
||||
StockSemi.production_manager.isnot(None),
|
||||
StockSemi.production_manager != ''
|
||||
)
|
||||
if keyword:
|
||||
query = query.filter(StockSemi.production_manager.ilike(f'%{keyword}%'))
|
||||
records = query.distinct().all()
|
||||
return [r[0] for r in records if r[0]]
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def calculate_bom_cost(bom_no, bom_version):
|
||||
"""
|
||||
根据 BOM 编号和版本计算原材料总成本
|
||||
遍历 BOM 子件,使用原生 SQL 查物理表 bom_table,取每个子件在采购、半成品、成品三个表中的最高单价,乘以用量后累加
|
||||
"""
|
||||
from app.models.inbound.buy import StockBuy
|
||||
from app.models.inbound.semi import StockSemi
|
||||
from app.models.inbound.product import StockProduct
|
||||
from sqlalchemy import func, text
|
||||
try:
|
||||
# 使用原生 SQL 精准查询 bom_table,避免模型映射错误
|
||||
sql = text("""
|
||||
SELECT child_id, dosage
|
||||
FROM bom_table
|
||||
WHERE bom_no = :bom_no AND version = :version
|
||||
""")
|
||||
bom_lines = db.session.execute(sql, {'bom_no': bom_no, 'version': bom_version}).fetchall()
|
||||
|
||||
total_cost = 0.0
|
||||
for line in bom_lines:
|
||||
component_base_id = line[0] # child_id
|
||||
usage_qty = float(line[1] or 1.0) # dosage
|
||||
|
||||
# 1. 查采购表最高价 (不含税)
|
||||
buy_price = db.session.query(func.max(StockBuy.pre_tax_unit_price)).filter(
|
||||
StockBuy.base_id == component_base_id
|
||||
).scalar() or 0.0
|
||||
|
||||
# 2. 查半成品表最高价 (单件成本映射存在 manual_cost 里了)
|
||||
semi_price = db.session.query(func.max(StockSemi.manual_cost)).filter(
|
||||
StockSemi.base_id == component_base_id
|
||||
).scalar() or 0.0
|
||||
|
||||
# 3. 查成品表最高价 (同样存储在 manual_cost 字段里)
|
||||
product_price = db.session.query(func.max(StockProduct.manual_cost)).filter(
|
||||
StockProduct.base_id == component_base_id
|
||||
).scalar() or 0.0
|
||||
|
||||
# 4. 取三个表中的最大值,乘以用量 (dosage)
|
||||
max_price = max(float(buy_price), float(semi_price), float(product_price))
|
||||
total_cost += max_price * usage_qty
|
||||
|
||||
return round(total_cost, 2)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
raise e
|
||||
|
||||
@ -48,7 +48,7 @@ class OutboundService:
|
||||
if table_type == 'stock_product':
|
||||
return float(item.sale_price) if item.sale_price else 0
|
||||
elif table_type == 'stock_buy':
|
||||
return float(item.unit_price) if item.unit_price else 0
|
||||
return float(item.pre_tax_unit_price) if item.pre_tax_unit_price else 0
|
||||
return 0
|
||||
|
||||
prod = StockProduct.query.filter(
|
||||
|
||||
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
|
||||
from functools import wraps
|
||||
from flask_jwt_extended import get_jwt
|
||||
from flask import jsonify
|
||||
from flask_jwt_extended import get_jwt, verify_jwt_in_request
|
||||
from flask import jsonify, g
|
||||
import logging
|
||||
|
||||
|
||||
def role_required(*roles):
|
||||
@ -15,16 +16,74 @@ def role_required(*roles):
|
||||
def decorator(*args, **kwargs):
|
||||
claims = get_jwt()
|
||||
user_role = claims.get('role')
|
||||
user_role_upper = user_role.upper() if user_role else None
|
||||
|
||||
# 如果是超级管理员,拥有上帝视角,直接放行 (可选)
|
||||
if user_role == 'super_admin':
|
||||
if user_role_upper == 'SUPER_ADMIN':
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
if user_role not in roles:
|
||||
if user_role_upper not in [r.upper() for r in roles]:
|
||||
return jsonify(msg='权限不足:您没有访问此资源的权限'), 403
|
||||
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
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 and user_role.upper() == '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">
|
||||
import { computed } from 'vue'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import { InfoFilled, SwitchButton, UserFilled } from '@element-plus/icons-vue'
|
||||
@ -14,6 +14,13 @@ const isLoginPage = computed(() => {
|
||||
return route.path === '/login'
|
||||
})
|
||||
|
||||
// 页面加载时刷新权限
|
||||
onMounted(() => {
|
||||
if (userStore.token) {
|
||||
userStore.refreshUserPermissions()
|
||||
}
|
||||
})
|
||||
|
||||
// --- 退出登录逻辑 Start ---
|
||||
const handleLogout = () => {
|
||||
ElMessageBox.confirm(
|
||||
@ -82,7 +89,7 @@ const handleLogout = () => {
|
||||
<footer v-if="!isLoginPage" class="app-footer">
|
||||
<span class="version-tag">
|
||||
<el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon>
|
||||
当前版本: 1.3 Beta (2.25权限管理版)
|
||||
当前版本: 2.2录入测试版
|
||||
</span>
|
||||
</footer>
|
||||
</div>
|
||||
@ -197,4 +204,4 @@ const handleLogout = () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -11,8 +11,11 @@ export function getBomList(params?: any) {
|
||||
|
||||
// 获取BOM详情
|
||||
export function getBomDetail(bomNo: string) {
|
||||
// 去除首尾斜杠,保留中间斜杠并进行 URL 编码
|
||||
const trimmed = bomNo.replace(/^\/+|\/+$/g, '');
|
||||
const encoded = encodeURIComponent(trimmed);
|
||||
return request({
|
||||
url: `/v1/bom/detail/${bomNo}`,
|
||||
url: `/v1/bom/detail/${encoded}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
@ -28,8 +31,11 @@ export function saveBom(data: any) {
|
||||
|
||||
// 删除BOM(暂未实现,预留)
|
||||
export function deleteBom(bomNo: string) {
|
||||
// 去除首尾斜杠,保留中间斜杠并进行 URL 编码
|
||||
const trimmed = bomNo.replace(/^\/+|\/+$/g, '');
|
||||
const encoded = encodeURIComponent(trimmed);
|
||||
return request({
|
||||
url: `/v1/bom/${bomNo}`,
|
||||
url: `/v1/bom/${encoded}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
@ -33,11 +33,12 @@ export function deleteProductInbound(id: number) {
|
||||
})
|
||||
}
|
||||
|
||||
export function searchMaterialBase(keyword: string) {
|
||||
// 搜索基础物料 (已增加 page 参数)
|
||||
export function searchMaterialBase(keyword: string, page: number = 1) {
|
||||
return request({
|
||||
url: '/inbound/product/search-base',
|
||||
method: 'get',
|
||||
params: { keyword }
|
||||
params: { keyword, page }
|
||||
})
|
||||
}
|
||||
|
||||
@ -65,4 +66,13 @@ export function getFilterOptions() {
|
||||
url: '/inbound/product/options',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// [新增] 负责人历史记录
|
||||
export function getManagerHistory(params: any) {
|
||||
return request({
|
||||
url: '/inbound/product/suggestions/managers',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
@ -35,12 +35,12 @@ export function deleteSemiInbound(id: number) {
|
||||
})
|
||||
}
|
||||
|
||||
// 5. 搜索基础物料
|
||||
export function searchMaterialBase(keyword: string) {
|
||||
// 5. 搜索基础物料 (已增加 page 参数)
|
||||
export function searchMaterialBase(keyword: string, page: number = 1) {
|
||||
return request({
|
||||
url: '/inbound/semi/search-base',
|
||||
method: 'get',
|
||||
params: { keyword }
|
||||
params: { keyword, page }
|
||||
})
|
||||
}
|
||||
|
||||
@ -68,4 +68,13 @@ export function getFilterOptions() {
|
||||
url: '/inbound/semi/options',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// [新增] 生产负责人历史记录
|
||||
export function getManagerHistory(params: any) {
|
||||
return request({
|
||||
url: '/inbound/semi/suggestions/managers',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
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: {
|
||||
title: '账号开通',
|
||||
icon: 'User',
|
||||
// 子路由也建议加上权限限制
|
||||
roles: ['SUPER_ADMIN', 'SUPERVISOR']
|
||||
}
|
||||
},
|
||||
// [新增] 权限分配页面,只有超级管理员可进
|
||||
{
|
||||
path: 'permission',
|
||||
name: 'PermissionConfig',
|
||||
component: () => import('@/views/system/PermissionConfig.vue'),
|
||||
meta: {
|
||||
title: '权限分配',
|
||||
icon: 'Lock',
|
||||
roles: ['SUPER_ADMIN']
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -224,11 +234,10 @@ router.beforeEach((to, from, next) => {
|
||||
const token = userStore.token || localStorage.getItem('token')
|
||||
|
||||
// [修复] 优先从 user 对象获取,并统一转大写,防止大小写不一致导致权限失效
|
||||
// 注意:Store 中存储的可能是 user.role 或者直接是 role,根据你之前的 store 结构适配
|
||||
const rawRole = userStore.user?.role || userStore.role || localStorage.getItem('role') || 'user'
|
||||
const userRole = String(rawRole).toUpperCase()
|
||||
|
||||
// 调试日志:如果跳转有问题,请按 F12 查看控制台输出
|
||||
// 调试日志
|
||||
if (to.path.includes('/system')) {
|
||||
console.log(`路由守卫检查: Path=${to.path}, UserRole=${userRole}, Required=${to.meta.roles}`)
|
||||
}
|
||||
@ -249,7 +258,6 @@ router.beforeEach((to, from, next) => {
|
||||
|
||||
// 权限检查逻辑
|
||||
if (to.meta.roles && Array.isArray(to.meta.roles)) {
|
||||
// [修复] to.meta.roles 里已经是大写了,userRole 也转大写了,现在可以安全比对
|
||||
if (to.meta.roles.includes(userRole)) {
|
||||
next()
|
||||
} else {
|
||||
|
||||
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 { login } from '@/api/auth'
|
||||
import { getRolePermissions } from '@/api/system/permission'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
@ -7,6 +8,7 @@ export const useUserStore = defineStore('user', () => {
|
||||
const token = ref(localStorage.getItem('token') || '')
|
||||
const role = ref(localStorage.getItem('role') || '')
|
||||
const username = ref(localStorage.getItem('username') || '')
|
||||
const permissions = ref<string[]>(JSON.parse(localStorage.getItem('permissions') || '[]'))
|
||||
|
||||
// 2. Actions
|
||||
// 登录逻辑
|
||||
@ -33,7 +35,8 @@ export const useUserStore = defineStore('user', () => {
|
||||
|
||||
// 处理用户信息 (确保后端返回结构中有 user 字段)
|
||||
if (data.user) {
|
||||
role.value = data.user.role || 'user' // 默认给个 user 角色防止空
|
||||
const rawRole = data.user.role || 'user'
|
||||
role.value = rawRole.toUpperCase() // 角色统一转换为大写
|
||||
username.value = data.user.username || '用户'
|
||||
|
||||
// 持久化存储用户信息
|
||||
@ -44,6 +47,25 @@ export const useUserStore = defineStore('user', () => {
|
||||
// 持久化存储 Token
|
||||
localStorage.setItem('token', data.access_token)
|
||||
|
||||
// 登录成功后,根据角色获取权限
|
||||
if (role.value) {
|
||||
try {
|
||||
const permRes = await getRolePermissions(role.value)
|
||||
const permData = permRes.data || permRes
|
||||
// 合并 menus 和 elements 两个数组
|
||||
const allPerms = [
|
||||
...(permData.menus || []),
|
||||
...(permData.elements || [])
|
||||
]
|
||||
permissions.value = allPerms
|
||||
localStorage.setItem('permissions', JSON.stringify(allPerms))
|
||||
} catch (error) {
|
||||
console.error('获取权限失败:', error)
|
||||
permissions.value = []
|
||||
localStorage.setItem('permissions', '[]')
|
||||
}
|
||||
}
|
||||
|
||||
return true // 返回 true 表示登录成功
|
||||
}
|
||||
|
||||
@ -53,11 +75,36 @@ export const useUserStore = defineStore('user', () => {
|
||||
token.value = ''
|
||||
role.value = ''
|
||||
username.value = ''
|
||||
permissions.value = []
|
||||
|
||||
// 2. 清空 LocalStorage (硬盘)
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('role')
|
||||
localStorage.removeItem('username')
|
||||
localStorage.removeItem('permissions')
|
||||
}
|
||||
|
||||
// 刷新用户权限(不重新登录)
|
||||
const refreshUserPermissions = async () => {
|
||||
if (!token.value || !role.value) {
|
||||
console.warn('无法刷新权限:用户未登录')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const permRes = await getRolePermissions(role.value)
|
||||
const permData = permRes.data || permRes
|
||||
// 合并 menus 和 elements 两个数组
|
||||
const allPerms = [
|
||||
...(permData.menus || []),
|
||||
...(permData.elements || [])
|
||||
]
|
||||
permissions.value = allPerms
|
||||
localStorage.setItem('permissions', JSON.stringify(allPerms))
|
||||
console.log('用户权限已刷新')
|
||||
} catch (error) {
|
||||
console.error('刷新权限失败:', error)
|
||||
// 可选:保留原有权限
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Getters / Helpers
|
||||
@ -66,12 +113,24 @@ export const useUserStore = defineStore('user', () => {
|
||||
return roles.includes(role.value)
|
||||
}
|
||||
|
||||
// 判断当前用户是否拥有某个权限(菜单或元素)
|
||||
const hasPermission = (code: string) => {
|
||||
// 超级管理员拥有所有权限
|
||||
if (role.value && role.value.toUpperCase() === 'SUPER_ADMIN') {
|
||||
return true
|
||||
}
|
||||
return permissions.value.includes(code)
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
role,
|
||||
username,
|
||||
permissions,
|
||||
handleLogin,
|
||||
logout,
|
||||
hasRole
|
||||
refreshUserPermissions,
|
||||
hasRole,
|
||||
hasPermission
|
||||
}
|
||||
})
|
||||
|
||||
@ -17,29 +17,29 @@
|
||||
<el-button :icon="Search" @click="fetchBomList" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button type="primary" :icon="Plus" @click="handleCreate">新建 BOM</el-button>
|
||||
<el-button v-if="userStore.hasPermission('bom_manage:operation')" type="primary" :icon="Plus" @click="handleCreate">新建 BOM</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" :data="bomList" border style="width: 100%">
|
||||
<el-table-column prop="bom_no" label="BOM编号" min-width="180" sortable />
|
||||
<el-table-column prop="parent_name" label="父件名称" min-width="150" />
|
||||
<el-table-column prop="parent_spec" label="父件规格" min-width="150" />
|
||||
<el-table-column prop="version" label="版本" width="100" align="center">
|
||||
<el-table-column v-if="hasColumnPermission('bom_no')" prop="bom_no" label="BOM编号" min-width="180" sortable />
|
||||
<el-table-column v-if="hasColumnPermission('parent_name')" prop="parent_name" label="父件名称" min-width="150" />
|
||||
<el-table-column v-if="hasColumnPermission('parent_spec')" prop="parent_spec" label="父件规格" min-width="150" />
|
||||
<el-table-column v-if="hasColumnPermission('version')" prop="version" label="版本" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag>{{ row.version }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<el-table-column v-if="hasColumnPermission('status')" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_enabled ? 'success' : 'danger'">
|
||||
{{ row.is_enabled ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="child_count" label="子件数" width="80" align="center" />
|
||||
<el-table-column label="操作" width="250" align="center" fixed="right">
|
||||
<el-table-column v-if="hasColumnPermission('child_count')" prop="child_count" label="子件数" width="80" align="center" />
|
||||
<el-table-column v-if="userStore.hasPermission('bom_manage:operation')" label="操作" width="250" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="success" link @click="handleSaveAs(row)">另存为</el-button>
|
||||
@ -54,7 +54,7 @@
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="16">
|
||||
<el-form-item label="父件 (成品)" prop="parent_id">
|
||||
<el-form-item label="父件 (成品)" prop="parent_id" v-if="hasFormFieldPermission('parent_id')">
|
||||
<el-select
|
||||
v-model="form.parent_id"
|
||||
placeholder="请搜索并选择父件"
|
||||
@ -79,15 +79,15 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="是否启用" prop="is_enabled">
|
||||
<el-switch v-model="form.is_enabled" active-text="启用" inactive-text="禁用" />
|
||||
<el-form-item label="是否启用" prop="is_enabled" v-if="hasFormFieldPermission('is_enabled')">
|
||||
<el-switch v-model="form.is_enabled" active-text="启用" inactive-text="禁用" :disabled="!userStore.hasPermission('bom_manage:operation')" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="14">
|
||||
<el-form-item label="BOM 编号" required>
|
||||
<el-form-item label="BOM 编号" required v-if="hasFormFieldPermission('bom_suffix')">
|
||||
<el-input v-model="form.bom_suffix" placeholder="输入后缀 (如 -001)" :disabled="isEditMode">
|
||||
<template #prepend v-if="form.bom_prefix">{{ form.bom_prefix }}</template>
|
||||
</el-input>
|
||||
@ -97,7 +97,7 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="10">
|
||||
<el-form-item label="版本号" prop="version">
|
||||
<el-form-item label="版本号" prop="version" v-if="hasFormFieldPermission('version')">
|
||||
<el-input v-model="form.version" placeholder="如: V1.0" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@ -106,7 +106,7 @@
|
||||
<div style="font-weight: bold; margin: 15px 0 10px 0; border-left: 4px solid #409EFF; padding-left: 10px;">子件列表</div>
|
||||
|
||||
<el-table :data="form.children" border style="width: 100%; margin-bottom: 15px" max-height="300">
|
||||
<el-table-column label="子件物料" min-width="280">
|
||||
<el-table-column label="子件物料" min-width="280" v-if="hasFormFieldPermission('child_id')">
|
||||
<template #default="{ row, $index }">
|
||||
<el-select
|
||||
v-model="row.child_id"
|
||||
@ -129,26 +129,26 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="用量" width="140">
|
||||
<el-table-column label="用量" width="140" v-if="hasFormFieldPermission('dosage')">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.dosage" :min="0" :precision="4" style="width: 100%" controls-position="right" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="备注" width="150">
|
||||
<el-table-column label="备注" width="150" v-if="hasFormFieldPermission('remark')">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.remark" placeholder="备注" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="60" align="center">
|
||||
<el-table-column label="操作" width="60" align="center" v-if="userStore.hasPermission('bom_manage:operation')">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" link @click="removeChild($index)">删</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div style="margin-top: 10px; text-align: center;">
|
||||
<div style="margin-top: 10px; text-align: center;" v-if="hasFormFieldPermission('child_id')">
|
||||
<el-button type="primary" plain :icon="Plus" @click="addChild" style="width: 100%">添加一行子件</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
@ -169,6 +169,7 @@ import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'
|
||||
import { Plus, Search } from '@element-plus/icons-vue'
|
||||
import { getBomList, getBomDetail, saveBom, deleteBom } from '@/api/bom'
|
||||
import { getMaterialBaseList } from '@/api/inbound/stock'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
// 类型定义
|
||||
interface BomItem {
|
||||
@ -190,6 +191,7 @@ interface ChildRow {
|
||||
remark: string
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
@ -199,6 +201,41 @@ const bomList = ref<BomItem[]>([])
|
||||
const materialOptions = ref<MaterialBase[]>([])
|
||||
const searchKeyword = ref('')
|
||||
|
||||
// 列与权限Code的映射关系(数据库中的code)
|
||||
const permissionMap: Record<string, string> = {
|
||||
bom_no: 'bom_manage:bom_no',
|
||||
parent_name: 'bom_manage:parent_name',
|
||||
parent_spec: 'bom_manage:parent_spec',
|
||||
version: 'bom_manage:version',
|
||||
status: 'bom_manage:status',
|
||||
child_count: 'bom_manage:child_count',
|
||||
// 表单字段
|
||||
parent_id: 'bom_manage:parent_id',
|
||||
is_enabled: 'bom_manage:status',
|
||||
bom_suffix: 'bom_manage:bom_no',
|
||||
child_id: 'bom_manage:child_id',
|
||||
dosage: 'bom_manage:dosage',
|
||||
remark: 'bom_manage:remark',
|
||||
}
|
||||
|
||||
// 检查列权限
|
||||
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 hasFormFieldPermission = (fieldName: string) => {
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||||
return true
|
||||
}
|
||||
const code = permissionMap[fieldName]
|
||||
return code ? userStore.hasPermission(code) : false
|
||||
}
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const form = reactive({
|
||||
bom_prefix: '', // 自动生成的父件规格前缀
|
||||
@ -229,7 +266,9 @@ const fetchBomList = async () => {
|
||||
try {
|
||||
const res = await getBomList({ keyword: searchKeyword.value })
|
||||
if (res.code === 200) bomList.value = res.data
|
||||
} catch (error) { ElMessage.error('网络错误') }
|
||||
} catch (error) {
|
||||
// 错误已由全局拦截器统一处理
|
||||
}
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
@ -237,7 +276,9 @@ const fetchMaterialOptions = async () => {
|
||||
try {
|
||||
const res = await getMaterialBaseList()
|
||||
if (res.code === 200) materialOptions.value = res.data
|
||||
} catch (error) {}
|
||||
} catch (error) {
|
||||
// 错误已由全局拦截器统一处理
|
||||
}
|
||||
}
|
||||
|
||||
// 监听父件变化,自动设置前缀
|
||||
@ -300,7 +341,9 @@ const loadDetail = async (bomNo: string, version: string) => {
|
||||
form.bom_suffix = bomNo
|
||||
}
|
||||
}
|
||||
} catch (e) { ElMessage.error('获取详情失败') }
|
||||
} catch (e) {
|
||||
// 错误已由全局拦截器统一处理
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = (row: BomItem) => {
|
||||
@ -353,7 +396,9 @@ const submitForm = async () => {
|
||||
dialogVisible.value = false
|
||||
fetchBomList()
|
||||
} else { ElMessage.error(res.msg || '保存失败') }
|
||||
} catch (e) { ElMessage.error('网络错误') }
|
||||
} catch (e) {
|
||||
// 错误已由全局拦截器统一处理
|
||||
}
|
||||
finally { saving.value = false }
|
||||
})
|
||||
}
|
||||
@ -371,4 +416,4 @@ onMounted(() => {
|
||||
.option-row { display: flex; justify-content: space-between; width: 100%; }
|
||||
.option-name { font-weight: bold; color: #303133; }
|
||||
.option-spec { font-size: 12px; color: #909399; margin-left: 15px; }
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -52,11 +52,13 @@
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { ElMessageBox } from 'element-plus' // 引入 ElMessageBox
|
||||
import { usePermissionStore } from '@/stores/permission' // [新增] 引入权限Store
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { User, Lock } from '@element-plus/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const permissionStore = usePermissionStore() // [新增]
|
||||
const loading = ref(false)
|
||||
const loginFormRef = ref()
|
||||
|
||||
@ -74,23 +76,25 @@ const onLogin = async () => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
try {
|
||||
// 执行登录请求
|
||||
// 1. 执行登录请求
|
||||
const success = await userStore.handleLogin(loginForm)
|
||||
|
||||
if (success) {
|
||||
// 成功:跳转
|
||||
// [新增] 2. 登录成功后,立即拉取当前用户的权限字典
|
||||
// 这样进入 Dashboard 时,所有按钮/列的显示状态就已经确定了
|
||||
await permissionStore.loadPermissions()
|
||||
|
||||
// 3. 跳转
|
||||
router.push('/dashboard')
|
||||
} else {
|
||||
// 失败(业务逻辑拒绝,如账号密码错):弹出模态框
|
||||
// 失败(业务逻辑拒绝):弹出模态框
|
||||
showLoginFailAlert('用户名或密码错误')
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 失败(系统错误,如网络断开/500报错):弹出模态框
|
||||
// 优先取后端的报错信息,没有则显示默认
|
||||
// 失败(系统错误):弹出模态框
|
||||
const msg = error.response?.data?.msg || error.message || '登录遇到未知错误'
|
||||
showLoginFailAlert(msg)
|
||||
} finally {
|
||||
// 停止转圈,让用户可以看清弹窗
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
@ -103,8 +107,6 @@ const showLoginFailAlert = (msg: string) => {
|
||||
confirmButtonText: '确定',
|
||||
type: 'error',
|
||||
callback: () => {
|
||||
// 点击确定后,清空密码框,让用户重试
|
||||
// 页面绝对不会刷新,光标还在
|
||||
loginForm.password = ''
|
||||
}
|
||||
})
|
||||
|
||||
@ -71,7 +71,7 @@
|
||||
<el-icon style="margin-right: 5px"><Download /></el-icon>导出库存统计
|
||||
</el-button>
|
||||
|
||||
<el-button type="primary" @click="handleAdd" style="margin-right: 10px">
|
||||
<el-button v-if="userStore.hasPermission('material_list:operation')" type="primary" @click="handleAdd" style="margin-right: 10px">
|
||||
<el-icon style="margin-right: 5px"><Plus /></el-icon>新增
|
||||
</el-button>
|
||||
|
||||
@ -98,18 +98,18 @@
|
||||
<div style="font-weight: bold; margin-bottom: 5px; border-bottom: 1px solid #eee; padding-bottom: 5px">
|
||||
列展示设置
|
||||
</div>
|
||||
<el-checkbox v-model="columns.id.visible" label="ID" />
|
||||
<el-checkbox v-model="columns.companyName.visible" label="所属公司" />
|
||||
<el-checkbox v-model="columns.name.visible" label="名称" />
|
||||
<el-checkbox v-model="columns.commonName.visible" label="俗名" />
|
||||
<el-checkbox v-model="columns.category.visible" label="类别" />
|
||||
<el-checkbox v-model="columns.type.visible" label="类型" />
|
||||
<el-checkbox v-model="columns.spec.visible" label="规格型号" />
|
||||
<el-checkbox v-model="columns.unit.visible" label="单位" />
|
||||
<el-checkbox v-model="columns.inventory.visible" label="库存数" />
|
||||
<el-checkbox v-model="columns.available.visible" label="可用数" />
|
||||
<el-checkbox v-model="columns.files.visible" label="资料" />
|
||||
<el-checkbox v-model="columns.isEnabled.visible" label="状态" />
|
||||
<el-checkbox v-model="columns.id.visible" label="ID" :disabled="!userStore.hasPermission(permissionMap.id)" />
|
||||
<el-checkbox v-model="columns.companyName.visible" label="所属公司" :disabled="!userStore.hasPermission(permissionMap.companyName)" />
|
||||
<el-checkbox v-model="columns.name.visible" label="名称" :disabled="!userStore.hasPermission(permissionMap.name)" />
|
||||
<el-checkbox v-model="columns.commonName.visible" label="俗名" :disabled="!userStore.hasPermission(permissionMap.commonName)" />
|
||||
<el-checkbox v-model="columns.category.visible" label="类别" :disabled="!userStore.hasPermission(permissionMap.category)" />
|
||||
<el-checkbox v-model="columns.type.visible" label="类型" :disabled="!userStore.hasPermission(permissionMap.type)" />
|
||||
<el-checkbox v-model="columns.spec.visible" label="规格型号" :disabled="!userStore.hasPermission(permissionMap.spec)" />
|
||||
<el-checkbox v-model="columns.unit.visible" label="单位" :disabled="!userStore.hasPermission(permissionMap.unit)" />
|
||||
<el-checkbox v-model="columns.inventory.visible" label="库存数" :disabled="!userStore.hasPermission(permissionMap.inventory)" />
|
||||
<el-checkbox v-model="columns.available.visible" label="可用数" :disabled="!userStore.hasPermission(permissionMap.available)" />
|
||||
<el-checkbox v-model="columns.files.visible" label="资料" :disabled="!userStore.hasPermission(permissionMap.files)" />
|
||||
<el-checkbox v-model="columns.isEnabled.visible" label="状态" :disabled="!userStore.hasPermission(permissionMap.isEnabled)" />
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
@ -121,6 +121,7 @@
|
||||
border
|
||||
stripe
|
||||
:size="tableSize"
|
||||
@sort-change="handleSortChange"
|
||||
style="width: 100%; margin-top: 15px"
|
||||
>
|
||||
<el-table-column v-if="columns.id.visible" prop="id" label="ID" min-width="80" align="center" fixed="left" />
|
||||
@ -149,7 +150,7 @@
|
||||
<el-table-column v-if="columns.spec.visible" prop="spec" label="规格型号" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column v-if="columns.unit.visible" prop="unit" label="单位" min-width="80" align="center" />
|
||||
|
||||
<el-table-column v-if="columns.inventory.visible" prop="inventoryCount" label="库存数" min-width="100" align="center">
|
||||
<el-table-column v-if="columns.inventory.visible" prop="inventoryCount" label="库存数" min-width="100" align="center" sortable="custom">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ fontWeight: 'bold', color: row.inventoryCount > 0 ? '#67C23A' : '#909399' }">
|
||||
{{ row.inventoryCount }}
|
||||
@ -157,7 +158,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column v-if="columns.available.visible" prop="availableCount" label="可用数" min-width="100" align="center">
|
||||
<el-table-column v-if="columns.available.visible" prop="availableCount" label="可用数" min-width="100" align="center" sortable="custom">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ fontWeight: 'bold', color: row.availableCount > 0 ? '#409EFF' : '#909399' }">
|
||||
{{ row.availableCount }}
|
||||
@ -210,14 +211,15 @@
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
:loading="scope.row.statusLoading"
|
||||
:disabled="!userStore.hasPermission('material_list:operation')"
|
||||
@change="handleStatusChange(scope.row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="150" fixed="right" align="center">
|
||||
<el-table-column v-if="userStore.hasPermission('material_list:operation')" label="操作" min-width="150" fixed="right" align="center">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
|
||||
<el-button v-if="userStore.hasPermission('material_list:operation')" link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button v-if="userStore.hasPermission('material_list:operation')" link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@ -246,12 +248,12 @@
|
||||
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-form-item label="名称" prop="name" v-if="hasFieldPermission('name')">
|
||||
<el-input v-model="form.name" placeholder="内部名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="俗名" prop="commonName">
|
||||
<el-form-item label="俗名" prop="commonName" v-if="hasFieldPermission('commonName')">
|
||||
<el-input v-model="form.commonName" placeholder="标准名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@ -259,7 +261,7 @@
|
||||
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="所属公司" prop="companyName">
|
||||
<el-form-item label="所属公司" prop="companyName" v-if="hasFieldPermission('companyName')">
|
||||
<el-autocomplete
|
||||
v-model="form.companyName"
|
||||
:fetch-suggestions="querySearchCompany"
|
||||
@ -270,7 +272,7 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="类别" prop="category">
|
||||
<el-form-item label="类别" prop="category" v-if="hasFieldPermission('category')">
|
||||
<div style="display: flex; width: 100%; align-items: center;">
|
||||
<el-cascader
|
||||
v-model="tempCategoryPrefix"
|
||||
@ -298,7 +300,7 @@
|
||||
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="类型" prop="type">
|
||||
<el-form-item label="类型" prop="type" v-if="hasFieldPermission('type')">
|
||||
<el-autocomplete
|
||||
v-model="form.type"
|
||||
:fetch-suggestions="querySearchType"
|
||||
@ -309,7 +311,7 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="规格型号" prop="spec">
|
||||
<el-form-item label="规格型号" prop="spec" v-if="hasFieldPermission('spec')">
|
||||
<el-input v-model="form.spec" placeholder="请输入规格型号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@ -317,7 +319,7 @@
|
||||
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="计量单位" prop="unit">
|
||||
<el-form-item label="计量单位" prop="unit" v-if="hasFieldPermission('unit')">
|
||||
<el-input v-model="form.unit" placeholder="如: 个, 台, 米" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@ -329,7 +331,7 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="产品图" prop="generalImage">
|
||||
<el-form-item label="产品图" prop="generalImage" v-if="hasFieldPermission('files')">
|
||||
<div class="upload-container">
|
||||
<el-upload
|
||||
v-model:file-list="fileListImage"
|
||||
@ -357,7 +359,7 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="说明书" prop="generalManual">
|
||||
<el-form-item label="说明书" prop="generalManual" v-if="hasFieldPermission('files')">
|
||||
<div class="upload-container">
|
||||
<el-upload
|
||||
v-model:file-list="fileListManual"
|
||||
@ -385,7 +387,7 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="isEnabled">
|
||||
<el-form-item label="状态" prop="isEnabled" v-if="hasFieldPermission('isEnabled')">
|
||||
<el-radio-group v-model="form.isEnabled">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
@ -420,6 +422,7 @@ import { ref, reactive, onMounted, nextTick } from 'vue';
|
||||
import { Plus, Document, Refresh, Setting, Rank, Camera, Link, Download } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
|
||||
import {
|
||||
listMaterialBase,
|
||||
@ -432,6 +435,8 @@ import {
|
||||
import { uploadFile, deleteFile } from '@/api/common/upload';
|
||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// --- 类型定义 ---
|
||||
interface MaterialBaseVO {
|
||||
id: number;
|
||||
@ -459,6 +464,8 @@ interface QueryParams {
|
||||
type: string;
|
||||
company: string;
|
||||
isEnabled?: number;
|
||||
orderByColumn: string;
|
||||
isAsc: string | undefined;
|
||||
}
|
||||
|
||||
interface CascaderOption {
|
||||
@ -501,6 +508,52 @@ const columns = reactive({
|
||||
isEnabled: { visible: true }
|
||||
});
|
||||
|
||||
// 列与权限Code的映射关系(数据库中的code)
|
||||
const permissionMap: Record<string, string> = {
|
||||
id: 'material_list:id',
|
||||
companyName: 'material_list:companyName',
|
||||
name: 'material_list:name',
|
||||
commonName: 'material_list:commonName',
|
||||
category: 'material_list:category',
|
||||
type: 'material_list:type',
|
||||
spec: 'material_list:spec',
|
||||
unit: 'material_list:unit',
|
||||
inventory: 'material_list:inventoryCount',
|
||||
available: 'material_list:availableCount',
|
||||
files: 'material_list:files',
|
||||
isEnabled: 'material_list:isEnabled'
|
||||
};
|
||||
|
||||
// 根据用户权限初始化列显示状态
|
||||
const initColumnPermissions = () => {
|
||||
// 超级管理员跳过权限检查,显示所有列
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 普通用户:严格执行列级权限控制,没有权限的列必须隐藏
|
||||
Object.keys(columns).forEach(key => {
|
||||
const code = permissionMap[key];
|
||||
if (code) {
|
||||
// 如果不具备该权限,必须设为 false
|
||||
columns[key].visible = !!userStore.hasPermission(code);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 检查字段权限(用于表单)
|
||||
const hasFieldPermission = (field: string) => {
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||||
return true;
|
||||
}
|
||||
const code = permissionMap[field];
|
||||
// 如果permissionMap中没有该字段,默认允许
|
||||
if (!code) {
|
||||
return true;
|
||||
}
|
||||
return userStore.hasPermission(code);
|
||||
};
|
||||
|
||||
const companyOptions = ref<string[]>([]);
|
||||
const categoryOptions = ref<string[]>([]);
|
||||
const typeOptions = ref<string[]>([]);
|
||||
@ -516,7 +569,9 @@ const queryParams = reactive<QueryParams>({
|
||||
category: '',
|
||||
type: '',
|
||||
company: '',
|
||||
isEnabled: undefined
|
||||
isEnabled: undefined,
|
||||
orderByColumn: '',
|
||||
isAsc: undefined
|
||||
});
|
||||
|
||||
// --- 弹窗与表单相关 ---
|
||||
@ -704,6 +759,18 @@ const handleInputSearch = () => {
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const handleSortChange = ({ column, prop, order }: any) => {
|
||||
if (prop && (prop === 'inventoryCount' || prop === 'availableCount')) {
|
||||
queryParams.orderByColumn = prop;
|
||||
queryParams.isAsc = order === 'ascending' ? 'asc' : order === 'descending' ? 'desc' : undefined;
|
||||
} else {
|
||||
queryParams.orderByColumn = '';
|
||||
queryParams.isAsc = undefined;
|
||||
}
|
||||
queryParams.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNum = 1;
|
||||
getList();
|
||||
@ -715,6 +782,8 @@ const resetQuery = () => {
|
||||
queryParams.type = '';
|
||||
queryParams.company = '';
|
||||
queryParams.isEnabled = undefined;
|
||||
queryParams.orderByColumn = '';
|
||||
queryParams.isAsc = undefined;
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
@ -988,6 +1057,8 @@ const handleCameraConfirm = async (file: File) => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 先根据权限初始化列显示状态
|
||||
initColumnPermissions();
|
||||
getList();
|
||||
getOptionsList();
|
||||
});
|
||||
@ -1043,4 +1114,4 @@ onMounted(() => {
|
||||
.long-dropdown .el-select-dropdown__wrap {
|
||||
max-height: 600px !important; /* 可以根据屏幕大小适当调整 */
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -8,14 +8,14 @@
|
||||
<span class="subtitle">(请添加需要出库的物品)</span>
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="primary" :icon="Plus" @click="openManualSelect">
|
||||
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="primary" :icon="Plus" @click="openManualSelect">
|
||||
手动添加库存
|
||||
</el-button>
|
||||
<el-button type="warning" :icon="List" @click="openBomSelect">
|
||||
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="warning" :icon="List" @click="openBomSelect">
|
||||
按 BOM 套餐添加
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button type="success" :icon="Printer" :disabled="selectedItems.length === 0" @click="handlePreview">
|
||||
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="success" :icon="Printer" :disabled="selectedItems.length === 0" @click="handlePreview">
|
||||
生成预览 & 打印
|
||||
</el-button>
|
||||
</div>
|
||||
@ -71,6 +71,7 @@
|
||||
size="small"
|
||||
style="width: 100%"
|
||||
controls-position="right"
|
||||
:disabled="!userStore.hasPermission('outbound_selection:operation')"
|
||||
@change="(val) => handleMainQuantityChange(val, row)"
|
||||
/>
|
||||
</template>
|
||||
@ -78,7 +79,7 @@
|
||||
|
||||
<el-table-column label="操作" width="80" align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" link @click="removeRow($index)">移除</el-button>
|
||||
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="danger" link @click="removeRow($index)">移除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@ -133,6 +134,7 @@
|
||||
size="small"
|
||||
style="width: 100%"
|
||||
placeholder="0"
|
||||
:disabled="!userStore.hasPermission('outbound_selection:operation')"
|
||||
@click.stop
|
||||
@change="(val) => handleManualQuantityChange(val, row)"
|
||||
/>
|
||||
@ -144,7 +146,7 @@
|
||||
已勾选 {{ tempSelection.length }} 项
|
||||
</span>
|
||||
<el-button @click="manualDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmManualAdd">确认添加</el-button>
|
||||
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="primary" @click="confirmManualAdd">确认添加</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@ -161,7 +163,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="生产套数">
|
||||
<el-input-number v-model="bomSets" :min="1" label="套" style="width: 200px;" />
|
||||
<el-input-number v-model="bomSets" :min="1" label="套" style="width: 200px;" :disabled="!userStore.hasPermission('outbound_selection:operation')" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div style="margin-left: 100px; color: #909399; font-size: 12px;">
|
||||
@ -169,7 +171,7 @@
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="bomSelectVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmBomAdd">一键计算并添加</el-button>
|
||||
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="primary" @click="confirmBomAdd">一键计算并添加</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@ -204,11 +206,11 @@
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="previewVisible = false">取消</el-button>
|
||||
|
||||
<el-button type="warning" :icon="Download" :loading="exportLoading" @click="confirmExport">
|
||||
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="warning" :icon="Download" :loading="exportLoading" @click="confirmExport">
|
||||
导出 Excel
|
||||
</el-button>
|
||||
|
||||
<el-button type="primary" :icon="Printer" :loading="printLoading" @click="confirmPrint">
|
||||
<el-button v-if="userStore.hasPermission('outbound_selection:operation')" type="primary" :icon="Printer" :loading="printLoading" @click="confirmPrint">
|
||||
确认打印 (A4)
|
||||
</el-button>
|
||||
</span>
|
||||
@ -283,6 +285,9 @@ import { Printer, Search, Plus, Download, List } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElTable, ElMessageBox } from 'element-plus'
|
||||
import { getAllStock, printSelectionList } from '@/api/inbound/stock'
|
||||
import { getBomList, getBomDetail } from '@/api/bom'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// --- 状态变量 ---
|
||||
const selectedItems = ref<any[]>([])
|
||||
@ -598,4 +603,4 @@ const confirmExport = () => {
|
||||
.sig-label { font-size: 14px; margin-bottom: 40px; text-align: left; width: 100%; }
|
||||
.sig-line { border-bottom: 1px solid #000; width: 100%; height: 1px; display: block; }
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -17,10 +17,14 @@
|
||||
|
||||
<div class="scan-section">
|
||||
|
||||
<div class="camera-placeholder" @click="showCamera = true">
|
||||
<div v-if="userStore.hasPermission('outbound_create:operation')" class="camera-placeholder" @click="showCamera = true">
|
||||
<el-icon :size="40" color="#409EFF"><CameraFilled /></el-icon>
|
||||
<span class="text">点击开启全屏扫码</span>
|
||||
</div>
|
||||
<div v-else class="camera-placeholder" style="background-color: #f5f5f5; cursor: not-allowed;">
|
||||
<el-icon :size="40" color="#909399"><CameraFilled /></el-icon>
|
||||
<span class="text">无扫码权限</span>
|
||||
</div>
|
||||
|
||||
<div class="input-box">
|
||||
<el-input
|
||||
@ -30,12 +34,13 @@
|
||||
clearable
|
||||
ref="barcodeRef"
|
||||
size="large"
|
||||
:disabled="!userStore.hasPermission('outbound_create:operation')"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Scissor /></el-icon>
|
||||
</template>
|
||||
<template #append>
|
||||
<el-button @click="handleManualInput">添加</el-button>
|
||||
<el-button @click="handleManualInput" :disabled="!userStore.hasPermission('outbound_create:operation')">添加</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
@ -64,13 +69,14 @@
|
||||
:max="parseFloat(row.available_quantity)"
|
||||
size="small"
|
||||
style="width: 100px"
|
||||
:disabled="!userStore.hasPermission('outbound_create:operation')"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="60" align="center" fixed="right">
|
||||
<template #default="{$index}">
|
||||
<el-button type="danger" icon="Delete" circle size="small" @click="removeFromCart($index)" />
|
||||
<el-button v-if="userStore.hasPermission('outbound_create:operation')" type="danger" icon="Delete" circle size="small" @click="removeFromCart($index)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@ -120,7 +126,7 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="电子签名确认" required>
|
||||
<div class="signature-box" @click="openSignatureDialog">
|
||||
<div class="signature-box" @click="openSignatureDialog" v-if="userStore.hasPermission('outbound_create:operation')">
|
||||
<div v-if="signaturePreviewUrl" class="signed-img">
|
||||
<img :src="signaturePreviewUrl" alt="签名" />
|
||||
<span class="re-sign-tip">点击重签</span>
|
||||
@ -130,11 +136,17 @@
|
||||
<span>点击此处进行全屏签名</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="signature-box" style="background-color: #f5f5f5; cursor: not-allowed;">
|
||||
<div class="unsigned-placeholder">
|
||||
<el-icon :size="24"><EditPen /></el-icon>
|
||||
<span>无签名权限</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<div class="bottom-actions">
|
||||
<el-button @click="clearAll" icon="Refresh">清空</el-button>
|
||||
<el-button type="primary" size="large" :loading="loading" @click="submitForm" icon="Select">
|
||||
<el-button v-if="userStore.hasPermission('outbound_create:operation')" @click="clearAll" icon="Refresh">清空</el-button>
|
||||
<el-button v-if="userStore.hasPermission('outbound_create:operation')" type="primary" size="large" :loading="loading" @click="submitForm" icon="Select">
|
||||
确认出库
|
||||
</el-button>
|
||||
</div>
|
||||
@ -205,6 +217,8 @@ import { getStockByBarcode, submitOutbound, getOutboundList } from '@/api/outbou
|
||||
import { uploadFile } from '@/api/common/upload'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// --- 状态定义 ---
|
||||
const barcodeInput = ref('')
|
||||
const cartItems = ref<any[]>([])
|
||||
@ -212,7 +226,6 @@ const loading = ref(false)
|
||||
const showCamera = ref(false)
|
||||
const barcodeRef = ref()
|
||||
const formRef = ref()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 签名相关
|
||||
const showSignatureDialog = ref(false)
|
||||
@ -292,6 +305,10 @@ const onScanSuccess = (code: string) => {
|
||||
}
|
||||
|
||||
const handleManualInput = async () => {
|
||||
if (!userStore.hasPermission('outbound_create:operation')) {
|
||||
ElMessage.warning('无操作权限')
|
||||
return
|
||||
}
|
||||
const code = barcodeInput.value.trim()
|
||||
if (!code) return
|
||||
|
||||
@ -355,10 +372,18 @@ const handleManualInput = async () => {
|
||||
}
|
||||
|
||||
const removeFromCart = (index: number) => {
|
||||
if (!userStore.hasPermission('outbound_create:operation')) {
|
||||
ElMessage.warning('无操作权限')
|
||||
return
|
||||
}
|
||||
cartItems.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const clearAll = () => {
|
||||
if (!userStore.hasPermission('outbound_create:operation')) {
|
||||
ElMessage.warning('无操作权限')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm('确定清空所有已选商品吗?', '提示', { type: 'warning' })
|
||||
.then(() => {
|
||||
cartItems.value = []
|
||||
@ -372,6 +397,10 @@ const clearAll = () => {
|
||||
|
||||
// --- 提交逻辑 ---
|
||||
const submitForm = async () => {
|
||||
if (!userStore.hasPermission('outbound_create:operation')) {
|
||||
ElMessage.warning('无操作权限')
|
||||
return
|
||||
}
|
||||
if (!formRef.value) return
|
||||
if (cartItems.value.length === 0) return ElMessage.warning('请先添加商品')
|
||||
|
||||
@ -427,7 +456,13 @@ const submitForm = async () => {
|
||||
}
|
||||
|
||||
// --- 签名逻辑 (Canvas) ---
|
||||
const openSignatureDialog = () => { showSignatureDialog.value = true }
|
||||
const openSignatureDialog = () => {
|
||||
if (!userStore.hasPermission('outbound_create:operation')) {
|
||||
ElMessage.warning('无签名权限')
|
||||
return
|
||||
}
|
||||
showSignatureDialog.value = true
|
||||
}
|
||||
|
||||
const initCanvas = async () => {
|
||||
await nextTick()
|
||||
@ -618,4 +653,4 @@ onUnmounted(() => {
|
||||
.sidebar-actions { flex-direction: row; width: 100%; gap: 10px; }
|
||||
.sidebar-actions .el-button { flex: 1; height: 40px; }
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
<el-button type="primary" class="filter-item" style="margin-left: 10px;" @click="fetchData">查询</el-button>
|
||||
<el-button type="success" class="filter-item" @click="$router.push('/outbound/create')">新建出库</el-button>
|
||||
<el-button v-if="userStore.hasPermission('outbound_create:operation')" type="success" class="filter-item" @click="$router.push('/outbound/create')">新建出库</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
@ -35,18 +35,18 @@
|
||||
<div style="padding: 10px 40px; background: #fafafa;">
|
||||
<h4 style="margin: 0 0 10px 0; color: #409EFF;">商品明细 (按单价降序)</h4>
|
||||
<el-table :data="props.row.items" border size="small">
|
||||
<el-table-column prop="sku" label="SKU" width="150" />
|
||||
<el-table-column v-if="hasColumnPermission('sku')" prop="sku" label="SKU" width="150" />
|
||||
|
||||
<el-table-column prop="name" label="名称" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="material_type" label="类型" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="category" label="类别" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="spec_model" label="规格型号" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column v-if="hasColumnPermission('name')" prop="name" label="名称" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column v-if="hasColumnPermission('material_type')" prop="material_type" label="类型" width="120" show-overflow-tooltip />
|
||||
<el-table-column v-if="hasColumnPermission('category')" prop="category" label="类别" width="120" show-overflow-tooltip />
|
||||
<el-table-column v-if="hasColumnPermission('spec_model')" prop="spec_model" label="规格型号" min-width="150" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="quantity" label="数量" width="100" />
|
||||
<el-table-column prop="unit_price" label="单价" width="120">
|
||||
<el-table-column v-if="hasColumnPermission('quantity')" prop="quantity" label="数量" width="100" />
|
||||
<el-table-column v-if="hasColumnPermission('unit_price')" prop="unit_price" label="单价" width="120">
|
||||
<template #default="{row}">¥{{ row.unit_price }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="subtotal" label="小计">
|
||||
<el-table-column v-if="hasColumnPermission('subtotal')" prop="subtotal" label="小计">
|
||||
<template #default="{row}">
|
||||
<span style="color: #F56C6C; font-weight: bold;">¥{{ row.subtotal.toFixed(2) }}</span>
|
||||
</template>
|
||||
@ -56,33 +56,33 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="outbound_no" label="出库单号" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column v-if="hasColumnPermission('outbound_no')" prop="outbound_no" label="出库单号" min-width="200" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="outbound_time" label="出库时间" width="170" align="center">
|
||||
<el-table-column v-if="hasColumnPermission('outbound_time')" prop="outbound_time" label="出库时间" width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.outbound_time ? row.outbound_time.substring(0, 16) : '' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="outbound_type" label="类型" width="100" align="center">
|
||||
<el-table-column v-if="hasColumnPermission('outbound_type')" prop="outbound_type" label="类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getTagType(row.outbound_type)">{{ formatType(row.outbound_type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="total_amount" label="总金额" width="120" align="right">
|
||||
<el-table-column v-if="hasColumnPermission('total_amount')" prop="total_amount" label="总金额" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span style="color: #F56C6C; font-weight: bold; font-size: 15px;">¥{{ row.total_amount }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="consumer_name" label="领用/客户" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column v-if="hasColumnPermission('consumer_name')" prop="consumer_name" label="领用/客户" min-width="120" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="operator_name" label="操作员" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column v-if="hasColumnPermission('operator_name')" prop="operator_name" label="操作员" min-width="100" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column v-if="hasColumnPermission('remark')" prop="remark" label="备注" min-width="150" show-overflow-tooltip />
|
||||
|
||||
<el-table-column label="签名" width="120" align="center">
|
||||
<el-table-column v-if="hasColumnPermission('signature_path')" label="签名" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.signature_path" class="signature-cell">
|
||||
<el-image
|
||||
@ -121,6 +121,39 @@
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { getOutboundList } from '@/api/outbound'
|
||||
import { Picture } from '@element-plus/icons-vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 列与权限Code的映射关系(数据库中的code)
|
||||
const permissionMap: Record<string, string> = {
|
||||
outbound_no: 'outbound_list:outbound_no',
|
||||
outbound_time: 'outbound_list:outbound_time',
|
||||
outbound_type: 'outbound_list:outbound_type',
|
||||
total_amount: 'outbound_list:total_amount',
|
||||
consumer_name: 'outbound_list:consumer_name',
|
||||
operator_name: 'outbound_list:operator_name',
|
||||
remark: 'outbound_list:remark',
|
||||
signature_path: 'outbound_list:signature_path',
|
||||
// 明细列
|
||||
sku: 'outbound_list:sku',
|
||||
name: 'outbound_list:name',
|
||||
material_type: 'outbound_list:material_type',
|
||||
category: 'outbound_list:category',
|
||||
spec_model: 'outbound_list:spec_model',
|
||||
quantity: 'outbound_list:quantity',
|
||||
unit_price: 'outbound_list:unit_price',
|
||||
subtotal: 'outbound_list:subtotal',
|
||||
}
|
||||
|
||||
// 检查列权限
|
||||
const hasColumnPermission = (prop: string) => {
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||||
return true
|
||||
}
|
||||
const code = permissionMap[prop]
|
||||
return code ? userStore.hasPermission(code) : false
|
||||
}
|
||||
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
@ -198,4 +231,4 @@ onMounted(() => {
|
||||
background: #f5f7fa;
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="buy-module">
|
||||
<div class="header-container">
|
||||
<div class="search-form-area">
|
||||
<div class="header-container" style="flex-wrap: wrap;">
|
||||
<div class="search-form-area" style="flex-wrap: wrap;">
|
||||
|
||||
<el-select
|
||||
v-model="queryParams.company"
|
||||
@ -55,8 +55,8 @@
|
||||
<el-button class="reset-btn" @click="resetQuery">重置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="right-actions">
|
||||
<el-button type="primary" :icon="Plus" @click="handleCreate" class="add-btn">新增</el-button>
|
||||
<div class="right-actions" style="flex-wrap: wrap;">
|
||||
<el-button v-if="userStore.hasPermission('inbound_buy:operation')" type="primary" :icon="Plus" @click="handleCreate" class="add-btn">新增</el-button>
|
||||
<el-button :icon="Refresh" circle @click="fetchData" class="circle-btn" />
|
||||
|
||||
<el-popover placement="bottom-end" title="列配置" :width="500" trigger="click">
|
||||
@ -67,13 +67,13 @@
|
||||
<div class="col-group-title">基础信息</div>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12" v-for="c in baseColumns" :key="c.prop">
|
||||
<el-checkbox :label="c.prop">{{ c.label }}</el-checkbox>
|
||||
<el-checkbox :label="c.prop" :disabled="!hasColumnPermission(c.prop)">{{ c.label }}</el-checkbox>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="col-group-title" style="margin-top:10px">库存与商务</div>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12" v-for="c in stockColumns" :key="c.prop">
|
||||
<el-checkbox :label="c.prop">{{ c.label }}</el-checkbox>
|
||||
<el-checkbox :label="c.prop" :disabled="!hasColumnPermission(c.prop)">{{ c.label }}</el-checkbox>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-checkbox-group>
|
||||
@ -167,7 +167,7 @@
|
||||
</el-table-column>
|
||||
</template>
|
||||
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<el-table-column v-if="userStore.hasPermission('inbound_buy:operation')" label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="warning" size="default" @click="handlePrint(row)">
|
||||
<el-icon><Printer/></el-icon> 打印
|
||||
@ -197,7 +197,7 @@
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogStatus === 'create' ? '新增采购入库' : '编辑入库信息'"
|
||||
width="1000px"
|
||||
:width="'min(1000px, 95vw)'"
|
||||
top="4vh"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@ -272,12 +272,12 @@
|
||||
|
||||
<div class="read-only-grid">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="所属公司"><el-input v-model="form.company_name" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类别"><el-input v-model="form.category" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="规格型号"><el-input v-model="form.spec_model" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="单位"><el-input v-model="form.unit" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="所属公司" v-if="hasFormFieldPermission('company_name')"><el-input v-model="form.company_name" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="名称" v-if="hasFormFieldPermission('material_name')"><el-input v-model="form.material_name" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类型" v-if="hasFormFieldPermission('material_type')"><el-input v-model="form.material_type" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类别" v-if="hasFormFieldPermission('category')"><el-input v-model="form.category" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="规格型号" v-if="hasFormFieldPermission('spec_model')"><el-input v-model="form.spec_model" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="单位" v-if="hasFormFieldPermission('unit')"><el-input v-model="form.unit" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
@ -296,9 +296,6 @@
|
||||
<el-col :span="6">
|
||||
<el-form-item label="入库日期" prop="in_date"><el-date-picker v-model="form.in_date" type="date" value-format="YYYY-MM-DD" style="width:100%" disabled/></el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="条码" prop="barcode"><el-input v-model="form.barcode" placeholder="扫描条码" clearable/></el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="库位" prop="warehouse_location">
|
||||
<el-autocomplete
|
||||
@ -346,7 +343,7 @@
|
||||
<el-row :gutter="20" style="margin-top: 10px;">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="入库数量" prop="in_quantity">
|
||||
<el-input-number v-model="form.in_quantity" :min="1" controls-position="right" style="width:100%" class="strong-input"/>
|
||||
<el-input-number v-model="form.in_quantity" :min="1" controls-position="right" style="width:100%" class="strong-input" @change="updatePrices('qty')"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
@ -423,31 +420,72 @@
|
||||
</el-row>
|
||||
|
||||
<div class="divider-text">商务与采购信息</div>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="币种">
|
||||
<el-autocomplete v-model="form.currency" :fetch-suggestions="querySearchCurrency" placeholder="币种" style="width: 100%" :trigger-on-focus="true">
|
||||
<template #default="{ item }"><span>{{ item.value }}</span><span style="float:right; color:#999; font-size:12px">{{ item.desc }}</span></template>
|
||||
</el-autocomplete>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6"><el-form-item label="汇率"><el-input-number v-model="form.exchange_rate" :precision="2" controls-position="right" style="width:100%"/></el-form-item></el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="汇率">
|
||||
<el-input-number v-model="form.exchange_rate" :precision="2" controls-position="right" style="width:100%"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="税率">
|
||||
<el-select v-model="form.tax_rate" style="width:100%">
|
||||
<el-select v-model="form.tax_rate" style="width:100%" @change="updatePrices('tax')">
|
||||
<el-option label="0%" :value="0" />
|
||||
<el-option label="1%" :value="1" />
|
||||
<el-option label="13%" :value="13" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6"><el-form-item label="不含税单价" prop="unit_price"><el-input-number v-model="form.unit_price" :precision="4" controls-position="right" style="width:100%"/></el-form-item></el-col>
|
||||
|
||||
<el-col :span="6"><el-form-item label="总价"><el-input-number v-model="form.total_price" :precision="2" disabled :controls="false" style="width:100%" class="total-price-input"/></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
|
||||
<el-row :gutter="20" style="margin-top: 15px;">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="不含税单价" prop="unit_price">
|
||||
<el-input-number
|
||||
v-model="form.unit_price"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
style="width:100%"
|
||||
placeholder="请输入"
|
||||
@change="updatePrices('pre')"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="含税单价">
|
||||
<el-input-number
|
||||
v-model="form.post_tax_unit_price"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
style="width:100%"
|
||||
placeholder="请输入"
|
||||
@change="updatePrices('post')"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="不含税总价">
|
||||
<el-input-number
|
||||
v-model="form.total_price"
|
||||
:precision="2"
|
||||
disabled
|
||||
:controls="false"
|
||||
style="width:100%"
|
||||
class="total-price-input"
|
||||
placeholder="自动计算"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="margin-top: 15px;">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="供应商">
|
||||
<el-autocomplete
|
||||
@ -587,6 +625,7 @@ import {
|
||||
} from '@/api/inbound/buy'
|
||||
import {getLabelPreview, executePrint} from '@/api/common/print'
|
||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
// ------------------------------------
|
||||
// 自定义指令:v-loadmore (适配 Teleport 到 Body 的下拉框)
|
||||
@ -610,9 +649,59 @@ const vLoadmore = {
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// 表单字段权限检查
|
||||
// ------------------------------------
|
||||
const hasFormFieldPermission = (fieldName: string) => {
|
||||
// 超级管理员直接返回true
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||||
return true
|
||||
}
|
||||
// 根据字段名映射到权限码
|
||||
const map: Record<string, string> = {
|
||||
company_name: 'inbound_buy:company_name',
|
||||
material_name: 'inbound_buy:material_name',
|
||||
spec_model: 'inbound_buy:spec_model',
|
||||
category: 'inbound_buy:category',
|
||||
material_type: 'inbound_buy:material_type',
|
||||
unit: 'inbound_buy:unit',
|
||||
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',
|
||||
inspection_status: 'inbound_buy:inspection_status',
|
||||
in_quantity: 'inbound_buy:in_quantity',
|
||||
stock_quantity: 'inbound_buy:stock_quantity',
|
||||
available_quantity: 'inbound_buy:available_quantity',
|
||||
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',
|
||||
purchaser: 'inbound_buy:purchaser',
|
||||
purchaser_email: 'inbound_buy:purchaser_email',
|
||||
source_link: 'inbound_buy:original_link',
|
||||
detail_link: 'inbound_buy:detail_link',
|
||||
arrival_photo: 'inbound_buy:arrival_photo',
|
||||
inspection_report: 'inbound_buy:inspection_report',
|
||||
print_copies: 'inbound_buy:print_copies',
|
||||
}
|
||||
const code = map[fieldName]
|
||||
if (!code) {
|
||||
// 没有映射的字段默认显示
|
||||
return true
|
||||
}
|
||||
return userStore.hasPermission(code)
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// 状态与变量
|
||||
// ------------------------------------
|
||||
const userStore = useUserStore()
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const visible = ref(false)
|
||||
@ -689,7 +778,7 @@ const stockColumns = [
|
||||
{prop: 'tax_rate', label: '税率', minWidth: '80'},
|
||||
{prop: 'unit_price', label: '不含税单价', minWidth: '120'},
|
||||
|
||||
{prop: 'total_price', label: '总价', minWidth: '120'},
|
||||
{prop: 'total_price', label: '不含税总价', minWidth: '120'},
|
||||
{prop: 'currency', label: '币种', minWidth: '80'},
|
||||
{prop: 'exchange_rate', label: '汇率', minWidth: '80'},
|
||||
{prop: 'supplier_name', label: '供应商', minWidth: '150'},
|
||||
@ -701,6 +790,78 @@ const stockColumns = [
|
||||
{prop: 'inspection_report', label: '检测报告', minWidth: '100'}
|
||||
]
|
||||
|
||||
// 列与权限Code的映射关系(数据库中的code)
|
||||
const permissionMap: Record<string, string> = {
|
||||
id: 'inbound_buy:id',
|
||||
base_id: 'inbound_buy:base_id',
|
||||
company_name: 'inbound_buy:company_name',
|
||||
material_name: 'inbound_buy:material_name',
|
||||
material_type: 'inbound_buy:material_type',
|
||||
category: 'inbound_buy:category',
|
||||
spec_model: 'inbound_buy:spec_model',
|
||||
unit: 'inbound_buy:unit',
|
||||
sku: 'inbound_buy:sku',
|
||||
inbound_date: 'inbound_buy:inbound_date',
|
||||
barcode: 'inbound_buy:barcode',
|
||||
sn_bn: 'inbound_buy:sn_bn',
|
||||
status: 'inbound_buy:status',
|
||||
inspection_status: 'inbound_buy:inspection_status',
|
||||
qty_inbound: 'inbound_buy:qty_inbound',
|
||||
qty_stock: 'inbound_buy:qty_stock',
|
||||
qty_available: 'inbound_buy:qty_available',
|
||||
warehouse_loc: 'inbound_buy:warehouse_loc',
|
||||
tax_rate: 'inbound_buy:tax_rate',
|
||||
unit_price: 'inbound_buy:unit_price',
|
||||
total_price: 'inbound_buy:total_price',
|
||||
currency: 'inbound_buy:currency',
|
||||
exchange_rate: 'inbound_buy:exchange_rate',
|
||||
supplier_name: 'inbound_buy:supplier_name',
|
||||
purchaser: 'inbound_buy:purchaser',
|
||||
purchaser_email: 'inbound_buy:purchaser_email',
|
||||
source_link: 'inbound_buy:source_link',
|
||||
detail_link: 'inbound_buy:detail_link',
|
||||
arrival_photo: 'inbound_buy:arrival_photo',
|
||||
inspection_report: 'inbound_buy:inspection_report'
|
||||
}
|
||||
|
||||
// 根据用户权限初始化列显示状态
|
||||
const initColumnPermissions = () => {
|
||||
// 超级管理员跳过权限检查,显示所有列
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||||
return
|
||||
}
|
||||
|
||||
// 普通用户:严格执行列级权限控制,没有权限的列必须隐藏
|
||||
// 遍历 allColumns,将没有权限的列从 visibleColumnProps 中移除
|
||||
const allowedColumns = allColumns.filter(col => {
|
||||
const code = permissionMap[col.prop]
|
||||
if (code) {
|
||||
return userStore.hasPermission(code)
|
||||
}
|
||||
// 如果没有映射,默认隐藏
|
||||
return false
|
||||
}).map(col => col.prop)
|
||||
|
||||
// 更新 visibleColumnProps,只保留有权限的列
|
||||
// 同时保持用户之前已经选择的有权限的列
|
||||
const currentVisible = visibleColumnProps.value.filter(prop => allowedColumns.includes(prop))
|
||||
// 如果当前没有可见列,则使用 allowedColumns 作为默认
|
||||
if (currentVisible.length === 0) {
|
||||
visibleColumnProps.value = allowedColumns
|
||||
} else {
|
||||
visibleColumnProps.value = currentVisible
|
||||
}
|
||||
}
|
||||
|
||||
// 检查列权限
|
||||
const hasColumnPermission = (prop: string) => {
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||||
return true
|
||||
}
|
||||
const code = permissionMap[prop]
|
||||
return code ? userStore.hasPermission(code) : false
|
||||
}
|
||||
|
||||
const allColumns = [...baseColumns, ...stockColumns]
|
||||
|
||||
const defaultColumns = [
|
||||
@ -719,7 +880,9 @@ const form = reactive({
|
||||
material_name: '', spec_model: '', category: '', unit: '', material_type: '',
|
||||
sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', inspection_status: '未检',
|
||||
in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '',
|
||||
unit_price: 0, total_price: 0,
|
||||
unit_price: undefined as number | undefined,
|
||||
post_tax_unit_price: undefined as number | undefined,
|
||||
total_price: undefined as number | undefined,
|
||||
tax_rate: 0,
|
||||
currency: 'CNY', exchange_rate: 1.00,
|
||||
supplier_name: '', purchaser: '', purchaser_email: '', source_link: '', detail_link: '',
|
||||
@ -727,7 +890,6 @@ const form = reactive({
|
||||
print_copies: 1
|
||||
})
|
||||
|
||||
|
||||
// ------------------------------------
|
||||
// 建议/Autocomplete 逻辑
|
||||
// ------------------------------------
|
||||
@ -916,7 +1078,45 @@ const handleEntryModeChange = (val: string) => {
|
||||
if(formRef.value) formRef.value.clearValidate('batch_number')
|
||||
}
|
||||
}
|
||||
watch(() => [form.in_quantity, form.unit_price], () => { form.total_price = Number((form.in_quantity * form.unit_price).toFixed(4)) })
|
||||
// 价格联动计算 (精确到小数点后2位,并支持空值)
|
||||
const updatePrices = (source: string) => {
|
||||
const taxMultiplier = 1 + (form.tax_rate || 0) / 100;
|
||||
if (source === 'pre') {
|
||||
if (form.unit_price !== undefined && form.unit_price !== null) {
|
||||
form.post_tax_unit_price = Number((form.unit_price * taxMultiplier).toFixed(2));
|
||||
} else {
|
||||
form.post_tax_unit_price = undefined;
|
||||
}
|
||||
} else if (source === 'post') {
|
||||
if (form.post_tax_unit_price !== undefined && form.post_tax_unit_price !== null) {
|
||||
form.unit_price = Number((form.post_tax_unit_price / taxMultiplier).toFixed(2));
|
||||
} else {
|
||||
form.unit_price = undefined;
|
||||
}
|
||||
} else if (source === 'tax') {
|
||||
if (form.unit_price !== undefined && form.unit_price !== null) {
|
||||
form.post_tax_unit_price = Number((form.unit_price * taxMultiplier).toFixed(2));
|
||||
}
|
||||
}
|
||||
|
||||
if (form.in_quantity !== undefined && form.unit_price !== undefined && form.unit_price !== null) {
|
||||
form.total_price = Number((form.in_quantity * form.unit_price).toFixed(2));
|
||||
} else {
|
||||
form.total_price = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => [form.in_quantity, form.unit_price], () => {
|
||||
if (form.unit_price !== undefined && form.unit_price !== null) {
|
||||
form.total_price = Number((form.in_quantity * form.unit_price).toFixed(2));
|
||||
// 同时更新含税单价
|
||||
const taxMultiplier = 1 + (form.tax_rate || 0) / 100;
|
||||
form.post_tax_unit_price = Number((form.unit_price * taxMultiplier).toFixed(2));
|
||||
} else {
|
||||
form.total_price = undefined;
|
||||
form.post_tax_unit_price = undefined;
|
||||
}
|
||||
})
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
@ -975,13 +1175,20 @@ const handleUpdate = (row: any) => {
|
||||
unit: row.unit, material_type: row.material_type, sku: row.sku, barcode: row.barcode, in_date: row.inbound_date,
|
||||
warehouse_location: row.warehouse_loc, status: row.status, inspection_status: row.inspection_status,
|
||||
in_quantity: Number(row.qty_inbound), stock_quantity: Number(row.qty_stock), available_quantity: Number(row.qty_available),
|
||||
unit_price: Number(row.unit_price), total_price: Number(row.total_price),
|
||||
unit_price: (row.unit_price !== null && row.unit_price !== undefined) ? Number(row.unit_price) : undefined,
|
||||
total_price: (row.total_price !== null && row.total_price !== undefined) ? Number(row.total_price) : undefined,
|
||||
tax_rate: Number(row.tax_rate),
|
||||
currency: row.currency, exchange_rate: Number(row.exchange_rate),
|
||||
supplier_name: row.supplier_name, purchaser: row.purchaser, purchaser_email: row.purchaser_email,
|
||||
source_link: row.source_link, detail_link: row.detail_link,
|
||||
arrival_photo: row.arrival_photo || [], inspection_report: row.inspection_report || []
|
||||
})
|
||||
// 计算含税单价
|
||||
if (form.unit_price !== undefined && form.unit_price !== null) {
|
||||
const taxMultiplier = 1 + (form.tax_rate || 0) / 100;
|
||||
form.post_tax_unit_price = Number((form.unit_price * taxMultiplier).toFixed(2));
|
||||
}
|
||||
|
||||
arrivalFileList.value = form.arrival_photo.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
||||
const reports = form.inspection_report || []
|
||||
const reportImgs = reports.filter(r => !isExternalLink(r))
|
||||
@ -1004,7 +1211,13 @@ const submitForm = async () => {
|
||||
const onlyImages = finalReportList.filter(item => !isExternalLink(item))
|
||||
if (inspection_report_url.value) onlyImages.push(inspection_report_url.value)
|
||||
|
||||
const payload = { ...form, inspection_report: onlyImages, in_quantity: Number(form.in_quantity), unit_price: Number(form.unit_price) }
|
||||
const payload = {
|
||||
...form,
|
||||
inspection_report: onlyImages,
|
||||
in_quantity: Number(form.in_quantity || 0),
|
||||
unit_price: Number(form.unit_price || 0),
|
||||
post_tax_unit_price: Number(form.post_tax_unit_price || 0)
|
||||
}
|
||||
try {
|
||||
if (dialogStatus.value === 'create') {
|
||||
const res: any = await createBuyInbound(payload)
|
||||
@ -1170,16 +1383,26 @@ const resetForm = () => {
|
||||
id: undefined, base_id: undefined,
|
||||
company_name: '',
|
||||
material_name: '', spec_model: '', category: '', unit: '', material_type: '', sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', inspection_status: '未检', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '',
|
||||
unit_price: 0, total_price: 0,
|
||||
unit_price: undefined, post_tax_unit_price: undefined, total_price: undefined,
|
||||
tax_rate: 0,
|
||||
currency: 'CNY', exchange_rate: 1.00, supplier_name: '', purchaser: '', purchaser_email: '', source_link: '', detail_link: '', arrival_photo: [], inspection_report: [],
|
||||
print_copies: 1
|
||||
})
|
||||
}
|
||||
const getStatusType = (status: string) => { const map: any = {'在库': 'success', '出库': 'info', '损耗': 'danger'}; return map[status] || 'warning' }
|
||||
const formatMoney = (val: any, currency = '¥') => { const num = Number(val); return isNaN(num) ? '-' : `${currency} ${num.toFixed(2)}` }
|
||||
|
||||
// 列表金额显示增加千分位处理,并保留2位小数
|
||||
const formatMoney = (val: any, currency = '¥') => {
|
||||
const num = Number(val);
|
||||
if (isNaN(num)) return '-';
|
||||
const parts = num.toFixed(2).split('.');
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
return `${currency} ${parts.join('.')}`;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 先根据权限初始化列显示状态
|
||||
initColumnPermissions()
|
||||
fetchData()
|
||||
fetchOptions()
|
||||
})
|
||||
@ -1360,6 +1583,11 @@ onMounted(() => {
|
||||
.camera-card:hover { border-color: #409EFF; color: #409EFF; }
|
||||
.camera-card .text { font-size: 12px; margin-top: 5px; }
|
||||
.camera-card .el-icon { font-size: 24px; }
|
||||
|
||||
/* 自定义千分位无箭头输入框样式,用于强迫症优化显示 */
|
||||
:deep(.el-input-number .el-input__inner) {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@ -16,22 +16,49 @@
|
||||
|
||||
<el-input
|
||||
v-model="queryParams.keyword"
|
||||
placeholder="🔍 搜索物料 / SN / 工单..."
|
||||
placeholder="请输入名称或规格"
|
||||
class="filter-item-input"
|
||||
clearable
|
||||
@clear="fetchData"
|
||||
@keyup.enter="fetchData"
|
||||
style="width: 260px;"
|
||||
style="width: 240px;"
|
||||
>
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
|
||||
<el-select
|
||||
v-model="queryParams.category"
|
||||
placeholder="类别"
|
||||
class="filter-item-select"
|
||||
clearable
|
||||
filterable
|
||||
@change="fetchData"
|
||||
style="width: 160px;"
|
||||
>
|
||||
<el-option v-for="item in categoryOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
|
||||
<el-select
|
||||
v-model="queryParams.material_type"
|
||||
placeholder="类型"
|
||||
class="filter-item-select"
|
||||
clearable
|
||||
filterable
|
||||
@change="fetchData"
|
||||
style="width: 160px;"
|
||||
>
|
||||
<el-option v-for="item in typeOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
|
||||
<el-button type="primary" plain class="search-btn" @click="fetchData">搜索</el-button>
|
||||
<el-button class="reset-btn" @click="resetQuery">重置</el-button>
|
||||
|
||||
<el-select
|
||||
v-model="queryParams.statuses"
|
||||
multiple
|
||||
collapse-tags
|
||||
placeholder="状态筛选"
|
||||
style="width: 220px;"
|
||||
style="width: 200px; margin-left: 10px;"
|
||||
@change="fetchData"
|
||||
>
|
||||
<el-option label="在库" value="在库" />
|
||||
@ -41,13 +68,13 @@
|
||||
</div>
|
||||
|
||||
<div class="right-tools">
|
||||
<el-button type="primary" :icon="Plus" @click="handleCreate" class="action-btn">成品入库登记</el-button>
|
||||
<el-button v-if="userStore.hasPermission('inbound_product:operation')" type="primary" :icon="Plus" @click="handleCreate" class="action-btn">成品入库登记</el-button>
|
||||
<el-button :icon="Refresh" @click="fetchData" class="action-btn">刷新</el-button>
|
||||
<el-popover placement="bottom-end" title="列配置" :width="500" trigger="click">
|
||||
<template #reference><el-button :icon="Setting" class="action-btn">表头</el-button></template>
|
||||
<el-checkbox-group v-model="visibleColumnProps" class="column-selector">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8" v-for="c in allColumns" :key="c.prop"><el-checkbox :label="c.prop">{{ c.label }}</el-checkbox></el-col>
|
||||
<el-col :span="8" v-for="c in allColumns" :key="c.prop"><el-checkbox :label="c.prop" :disabled="!hasColumnPermission(c.prop)">{{ c.label }}</el-checkbox></el-col>
|
||||
</el-row>
|
||||
</el-checkbox-group>
|
||||
</el-popover>
|
||||
@ -79,8 +106,7 @@
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="col.prop === 'company_name'">
|
||||
<el-tag v-if="scope.row.company_name" type="info" effect="plain" size="small" style="font-weight: bold;">{{ scope.row.company_name }}</el-tag>
|
||||
<span v-else>-</span>
|
||||
<span>{{ scope.row.company_name || '-' }}</span>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="['serial_number'].includes(col.prop)">
|
||||
@ -130,14 +156,14 @@
|
||||
</el-link>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="['sale_price', 'raw_material_cost', 'manual_cost'].includes(col.prop)">
|
||||
<template #default="scope" v-else-if="['sale_price', 'raw_material_cost', 'unit_total_cost', 'total_price'].includes(col.prop)">
|
||||
<span class="money-text">{{ formatMoney(scope.row[col.prop]) }}</span>
|
||||
</template>
|
||||
|
||||
</el-table-column>
|
||||
</template>
|
||||
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<el-table-column v-if="userStore.hasPermission('inbound_product:operation')" label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="warning" size="default" @click="handlePrint(row)">
|
||||
<el-icon><Printer/></el-icon>
|
||||
@ -150,7 +176,7 @@
|
||||
|
||||
<el-pagination class="pagination-bar" v-model:current-page="queryParams.page" v-model:page-size="queryParams.pageSize" :total="total" layout="total, sizes, prev, pager, next" background @change="fetchData" />
|
||||
|
||||
<el-dialog v-model="visible" :title="dialogStatus === 'create' ? '成品入库' : '编辑成品'" width="1100px" top="5vh" :close-on-click-modal="false" class="stylish-dialog compact-layout">
|
||||
<el-dialog v-model="visible" :title="dialogStatus === 'create' ? '成品入库' : '编辑成品'" width="min(1000px, 95vw)" top="5vh" :close-on-click-modal="false" class="stylish-dialog compact-layout">
|
||||
<div class="dialog-scroll-container">
|
||||
<el-form :model="form" label-width="110px" ref="formRef" :rules="rules" size="default" class="stylish-form">
|
||||
|
||||
@ -163,7 +189,7 @@
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<el-row :gutter="24" v-if="dialogStatus === 'create'" style="margin-bottom: 20px;">
|
||||
<el-col :span="10">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="物料搜索" prop="base_id" class="highlight-label">
|
||||
<el-select
|
||||
v-model="form.base_id"
|
||||
@ -171,7 +197,7 @@
|
||||
remote
|
||||
reserve-keyword
|
||||
clearable
|
||||
placeholder="搜名称/规格..."
|
||||
placeholder="请输入名称或规格进行检索..."
|
||||
:remote-method="handleSearchMaterial"
|
||||
@visible-change="handleMaterialDropdownVisible"
|
||||
:loading="searchLoading"
|
||||
@ -179,7 +205,7 @@
|
||||
@change="onMaterialSelected"
|
||||
default-first-option
|
||||
v-loadmore="loadMoreMaterials"
|
||||
popper-class="long-dropdown"
|
||||
popper-class="product-dropdown"
|
||||
>
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
<el-option v-for="item in materialOptions" :key="item.id" :label="item.name" :value="item.id">
|
||||
@ -203,20 +229,20 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="14" style="display: flex; align-items: center;">
|
||||
<el-col :span="12" style="display: flex; align-items: center;">
|
||||
<span class="search-tip">
|
||||
<el-icon><InfoFilled /></el-icon> 未输入时展示最新物料;输入关键词进行精确搜索。
|
||||
<el-icon><InfoFilled /></el-icon> 支持名称、规格型号、公司名称模糊搜索
|
||||
</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="read-only-grid">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8"><el-form-item label="所属公司"><el-input v-model="form.company_name" readonly class="is-text-view" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" readonly class="is-text-view" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="规格"><el-input v-model="form.spec_model" readonly class="is-text-view" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="单位"><el-input v-model="form.unit" readonly class="is-text-view" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" readonly class="is-text-view" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类别"><el-input v-model="form.category" readonly class="is-text-view" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="所属公司" v-if="hasFormFieldPermission('company_name')"><el-input v-model="form.company_name" readonly class="is-text-view" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="名称" v-if="hasFormFieldPermission('material_name')"><el-input v-model="form.material_name" readonly class="is-text-view" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="规格" v-if="hasFormFieldPermission('spec_model')"><el-input v-model="form.spec_model" readonly class="is-text-view" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="单位" v-if="hasFormFieldPermission('unit')"><el-input v-model="form.unit" readonly class="is-text-view" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类型" v-if="hasFormFieldPermission('material_type')"><el-input v-model="form.material_type" readonly class="is-text-view" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类别" v-if="hasFormFieldPermission('category')"><el-input v-model="form.category" readonly class="is-text-view" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
@ -227,7 +253,6 @@
|
||||
<div class="card-content">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="6"><el-form-item label="SKU" prop="sku"><el-input v-model="form.sku" placeholder="自动生成" disabled /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="条码" prop="barcode"><el-input v-model="form.barcode" placeholder="自动生成" clearable /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="库位" prop="warehouse_location"><el-input v-model="form.warehouse_location" placeholder="例如: B-01-01" clearable /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="入库日期"><el-date-picker v-model="form.in_date" type="date" value-format="YYYY-MM-DD" style="width:100%" disabled /></el-form-item></el-col>
|
||||
</el-row>
|
||||
@ -352,7 +377,13 @@
|
||||
<el-autocomplete v-model="form.production_manager" :fetch-suggestions="querySearchManager" placeholder="输入或选择负责人" style="width: 100%" clearable :trigger-on-focus="true" @select="handleManagerSelect"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8"><el-form-item label="产品定价"><el-input-number v-model="form.sale_price" :precision="2" style="width:100%"><template #prefix>¥</template></el-input-number></el-form-item></el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="产品定价">
|
||||
<el-input-number v-model="form.sale_price" :precision="2" :controls="false" style="width:100%" placeholder="请输入">
|
||||
<template #prefix>¥</template>
|
||||
</el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="24">
|
||||
@ -361,8 +392,23 @@
|
||||
<el-date-picker v-model="form.production_time_range" type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6"><el-form-item label="原料成本"><el-input-number v-model="form.raw_material_cost" :precision="2" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="人工成本"><el-input-number v-model="form.manual_cost" :precision="2" style="width:100%" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="原料成本">
|
||||
<el-input-number v-model="form.raw_material_cost" :precision="2" :controls="false" style="width:100%" placeholder="请输入"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="单件成本">
|
||||
<el-input-number v-model="form.unit_total_cost" :precision="2" :controls="false" style="width:100%" placeholder="请输入"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="总成本">
|
||||
<el-input-number v-model="form.total_price" :precision="2" :controls="false" style="width:100%" placeholder="自动计算" disabled/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24" style="margin-top:10px">
|
||||
<el-col :span="24"><el-form-item label="详情链接"><el-input v-model="form.detail_link" /></el-form-item></el-col>
|
||||
@ -427,11 +473,14 @@ import {
|
||||
deleteProductInbound,
|
||||
searchMaterialBase,
|
||||
searchBom,
|
||||
getFilterOptions // [新增]
|
||||
getFilterOptions,
|
||||
getManagerHistory, // [新增]
|
||||
calculateBomCost
|
||||
} from '@/api/inbound/product'
|
||||
import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
||||
import { getLabelPreview, executePrint } from '@/api/common/print'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
// ------------------------------------
|
||||
// v-loadmore
|
||||
@ -439,7 +488,8 @@ import { getLabelPreview, executePrint } from '@/api/common/print'
|
||||
const vLoadmore = {
|
||||
mounted(el: any, binding: any) {
|
||||
const checkAndBind = () => {
|
||||
const dropDownWrap = document.querySelector('.long-dropdown .el-select-dropdown__wrap')
|
||||
// 这里的 .product-dropdown 是唯一标识,防止和采购/半成品页面冲突
|
||||
const dropDownWrap = document.querySelector('.product-dropdown .el-select-dropdown__wrap')
|
||||
if (dropDownWrap && !dropDownWrap.getAttribute('data-loadmore-bound')) {
|
||||
dropDownWrap.setAttribute('data-loadmore-bound', 'true')
|
||||
dropDownWrap.addEventListener('scroll', function (this: any) {
|
||||
@ -455,6 +505,7 @@ const vLoadmore = {
|
||||
}
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const visible = ref(false)
|
||||
@ -464,7 +515,7 @@ const dialogStatus = ref<'create' | 'update'>('create')
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const formRef = ref()
|
||||
const queryParams = reactive({ page: 1, pageSize: 100, keyword: '', statuses: ['在库', '借库'], company: '' })
|
||||
const queryParams = reactive({ page: 1, pageSize: 100, keyword: '', category: '', material_type: '', statuses: ['在库', '借库'], company: '' })
|
||||
const categoryOptions = ref<string[]>([])
|
||||
const typeOptions = ref<string[]>([])
|
||||
const companyOptions = ref<string[]>([]) // [新增]
|
||||
@ -518,16 +569,81 @@ const allColumns = [
|
||||
{ prop: 'bom_code', label: 'BOM', minWidth: '100' },
|
||||
{ prop: 'production_manager', label: '负责人', minWidth: '100' },
|
||||
{ prop: 'raw_material_cost', label: '原料成本', minWidth: '100' },
|
||||
{ prop: 'manual_cost', label: '人工成本', minWidth: '100' },
|
||||
{ prop: 'unit_total_cost', label: '单件成本', minWidth: '100' },
|
||||
{ prop: 'total_price', label: '总成本', minWidth: '100' },
|
||||
{ prop: 'inbound_date', label: '生产日期', minWidth: '120' },
|
||||
{ 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',
|
||||
unit_total_cost: 'inbound_product:unit_total_cost',
|
||||
total_price: 'inbound_product:total_price',
|
||||
inbound_date: 'inbound_product:inbound_date',
|
||||
detail_link: 'inbound_product:detail_link',
|
||||
}
|
||||
|
||||
// 根据用户权限初始化列显示状态
|
||||
const initColumnPermissions = () => {
|
||||
// 超级管理员跳过权限检查,显示所有列
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||||
return
|
||||
}
|
||||
|
||||
// 普通用户:严格执行列级权限控制,没有权限的列必须隐藏
|
||||
// 遍历 allColumns,将没有权限的列从 visibleColumnProps 中移除
|
||||
const allowedColumns = allColumns.filter(col => {
|
||||
const code = permissionMap[col.prop]
|
||||
if (code) {
|
||||
return userStore.hasPermission(code)
|
||||
}
|
||||
// 如果没有映射,默认隐藏
|
||||
return false
|
||||
}).map(col => col.prop)
|
||||
|
||||
// 更新 visibleColumnProps,只保留有权限的列
|
||||
// 同时保持用户之前已经选择的有权限的列
|
||||
const currentVisible = visibleColumnProps.value.filter(prop => allowedColumns.includes(prop))
|
||||
// 如果当前没有可见列,则使用 allowedColumns 作为默认
|
||||
if (currentVisible.length === 0) {
|
||||
visibleColumnProps.value = allowedColumns
|
||||
} else {
|
||||
visibleColumnProps.value = currentVisible
|
||||
}
|
||||
}
|
||||
|
||||
// 检查列权限
|
||||
const hasColumnPermission = (prop: string) => {
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||||
return true
|
||||
}
|
||||
const code = permissionMap[prop]
|
||||
return code ? userStore.hasPermission(code) : false
|
||||
}
|
||||
|
||||
const defaultVisibleCols = ['company_name', 'material_name', 'sku', 'serial_number', 'qty_stock', 'status', 'quality_status', 'product_photo', 'sale_price', 'order_id']
|
||||
const visibleColumnProps = ref(defaultVisibleCols)
|
||||
|
||||
const form = reactive({
|
||||
id: undefined, base_id: undefined,
|
||||
id: undefined, base_id: undefined as number | undefined,
|
||||
company_name: '', // [新增]
|
||||
material_name: '', spec_model: '', material_type: '', category: '', unit: '',
|
||||
sku: '', barcode: '', serial_number: '', in_date: '',
|
||||
@ -535,7 +651,10 @@ const form = reactive({
|
||||
warehouse_location: '', status: '在库', quality_status: '合格',
|
||||
bom_code: '', bom_version: '', work_order_code: '', order_id: '',
|
||||
production_manager: '', production_time_range: [] as string[],
|
||||
raw_material_cost: 0, manual_cost: 0, sale_price: 0,
|
||||
raw_material_cost: undefined as number | undefined,
|
||||
unit_total_cost: undefined as number | undefined,
|
||||
total_price: undefined as number | undefined,
|
||||
sale_price: undefined as number | undefined,
|
||||
quality_report_link: [] as string[], inspection_report_link: [] as string[], product_photo: [] as string[], detail_link: ''
|
||||
})
|
||||
|
||||
@ -549,7 +668,7 @@ const handleSearchBom = async (query: string) => {
|
||||
bomOptions.value = res.data || []
|
||||
} finally { bomSearchLoading.value = false }
|
||||
}
|
||||
const handleBomSelect = (val: string) => {
|
||||
const handleBomSelect = async (val: string) => {
|
||||
if (!val) {
|
||||
form.bom_code = ''
|
||||
form.bom_version = ''
|
||||
@ -558,6 +677,65 @@ const handleBomSelect = (val: string) => {
|
||||
const [code, version] = val.split('###')
|
||||
form.bom_code = code
|
||||
form.bom_version = version
|
||||
// 自动计算 BOM 成本并填入 raw_material_cost 和 unit_total_cost
|
||||
try {
|
||||
const res: any = await calculateBomCost({ bom_code: code, bom_version: version })
|
||||
if (res.code === 200 && typeof res.data === 'number') {
|
||||
form.raw_material_cost = res.data
|
||||
form.unit_total_cost = res.data
|
||||
}
|
||||
} catch (e) {
|
||||
// 计算失败不影响现有输入
|
||||
console.warn('BOM 成本计算失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// 表单字段权限检查
|
||||
// ------------------------------------
|
||||
const hasFormFieldPermission = (fieldName: string) => {
|
||||
// 超级管理员直接返回true
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||||
return true
|
||||
}
|
||||
// 根据字段名映射到权限码
|
||||
const map: Record<string, string> = {
|
||||
company_name: 'inbound_product:company_name',
|
||||
material_name: 'inbound_product:material_name',
|
||||
spec_model: 'inbound_product:spec_model',
|
||||
material_type: 'inbound_product:material_type',
|
||||
category: 'inbound_product:category',
|
||||
unit: 'inbound_product:unit',
|
||||
sku: 'inbound_product:sku',
|
||||
barcode: 'inbound_product:barcode',
|
||||
serial_number: 'inbound_product:serial_number',
|
||||
in_date: 'inbound_product:inbound_date',
|
||||
in_quantity: 'inbound_product:in_quantity',
|
||||
stock_quantity: 'inbound_product:stock_quantity',
|
||||
available_quantity: 'inbound_product:available_quantity',
|
||||
warehouse_location: 'inbound_product:warehouse_location',
|
||||
status: 'inbound_product:status',
|
||||
quality_status: 'inbound_product:quality_status',
|
||||
bom_code: 'inbound_product:bom_code',
|
||||
bom_version: 'inbound_product:bom_version',
|
||||
work_order_code: 'inbound_product:work_order_code',
|
||||
order_id: 'inbound_product:order_id',
|
||||
production_manager: 'inbound_product:production_manager',
|
||||
production_time_range: 'inbound_product:production_start_time',
|
||||
raw_material_cost: 'inbound_product:raw_material_cost',
|
||||
manual_cost: 'inbound_product:manual_cost',
|
||||
sale_price: 'inbound_product:sale_price',
|
||||
quality_report_link: 'inbound_product:quality_report_link',
|
||||
inspection_report_link: 'inbound_product:inspection_report_link',
|
||||
product_photo: 'inbound_product:product_photo',
|
||||
detail_link: 'inbound_product:detail_link',
|
||||
}
|
||||
const code = map[fieldName]
|
||||
if (!code) {
|
||||
// 没有映射的字段默认显示
|
||||
return true
|
||||
}
|
||||
return userStore.hasPermission(code)
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
@ -582,7 +760,7 @@ const rules = {
|
||||
|
||||
|
||||
// ------------------------------------
|
||||
// Material Search & Population Logic
|
||||
// Material Search & Population Logic (已修改)
|
||||
// ------------------------------------
|
||||
const handleMaterialDropdownVisible = (visible: boolean) => { if (visible && materialOptions.value.length === 0) handleSearchMaterialDebounced('') }
|
||||
|
||||
@ -601,9 +779,9 @@ const handleSearchMaterial = async (query: string) => {
|
||||
|
||||
try {
|
||||
const res: any = await searchMaterialBase(query, 1)
|
||||
const apiResults = (res.data || []).map((i: any) => ({ ...i, isHistory: false }))
|
||||
const apiResults = (res.data?.items || []).map((i: any) => ({ ...i, isHistory: false }))
|
||||
materialOptions.value = apiResults
|
||||
hasNextPage.value = res.has_next
|
||||
hasNextPage.value = res.data?.has_next ?? false
|
||||
} finally { searchLoading.value = false }
|
||||
}
|
||||
|
||||
@ -613,10 +791,10 @@ const loadMoreMaterials = async () => {
|
||||
searchPage.value += 1
|
||||
try {
|
||||
const res: any = await searchMaterialBase(searchKeyword.value, searchPage.value)
|
||||
if (res.data && res.data.length > 0) {
|
||||
const newItems = res.data.map((i: any) => ({...i, isHistory: false}))
|
||||
if (res.data && res.data.items && res.data.items.length > 0) {
|
||||
const newItems = res.data.items.map((i: any) => ({...i, isHistory: false}))
|
||||
materialOptions.value.push(...newItems)
|
||||
hasNextPage.value = res.has_next
|
||||
hasNextPage.value = res.data.has_next
|
||||
} else {
|
||||
hasNextPage.value = false
|
||||
}
|
||||
@ -640,12 +818,19 @@ const onMaterialSelected = (val: number) => {
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// Autocomplete (Manager) - 后端驱动
|
||||
// Autocomplete (Manager) - 后端历史记录驱动 (已修改为全局)
|
||||
// ------------------------------------
|
||||
const querySearchManager = async (query: string, cb: any) => {
|
||||
cb([])
|
||||
try {
|
||||
const res: any = await getManagerHistory({ keyword: query })
|
||||
if (res.code === 200) {
|
||||
const managers = (res.data || []).map((name: string) => ({ value: name }))
|
||||
cb(managers)
|
||||
} else { cb([]) }
|
||||
} catch (e) { cb([]) }
|
||||
}
|
||||
const handleManagerSelect = (item: any) => {
|
||||
form.production_manager = item.value
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
@ -696,10 +881,14 @@ const handleUpdate = (row: any) => {
|
||||
quality_report_link: row.quality_report_link || [],
|
||||
inspection_report_link: row.inspection_report_link || [],
|
||||
in_quantity: Number(row.qty_inbound),
|
||||
raw_material_cost: Number(row.raw_material_cost),
|
||||
manual_cost: Number(row.manual_cost),
|
||||
sale_price: Number(row.sale_price)
|
||||
raw_material_cost: (row.raw_material_cost !== null && row.raw_material_cost !== undefined) ? Number(row.raw_material_cost) : undefined,
|
||||
unit_total_cost: (row.unit_total_cost !== null && row.unit_total_cost !== undefined) ? Number(row.unit_total_cost) : undefined,
|
||||
sale_price: (row.sale_price !== null && row.sale_price !== undefined) ? Number(row.sale_price) : undefined
|
||||
})
|
||||
// 计算总成本
|
||||
const u = Number(form.unit_total_cost || 0)
|
||||
const q = Number(form.in_quantity || 1)
|
||||
form.total_price = Number((u * q).toFixed(2))
|
||||
if(row.production_start_time && row.production_end_time) { form.production_time_range = [row.production_start_time, row.production_end_time] } else { form.production_time_range = [] }
|
||||
productPhotoList.value = form.product_photo.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
||||
const qReports = form.quality_report_link || []
|
||||
@ -802,7 +991,17 @@ const submitForm = async () => {
|
||||
const iImages = iList.filter(item => !isExternalLink(item))
|
||||
if (inspection_url.value && !iList.includes(inspection_url.value)) iImages.push(inspection_url.value)
|
||||
else if (inspection_url.value) iImages.push(inspection_url.value)
|
||||
const payload = { ...form, quality_report_link: qImages, inspection_report_link: iImages, production_start_time: form.production_time_range?.[0], production_end_time: form.production_time_range?.[1] }
|
||||
const payload = {
|
||||
...form,
|
||||
quality_report_link: qImages,
|
||||
inspection_report_link: iImages,
|
||||
raw_material_cost: Number(form.raw_material_cost || 0),
|
||||
unit_total_cost: Number(form.unit_total_cost || 0),
|
||||
total_price: Number(form.total_price || 0),
|
||||
sale_price: Number(form.sale_price || 0),
|
||||
production_start_time: form.production_time_range?.[0],
|
||||
production_end_time: form.production_time_range?.[1]
|
||||
}
|
||||
delete payload.production_time_range
|
||||
try {
|
||||
if(dialogStatus.value === 'create') {
|
||||
@ -828,22 +1027,31 @@ const handlePrint = async (row: any) => {
|
||||
const confirmPrint = async () => { printing.value = true; try { await executePrint(currentPrintData.value); ElMessage.success('已发送'); printVisible.value = false } catch (e: any) { ElMessage.error('打印失败') } finally { printing.value = false } }
|
||||
const resetForm = () => {
|
||||
materialOptions.value = []; bomOptions.value = []; productPhotoList.value = []; qualityFileList.value = []; inspectionFileList.value = []; quality_url.value = ''; inspection_url.value = ''
|
||||
Object.assign(form, { id: undefined, base_id: undefined, material_name: '', spec_model: '', material_type: '', category: '', unit: '', sku: '', barcode: '', serial_number: '', in_date: '', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', status: '在库', quality_status: '合格', bom_code: '', bom_version: '', work_order_code: '', order_id: '', production_manager: '', production_time_range: [], raw_material_cost: 0, manual_cost: 0, sale_price: 0, quality_report_link: [], inspection_report_link: [], product_photo: [], detail_link: '' })
|
||||
Object.assign(form, { id: undefined, base_id: undefined, material_name: '', spec_model: '', material_type: '', category: '', unit: '', sku: '', barcode: '', serial_number: '', in_date: '', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', status: '在库', quality_status: '合格', bom_code: '', bom_version: '', work_order_code: '', order_id: '', production_manager: '', production_time_range: [], raw_material_cost: undefined, unit_total_cost: undefined, total_price: undefined, sale_price: undefined, quality_report_link: [], inspection_report_link: [], product_photo: [], detail_link: '' })
|
||||
}
|
||||
const getStatusType = (s:string) => ({'在库':'success','出库':'info','借库':'warning','损耗':'danger'}[s]||'warning')
|
||||
const getQualityType = (s:string) => ({'合格':'success','不合格':'danger','待检':'info'}[s]||'info')
|
||||
const formatMoney = (val:any) => isNaN(Number(val)) ? '-' : `¥ ${Number(val).toFixed(2)}`
|
||||
onMounted(() => {
|
||||
// 先根据权限初始化列显示状态
|
||||
initColumnPermissions()
|
||||
fetchData()
|
||||
fetchOptions()
|
||||
})
|
||||
|
||||
// 成本计算监听
|
||||
watch([() => form.unit_total_cost, () => form.in_quantity], ([unit, qty]) => {
|
||||
const unitNum = Number(unit || 0)
|
||||
const qtyNum = Number(qty || 1)
|
||||
form.total_price = Number((unitNum * qtyNum).toFixed(2))
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.product-module { background: #f5f7fa; padding: 20px; min-height: 100vh; }
|
||||
.header-tools { display: flex; justify-content: space-between; margin-bottom: 20px; background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05); }
|
||||
.left-tools { display: flex; gap: 10px; align-items: center; flex: 1; }
|
||||
.right-tools { display: flex; gap: 10px; align-items: center; }
|
||||
.header-tools { display: flex; justify-content: space-between; margin-bottom: 20px; background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05); flex-wrap: wrap; }
|
||||
.left-tools { display: flex; gap: 10px; align-items: center; flex: 1; flex-wrap: wrap; }
|
||||
.right-tools { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||||
.modern-table { border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05); }
|
||||
:deep(.table-header-gray th) { background-color: #f8f9fb !important; color: #606266; }
|
||||
.tag-sn { color: #409EFF; font-weight: bold; font-family: monospace; }
|
||||
@ -893,16 +1101,24 @@ onMounted(() => {
|
||||
.filter-item-input { /* 宽度已在行内样式控制 */ }
|
||||
.action-btn { font-weight: 500; }
|
||||
|
||||
.search-btn { background-color: #E6F1FC; border-color: #A3D0FD; color: #409EFF; }
|
||||
.search-btn:hover { background-color: #409EFF; border-color: #409EFF; color: #fff; }
|
||||
.reset-btn { background-color: #fff; border: 1px solid #dcdfe6; }
|
||||
.reset-btn:hover { border-color: #c0c4cc; color: #606266; }
|
||||
|
||||
/* [新增] 修复弹窗最小高度 */
|
||||
.dialog-scroll-container { min-height: 450px; }
|
||||
|
||||
/* [新增] 纯文本样式 */
|
||||
.is-text-view :deep(.el-input__wrapper) { box-shadow: none !important; background-color: transparent !important; border-bottom: 1px dashed #dcdfe6; border-radius: 0; padding-left: 0; }
|
||||
.is-text-view :deep(.el-input__inner) { color: #303133; font-weight: 600; font-size: 14px; cursor: text; }
|
||||
|
||||
/* 左对齐数字框 */
|
||||
:deep(.el-input-number .el-input__inner) { text-align: left; }
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.long-dropdown { width: 580px !important; }
|
||||
.long-dropdown .el-select-dropdown__wrap { max-height: 320px !important; }
|
||||
.long-dropdown .el-input__suffix { z-index: 10; }
|
||||
</style>
|
||||
.product-dropdown { width: 580px !important; }
|
||||
.product-dropdown .el-select-dropdown__wrap { max-height: 320px !important; }
|
||||
.product-dropdown .el-input__suffix { z-index: 10; }
|
||||
</style>
|
||||
|
||||
@ -69,7 +69,7 @@
|
||||
</div>
|
||||
|
||||
<div class="right-tools">
|
||||
<el-button type="primary" :icon="Plus" @click="handleCreate" class="add-btn">半成品入库</el-button>
|
||||
<el-button v-if="userStore.hasPermission('inbound_semi:operation')" type="primary" :icon="Plus" @click="handleCreate" class="add-btn">半成品入库</el-button>
|
||||
<el-button :icon="Refresh" circle @click="fetchData" class="circle-btn" />
|
||||
|
||||
<el-popover placement="bottom-end" title="列配置" :width="500" trigger="click">
|
||||
@ -80,13 +80,13 @@
|
||||
<div class="col-group-title">基础信息</div>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12" v-for="c in baseColumns" :key="c.prop">
|
||||
<el-checkbox :label="c.prop">{{ c.label }}</el-checkbox>
|
||||
<el-checkbox :label="c.prop" :disabled="!hasColumnPermission(c.prop)">{{ c.label }}</el-checkbox>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="col-group-title" style="margin-top:10px">生产与库存</div>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12" v-for="c in stockColumns" :key="c.prop">
|
||||
<el-checkbox :label="c.prop">{{ c.label }}</el-checkbox>
|
||||
<el-checkbox :label="c.prop" :disabled="!hasColumnPermission(c.prop)">{{ c.label }}</el-checkbox>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-checkbox-group>
|
||||
@ -119,8 +119,7 @@
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="col.prop === 'company_name'">
|
||||
<el-tag v-if="scope.row.company_name" type="info" effect="plain" size="small" style="font-weight: bold;">{{ scope.row.company_name }}</el-tag>
|
||||
<span v-else>-</span>
|
||||
<span>{{ scope.row.company_name || '-' }}</span>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="col.prop === 'sn_bn'">
|
||||
@ -183,13 +182,13 @@
|
||||
</el-link>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="['raw_material_cost', 'manual_cost', 'unit_total_cost'].includes(col.prop)">
|
||||
<template #default="scope" v-else-if="['raw_material_cost', 'unit_total_cost', 'total_price'].includes(col.prop)">
|
||||
<span class="money-text">{{ formatMoney(scope.row[col.prop]) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<el-table-column v-if="userStore.hasPermission('inbound_semi:operation')" label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="warning" size="default" @click="handlePrint(row)">
|
||||
<el-icon><Printer/></el-icon> 打印
|
||||
@ -219,7 +218,7 @@
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogStatus === 'create' ? '新增半成品入库' : '编辑半成品信息'"
|
||||
width="1050px"
|
||||
width="min(1000px, 95vw)"
|
||||
top="5vh"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@ -293,12 +292,12 @@
|
||||
|
||||
<div class="read-only-grid">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8"><el-form-item label="所属公司"><el-input v-model="form.company_name" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="规格型号"><el-input v-model="form.spec_model" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="单位"><el-input v-model="form.unit" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类别"><el-input v-model="form.category" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="所属公司" v-if="hasFormFieldPermission('company_name')"><el-input v-model="form.company_name" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="名称" v-if="hasFormFieldPermission('material_name')"><el-input v-model="form.material_name" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="规格型号" v-if="hasFormFieldPermission('spec_model')"><el-input v-model="form.spec_model" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="单位" v-if="hasFormFieldPermission('unit')"><el-input v-model="form.unit" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类别" v-if="hasFormFieldPermission('category')"><el-input v-model="form.category" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类型" v-if="hasFormFieldPermission('material_type')"><el-input v-model="form.material_type" readonly class="is-text-view"/></el-form-item></el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
@ -314,7 +313,6 @@
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="6"><el-form-item label="编码/SKU" prop="sku"><el-input v-model="form.sku" placeholder="系统自动生成" disabled/></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="入库日期" prop="in_date"><el-date-picker v-model="form.in_date" type="date" value-format="YYYY-MM-DD" style="width:100%" disabled/></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="条码" prop="barcode"><el-input v-model="form.barcode" placeholder="扫描条码" clearable/></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="库位" prop="warehouse_location"><el-input v-model="form.warehouse_location" placeholder="例如: B-01-01" clearable/></el-form-item></el-col>
|
||||
</el-row>
|
||||
|
||||
@ -467,11 +465,23 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="divider-text">成本核算 (单件)</div>
|
||||
<div class="divider-text">成本核算</div>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8"><el-form-item label="原材料成本"><el-input-number v-model="form.raw_material_cost" :precision="2" controls-position="right" style="width:100%"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="手动/工时"><el-input-number v-model="form.manual_cost" :precision="2" controls-position="right" style="width:100%"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="单件总成本"><el-input-number v-model="form.unit_total_cost" :precision="2" disabled :controls="false" style="width:100%" class="total-price-input"/></el-form-item></el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="原材料成本">
|
||||
<el-input-number v-model="form.raw_material_cost" :precision="2" :controls="false" style="width:100%" placeholder="请输入"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="单件成本">
|
||||
<el-input-number v-model="form.unit_total_cost" :precision="2" :controls="false" style="width:100%" placeholder="请输入"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="总成本">
|
||||
<el-input-number v-model="form.total_price" :precision="2" :controls="false" style="width:100%" placeholder="自动计算" disabled/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="24" style="margin-top:10px">
|
||||
@ -528,11 +538,14 @@ import {
|
||||
deleteSemiInbound,
|
||||
searchMaterialBase,
|
||||
searchBom,
|
||||
getFilterOptions
|
||||
getFilterOptions,
|
||||
getManagerHistory, // [新增]
|
||||
calculateBomCost
|
||||
} from '@/api/inbound/semi'
|
||||
import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
||||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
||||
import {getLabelPreview, executePrint} from '@/api/common/print'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
// ------------------------------------
|
||||
// 自定义指令:v-loadmore (适配 Teleport 到 Body 的下拉框)
|
||||
@ -559,6 +572,7 @@ const vLoadmore = {
|
||||
// ------------------------------------
|
||||
// 状态与变量
|
||||
// ------------------------------------
|
||||
const userStore = useUserStore()
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const visible = ref(false)
|
||||
@ -629,8 +643,8 @@ const stockColumns = [
|
||||
{prop: 'bom_version', label: 'BOM版本', minWidth: '90'},
|
||||
{prop: 'work_order_code', label: '工单号', minWidth: '120'},
|
||||
{prop: 'raw_material_cost', label: '原料成本', minWidth: '100'},
|
||||
{prop: 'manual_cost', label: '人工成本', minWidth: '100'},
|
||||
{prop: 'unit_total_cost', label: '单件总本', minWidth: '100'},
|
||||
{prop: 'unit_total_cost', label: '单件成本', minWidth: '100'},
|
||||
{prop: 'total_price', label: '总成本', minWidth: '100'},
|
||||
{prop: 'production_manager', label: '生产负责人', minWidth: '100'},
|
||||
{prop: 'production_start_time', label: '生产开始', minWidth: '160'},
|
||||
{prop: 'production_end_time', label: '生产结束', minWidth: '160'},
|
||||
@ -640,13 +654,89 @@ const stockColumns = [
|
||||
]
|
||||
const allColumns = [...baseColumns, ...stockColumns]
|
||||
|
||||
// 列与权限Code的映射关系(数据库中的code)
|
||||
const permissionMap: Record<string, string> = {
|
||||
id: 'inbound_semi:id',
|
||||
base_id: 'inbound_semi:base_id',
|
||||
company_name: 'inbound_semi:company_name',
|
||||
material_name: 'inbound_semi:material_name',
|
||||
category: 'inbound_semi:category',
|
||||
material_type: 'inbound_semi:material_type',
|
||||
spec_model: 'inbound_semi:spec_model',
|
||||
unit: 'inbound_semi:unit',
|
||||
sku: 'inbound_semi:sku',
|
||||
inbound_date: 'inbound_semi:inbound_date',
|
||||
barcode: 'inbound_semi:barcode',
|
||||
sn_bn: 'inbound_semi:sn_bn',
|
||||
status: 'inbound_semi:status',
|
||||
quality_status: 'inbound_semi:quality_status',
|
||||
qty_inbound: 'inbound_semi:qty_inbound',
|
||||
qty_stock: 'inbound_semi:qty_stock',
|
||||
qty_available: 'inbound_semi:qty_available',
|
||||
warehouse_loc: 'inbound_semi:warehouse_loc',
|
||||
bom_code: 'inbound_semi:bom_code',
|
||||
bom_version: 'inbound_semi:bom_version',
|
||||
work_order_code: 'inbound_semi:work_order_code',
|
||||
raw_material_cost: 'inbound_semi:raw_material_cost',
|
||||
unit_total_cost: 'inbound_semi:unit_total_cost',
|
||||
total_price: 'inbound_semi:total_price',
|
||||
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
|
||||
}
|
||||
|
||||
const allowedColumns = allColumns.filter(col => {
|
||||
const code = permissionMap[col.prop]
|
||||
if (code) {
|
||||
return userStore.hasPermission(code)
|
||||
}
|
||||
return false
|
||||
}).map(col => col.prop)
|
||||
|
||||
const currentVisible = visibleColumnProps.value.filter(prop => allowedColumns.includes(prop))
|
||||
if (currentVisible.length === 0) {
|
||||
visibleColumnProps.value = allowedColumns
|
||||
} else {
|
||||
visibleColumnProps.value = currentVisible
|
||||
}
|
||||
}
|
||||
|
||||
// 检查列权限
|
||||
const hasColumnPermission = (prop: string) => {
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||||
return true
|
||||
}
|
||||
const code = permissionMap[prop]
|
||||
return code ? userStore.hasPermission(code) : false
|
||||
}
|
||||
|
||||
const defaultColumns = ['company_name', 'material_name', 'spec_model', 'unit', 'inbound_date', 'sn_bn', 'status', 'quality_status', 'bom_code', 'work_order_code', 'qty_stock', 'qty_available', 'unit_total_cost', 'arrival_photo', 'quality_report_link']
|
||||
const visibleColumnProps = ref(defaultColumns)
|
||||
|
||||
const form = reactive({
|
||||
id: undefined, base_id: undefined as number | undefined,
|
||||
company_name: '', // [新增]
|
||||
material_name: '', spec_model: '', category: '', unit: '', material_type: '', sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', quality_status: '合格', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', bom_code: '', bom_version: '', work_order_code: '', raw_material_cost: 0, manual_cost: 0, unit_total_cost: 0, production_manager: '', production_time_range: [] as string[], arrival_photo: [] as string[], quality_report_link: [] as string[], detail_link: ''
|
||||
company_name: '',
|
||||
material_name: '', spec_model: '', category: '', unit: '', material_type: '', sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', quality_status: '合格', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', bom_code: '', bom_version: '', work_order_code: '',
|
||||
raw_material_cost: undefined as number | undefined,
|
||||
unit_total_cost: undefined as number | undefined,
|
||||
total_price: undefined as number | undefined,
|
||||
production_manager: '', production_time_range: [] as string[], arrival_photo: [] as string[], quality_report_link: [] as string[], detail_link: ''
|
||||
})
|
||||
|
||||
// === 监听计算总成本 ===
|
||||
watch([() => form.unit_total_cost, () => form.in_quantity], ([unit, qty]) => {
|
||||
const unitNum = Number(unit || 0)
|
||||
const qtyNum = Number(qty || 1)
|
||||
form.total_price = Number((unitNum * qtyNum).toFixed(2))
|
||||
})
|
||||
|
||||
// ------------------------------------
|
||||
@ -659,7 +749,7 @@ const handleSearchBom = async (query: string) => {
|
||||
bomOptions.value = res.data || []
|
||||
} finally { bomSearchLoading.value = false }
|
||||
}
|
||||
const handleBomSelect = (val: string) => {
|
||||
const handleBomSelect = async (val: string) => {
|
||||
// val 格式为 bom_no###version
|
||||
if (!val) {
|
||||
form.bom_code = ''
|
||||
@ -669,15 +759,33 @@ const handleBomSelect = (val: string) => {
|
||||
const [code, version] = val.split('###')
|
||||
form.bom_code = code
|
||||
form.bom_version = version
|
||||
// 自动计算 BOM 成本并填入 raw_material_cost 和 unit_total_cost
|
||||
try {
|
||||
const res: any = await calculateBomCost({ bom_code: code, bom_version: version })
|
||||
if (res.code === 200 && typeof res.data === 'number') {
|
||||
form.raw_material_cost = res.data
|
||||
form.unit_total_cost = res.data
|
||||
}
|
||||
} catch (e) {
|
||||
// 计算失败不影响现有输入
|
||||
console.warn('BOM 成本计算失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// Autocomplete & Search Logic (后端 API 驱动)
|
||||
// Autocomplete & Search Logic (后端 API 驱动,全局检索)
|
||||
// ------------------------------------
|
||||
const querySearchManager = async (query: string, cb: any) => {
|
||||
cb([])
|
||||
try {
|
||||
const res: any = await getManagerHistory({ keyword: query })
|
||||
if (res.code === 200) {
|
||||
const managers = (res.data || []).map((name: string) => ({ value: name }))
|
||||
cb(managers)
|
||||
} else { cb([]) }
|
||||
} catch (e) { cb([]) }
|
||||
}
|
||||
const handleManagerSelect = (item: any) => {
|
||||
form.production_manager = item.value
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
@ -700,9 +808,9 @@ const handleSearchMaterial = async (query: string) => {
|
||||
|
||||
try {
|
||||
const res: any = await searchMaterialBase(query, 1)
|
||||
const apiResults = (res.data || []).map((i: any) => ({...i, isHistory: false}))
|
||||
const apiResults = (res.data?.items || []).map((i: any) => ({...i, isHistory: false}))
|
||||
materialOptions.value = apiResults
|
||||
hasNextPage.value = res.has_next
|
||||
hasNextPage.value = res.data?.has_next ?? false
|
||||
} finally { searchLoading.value = false }
|
||||
}
|
||||
|
||||
@ -712,10 +820,10 @@ const loadMoreMaterials = async () => {
|
||||
searchPage.value += 1
|
||||
try {
|
||||
const res: any = await searchMaterialBase(searchKeyword.value, searchPage.value)
|
||||
if (res.data && res.data.length > 0) {
|
||||
const newItems = res.data.map((i: any) => ({...i, isHistory: false}))
|
||||
if (res.data && res.data.items && res.data.items.length > 0) {
|
||||
const newItems = res.data.items.map((i: any) => ({...i, isHistory: false}))
|
||||
materialOptions.value.push(...newItems)
|
||||
hasNextPage.value = res.has_next
|
||||
hasNextPage.value = res.data.has_next
|
||||
} else {
|
||||
hasNextPage.value = false
|
||||
}
|
||||
@ -765,6 +873,50 @@ const rules = {
|
||||
batch_number: [{validator: validateIdentity, trigger: 'blur'}, {validator: validateUnique, trigger: 'blur'}]
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// 表单字段权限检查
|
||||
// ------------------------------------
|
||||
const hasFormFieldPermission = (fieldName: string) => {
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||||
return true
|
||||
}
|
||||
const map: Record<string, string> = {
|
||||
company_name: 'inbound_semi:company_name',
|
||||
material_name: 'inbound_semi:material_name',
|
||||
spec_model: 'inbound_semi:spec_model',
|
||||
category: 'inbound_semi:category',
|
||||
material_type: 'inbound_semi:material_type',
|
||||
unit: 'inbound_semi:unit',
|
||||
sku: 'inbound_semi:sku',
|
||||
in_date: 'inbound_semi:inbound_date',
|
||||
barcode: 'inbound_semi:barcode',
|
||||
serial_number: 'inbound_semi:serial_number',
|
||||
batch_number: 'inbound_semi:batch_number',
|
||||
status: 'inbound_semi:status',
|
||||
quality_status: 'inbound_semi:quality_status',
|
||||
in_quantity: 'inbound_semi:in_quantity',
|
||||
stock_quantity: 'inbound_semi:stock_quantity',
|
||||
available_quantity: 'inbound_semi:available_quantity',
|
||||
warehouse_location: 'inbound_semi:warehouse_location',
|
||||
bom_code: 'inbound_semi:bom_code',
|
||||
bom_version: 'inbound_semi:bom_version',
|
||||
work_order_code: 'inbound_semi:work_order_code',
|
||||
raw_material_cost: 'inbound_semi:raw_material_cost',
|
||||
manual_cost: 'inbound_semi:manual_cost',
|
||||
unit_total_cost: 'inbound_semi:unit_total_cost',
|
||||
production_manager: 'inbound_semi:production_manager',
|
||||
production_time_range: 'inbound_semi:production_start_time',
|
||||
arrival_photo: 'inbound_semi:arrival_photo',
|
||||
quality_report_link: 'inbound_semi:quality_report_link',
|
||||
detail_link: 'inbound_semi:detail_link',
|
||||
}
|
||||
const code = map[fieldName]
|
||||
if (!code) {
|
||||
return true
|
||||
}
|
||||
return userStore.hasPermission(code)
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// Core Logic
|
||||
// ------------------------------------
|
||||
@ -790,7 +942,6 @@ const handleEntryModeChange = (val: string) => {
|
||||
if (val === 'batch') { form.serial_number = ''; form.batch_number = '000001'; if(formRef.value) formRef.value.clearValidate('serial_number') }
|
||||
else { form.batch_number = ''; if(formRef.value) formRef.value.clearValidate('batch_number') }
|
||||
}
|
||||
watch(() => [form.raw_material_cost, form.manual_cost], () => { form.unit_total_cost = Number((form.raw_material_cost + form.manual_cost).toFixed(2)) })
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
@ -847,12 +998,17 @@ const handleUpdate = (row: any) => {
|
||||
warehouse_location: row.warehouse_loc, status: row.status, quality_status: row.quality_status,
|
||||
in_quantity: Number(row.qty_inbound), stock_quantity: Number(row.qty_stock), available_quantity: Number(row.qty_available),
|
||||
bom_code: row.bom_code, bom_version: row.bom_version, work_order_code: row.work_order_code,
|
||||
raw_material_cost: Number(row.raw_material_cost) || 0, manual_cost: Number(row.manual_cost) || 0,
|
||||
raw_material_cost: (row.raw_material_cost !== null && row.raw_material_cost !== undefined) ? Number(row.raw_material_cost) : undefined,
|
||||
unit_total_cost: (row.unit_total_cost !== null && row.unit_total_cost !== undefined) ? Number(row.unit_total_cost) : undefined,
|
||||
production_manager: row.production_manager,
|
||||
production_time_range: (row.production_start_time && row.production_end_time) ? [row.production_start_time, row.production_end_time] : [],
|
||||
detail_link: row.detail_link,
|
||||
arrival_photo: row.arrival_photo || [], quality_report_link: row.quality_report_link || []
|
||||
})
|
||||
// 计算总成本
|
||||
const u = Number(form.unit_total_cost || 0)
|
||||
const q = Number(form.in_quantity || 1)
|
||||
form.total_price = Number((u * q).toFixed(2))
|
||||
arrivalFileList.value = form.arrival_photo.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
||||
const reports = form.quality_report_link || []
|
||||
const reportImgs = reports.filter(r => !isExternalLink(r))
|
||||
@ -945,7 +1101,16 @@ const submitForm = async () => {
|
||||
if (quality_report_url.value && !finalReportList.includes(quality_report_url.value)) finalReportList.push(quality_report_url.value)
|
||||
const onlyImages = finalReportList.filter(item => !isExternalLink(item))
|
||||
if (quality_report_url.value) onlyImages.push(quality_report_url.value)
|
||||
const payload: any = { ...form, quality_report_link: onlyImages, in_quantity: Number(form.in_quantity), raw_material_cost: Number(form.raw_material_cost), manual_cost: Number(form.manual_cost), production_start_time: form.production_time_range?.[0] || null, production_end_time: form.production_time_range?.[1] || null }
|
||||
const payload: any = {
|
||||
...form,
|
||||
quality_report_link: onlyImages,
|
||||
in_quantity: Number(form.in_quantity),
|
||||
raw_material_cost: Number(form.raw_material_cost || 0),
|
||||
unit_total_cost: Number(form.unit_total_cost || 0),
|
||||
total_price: Number(form.total_price || 0),
|
||||
production_start_time: form.production_time_range?.[0] || null,
|
||||
production_end_time: form.production_time_range?.[1] || null
|
||||
}
|
||||
delete payload.production_time_range
|
||||
try {
|
||||
if (dialogStatus.value === 'create') {
|
||||
@ -978,13 +1143,17 @@ const resetForm = () => {
|
||||
Object.assign(form, {
|
||||
id: undefined, base_id: undefined,
|
||||
company_name: '', // [新增]
|
||||
material_name: '', spec_model: '', category: '', unit: '', material_type: '', sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', quality_status: '合格', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', bom_code: '', bom_version: '', work_order_code: '', raw_material_cost: 0, manual_cost: 0, unit_total_cost: 0, production_manager: '', production_time_range: [], arrival_photo: [], quality_report_link: [], detail_link: '' })
|
||||
material_name: '', spec_model: '', category: '', unit: '', material_type: '', sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', quality_status: '合格', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '', bom_code: '', bom_version: '', work_order_code: '',
|
||||
raw_material_cost: undefined, unit_total_cost: undefined, total_price: undefined,
|
||||
production_manager: '', production_time_range: [], arrival_photo: [], quality_report_link: [], detail_link: '' })
|
||||
}
|
||||
const getStatusType = (status: string) => { const map: any = { '在库': 'success', '出库': 'info', '借库': 'warning', '损耗': 'danger' }; return map[status] || 'warning' }
|
||||
const getQualityType = (status: string) => { const map: any = { '合格': 'success', '不合格': 'danger', '待检': 'info', '返修中': 'warning' }; return map[status] || 'info' }
|
||||
const formatMoney = (val: any) => { const num = Number(val); return isNaN(num) ? '-' : `¥ ${num.toFixed(2)}` }
|
||||
const formatMoney = (val: any) => isNaN(Number(val)) ? '-' : `¥ ${Number(val).toFixed(2)}`
|
||||
|
||||
onMounted(() => {
|
||||
// 先根据权限初始化列显示状态
|
||||
initColumnPermissions()
|
||||
fetchData()
|
||||
fetchOptions()
|
||||
})
|
||||
@ -992,9 +1161,9 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.semi-module { background: #f5f7fa; padding: 20px; min-height: 100vh; }
|
||||
.header-tools { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05); }
|
||||
.left-tools { display: flex; gap: 10px; align-items: center; flex: 1; }
|
||||
.right-tools { display: flex; gap: 10px; align-items: center; }
|
||||
.header-tools { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; margin-bottom: 20px; background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05); }
|
||||
.left-tools { display: flex; gap: 10px; align-items: center; flex: 1; flex-wrap: wrap; }
|
||||
.right-tools { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||||
.action-btn { font-weight: 500; }
|
||||
.modern-table { border-radius: 8px; overflow: hidden; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05); }
|
||||
:deep(.table-header-gray th) { background-color: #f8f9fb !important; color: #606266; font-weight: 600; height: 50px; }
|
||||
@ -1056,10 +1225,13 @@ onMounted(() => {
|
||||
.opt-spec { color: #999; font-size: 12px; }
|
||||
.opt-tags { display: flex; gap: 5px; flex-shrink: 0; }
|
||||
.company-tag { font-weight: bold; }
|
||||
|
||||
/* 左对齐数字框 */
|
||||
:deep(.el-input-number .el-input__inner) { text-align: left; }
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.long-dropdown { width: 580px !important; }
|
||||
.long-dropdown .el-select-dropdown__wrap { max-height: 320px !important; }
|
||||
.long-dropdown .el-input__suffix { z-index: 10; }
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -45,21 +45,21 @@
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
<el-button type="success" @click="handleAdd">新增服务</el-button>
|
||||
<el-button v-if="userStore.hasPermission('inbound_service:operation')" type="success" @click="handleAdd">新增服务</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-table :data="tableData" border stripe style="width: 100%;" v-loading="loading">
|
||||
<el-table-column prop="sku" label="SKU" width="200" />
|
||||
<el-table-column prop="material_name" label="物料名称" />
|
||||
<el-table-column prop="provider_name" label="服务商" width="150" />
|
||||
<el-table-column prop="sale_price" label="售价" width="120">
|
||||
<el-table-column v-if="hasColumnPermission('sku')" prop="sku" label="SKU" width="200" />
|
||||
<el-table-column v-if="hasColumnPermission('material_name')" prop="material_name" label="物料名称" />
|
||||
<el-table-column v-if="hasColumnPermission('provider_name')" prop="provider_name" label="服务商" width="150" />
|
||||
<el-table-column v-if="hasColumnPermission('sale_price')" prop="sale_price" label="售价" width="120">
|
||||
<template #default="{row}">¥{{ row.sale_price.toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="简介" show-overflow-tooltip />
|
||||
<el-table-column prop="created_at" label="创建时间" width="160" />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<el-table-column v-if="hasColumnPermission('description')" prop="description" label="简介" show-overflow-tooltip />
|
||||
<el-table-column v-if="hasColumnPermission('created_at')" prop="created_at" label="创建时间" width="160" />
|
||||
<el-table-column v-if="userStore.hasPermission('inbound_service:operation')" label="操作" width="180" fixed="right">
|
||||
<template #default="{row}">
|
||||
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
@ -134,11 +134,11 @@
|
||||
|
||||
<div class="read-only-grid" v-if="form.base_id">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" disabled class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" disabled class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类别"><el-input v-model="form.category" disabled class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="规格型号"><el-input v-model="form.spec_model" disabled class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="单位"><el-input v-model="form.unit" disabled class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="名称" v-if="hasFormFieldPermission('material_name')"><el-input v-model="form.material_name" disabled class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类型" v-if="hasFormFieldPermission('material_type')"><el-input v-model="form.material_type" disabled class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类别" v-if="hasFormFieldPermission('category')"><el-input v-model="form.category" disabled class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="规格型号" v-if="hasFormFieldPermission('spec_model')"><el-input v-model="form.spec_model" disabled class="is-text-view"/></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="单位" v-if="hasFormFieldPermission('unit')"><el-input v-model="form.unit" disabled class="is-text-view"/></el-form-item></el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
@ -150,7 +150,7 @@
|
||||
<span>2. 服务详情</span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<el-form-item label="售价" prop="sale_price">
|
||||
<el-form-item label="售价" prop="sale_price" v-if="hasFormFieldPermission('sale_price')">
|
||||
<el-input-number
|
||||
v-model="form.sale_price"
|
||||
placeholder="请输入售价"
|
||||
@ -160,7 +160,7 @@
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="服务商" prop="provider_name">
|
||||
<el-form-item label="服务商" prop="provider_name" v-if="hasFormFieldPermission('provider_name')">
|
||||
<el-autocomplete
|
||||
v-model="form.provider_name"
|
||||
:fetch-suggestions="querySearchProvider"
|
||||
@ -171,7 +171,7 @@
|
||||
@select="handleProviderSelect"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="简介" prop="description">
|
||||
<el-form-item label="简介" prop="description" v-if="hasFormFieldPermission('description')">
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
@ -198,6 +198,7 @@ import { ref, reactive, onMounted } from 'vue'
|
||||
import { InfoFilled, Box, House } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import {
|
||||
getServiceList,
|
||||
createService,
|
||||
@ -212,6 +213,53 @@ import {
|
||||
type MaterialBaseItem
|
||||
} from '@/api/inbound/service'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 列与权限Code的映射关系(数据库中的code)
|
||||
const permissionMap: Record<string, string> = {
|
||||
sku: 'inbound_service:sku',
|
||||
material_name: 'inbound_service:material_name',
|
||||
provider_name: 'inbound_service:provider_name',
|
||||
sale_price: 'inbound_service:sale_price',
|
||||
description: 'inbound_service:description',
|
||||
created_at: 'inbound_service:created_at',
|
||||
}
|
||||
|
||||
// 检查列权限
|
||||
const hasColumnPermission = (prop: string) => {
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||||
return true
|
||||
}
|
||||
const code = permissionMap[prop]
|
||||
return code ? userStore.hasPermission(code) : false
|
||||
}
|
||||
|
||||
// 表单字段权限检查
|
||||
const hasFormFieldPermission = (fieldName: string) => {
|
||||
// 超级管理员直接返回true
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||||
return true
|
||||
}
|
||||
// 根据字段名映射到权限码
|
||||
const map: Record<string, string> = {
|
||||
base_id: 'inbound_service:base_id',
|
||||
material_name: 'inbound_service:material_name',
|
||||
spec_model: 'inbound_service:spec_model',
|
||||
category: 'inbound_service:category',
|
||||
material_type: 'inbound_service:material_type',
|
||||
unit: 'inbound_service:unit',
|
||||
sale_price: 'inbound_service:sale_price',
|
||||
provider_name: 'inbound_service:provider_name',
|
||||
description: 'inbound_service:description',
|
||||
}
|
||||
const code = map[fieldName]
|
||||
if (!code) {
|
||||
// 没有映射的字段默认显示
|
||||
return true
|
||||
}
|
||||
return userStore.hasPermission(code)
|
||||
}
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<ServiceItem[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
@ -10,12 +10,12 @@
|
||||
<p class="subtitle">单件自动确认,多件弹窗录入</p>
|
||||
|
||||
<div class="idle-actions">
|
||||
<el-button type="primary" size="large" class="action-btn-full" @click="startNewSession" :loading="btnLoading">
|
||||
<el-button v-if="userStore.hasPermission('inventory_stocktake:operation')" type="primary" size="large" class="action-btn-full" @click="startNewSession" :loading="btnLoading">
|
||||
开始新盘点
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
v-if="serverDraftCount > 0"
|
||||
v-if="serverDraftCount > 0 && userStore.hasPermission('inventory_stocktake:operation')"
|
||||
type="warning"
|
||||
plain
|
||||
size="large"
|
||||
@ -43,7 +43,7 @@
|
||||
<el-tag v-else-if="syncStatus === 'syncing'" type="warning" size="small" effect="dark" round>同步中...</el-tag>
|
||||
<el-tag v-else type="danger" size="small" effect="dark" round>同步失败</el-tag>
|
||||
</div>
|
||||
<el-button type="info" text bg size="small" @click="pauseSession" :icon="VideoPause">
|
||||
<el-button v-if="userStore.hasPermission('inventory_stocktake:operation')" type="info" text bg size="small" @click="pauseSession" :icon="VideoPause">
|
||||
暂停
|
||||
</el-button>
|
||||
</div>
|
||||
@ -84,7 +84,7 @@
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-button type="danger" size="large" class="w-100 action-btn" @click="openFinishDialog" :icon="Checked">
|
||||
<el-button v-if="userStore.hasPermission('inventory_stocktake:operation')" type="danger" size="large" class="w-100 action-btn" @click="openFinishDialog" :icon="Checked">
|
||||
结束盘点
|
||||
</el-button>
|
||||
</el-col>
|
||||
@ -132,6 +132,7 @@
|
||||
style="width: 100%"
|
||||
ref="qtyInputRef"
|
||||
placeholder="请输入实际点数"
|
||||
class="large-control-input"
|
||||
/>
|
||||
<p class="unit-text">单位: {{ currentItem.unit || '个' }}</p>
|
||||
</div>
|
||||
@ -139,7 +140,7 @@
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="showQtyDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleManualConfirm" size="large">确认数量</el-button>
|
||||
<el-button v-if="userStore.hasPermission('inventory_stocktake:operation')" type="primary" @click="handleManualConfirm" size="large">确认数量</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@ -190,6 +191,7 @@
|
||||
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="userStore.hasPermission('inventory_stocktake:operation')"
|
||||
type="primary"
|
||||
link
|
||||
icon="Edit"
|
||||
@ -237,7 +239,7 @@
|
||||
<el-button @click="showFinishDialog = false">返回修改</el-button>
|
||||
<div class="footer-right">
|
||||
<el-button type="success" @click="exportToExcel" :icon="Download">导出Excel</el-button>
|
||||
<el-button type="danger" @click="finishStocktake" :loading="printing" :icon="Checked">结束</el-button>
|
||||
<el-button v-if="userStore.hasPermission('inventory_stocktake:operation')" type="danger" @click="finishStocktake" :loading="printing" :icon="Checked">结束</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -276,6 +278,8 @@ interface StockItem {
|
||||
type?: string
|
||||
category?: string
|
||||
price?: number
|
||||
source_table?: string
|
||||
stock_id?: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
@ -292,6 +296,7 @@ const showQtyDialog = ref(false)
|
||||
|
||||
const allData = ref<StockItem[]>([])
|
||||
const scannedMap = ref<Map<string, number>>(new Map())
|
||||
const borrowedQuantities = ref<Record<string, number>>({})
|
||||
|
||||
const filterType = ref('all')
|
||||
const searchKeyword = ref('')
|
||||
@ -306,6 +311,34 @@ const api = {
|
||||
clearDraft: () => request({ url: '/v1/inbound/stock/draft/clear', method: 'post', data: { user_id: currentUser } })
|
||||
}
|
||||
|
||||
const typeToSourceTable = (type: string): string => {
|
||||
switch (type) {
|
||||
case 'material': return 'stock_buy'
|
||||
case 'semi': return 'stock_semi'
|
||||
case 'product': return 'stock_product'
|
||||
default: return ''
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchBorrowedQuantities(items: StockItem[]): Promise<void> {
|
||||
const payload = items.filter(i => i.source_table && i.stock_id).map(i => ({
|
||||
source_table: i.source_table,
|
||||
stock_id: i.stock_id
|
||||
}))
|
||||
if (payload.length === 0) return
|
||||
try {
|
||||
const res = await request({
|
||||
url: '/v1/inbound/stock/borrowed-quantities',
|
||||
method: 'post',
|
||||
data: { items: payload }
|
||||
})
|
||||
// res is map of key->qty
|
||||
borrowedQuantities.value = { ...borrowedQuantities.value, ...res }
|
||||
} catch (e) {
|
||||
console.error('获取借出数量失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await checkServerDraft()
|
||||
})
|
||||
@ -380,7 +413,9 @@ const loadData = async () => {
|
||||
qty_stock: stock,
|
||||
qty_actual: isScanned ? scannedMap.value.get(uuid)! : 0,
|
||||
scanned: isScanned,
|
||||
uniqueKey: `${type}_${item.id}`
|
||||
uniqueKey: `${type}_${item.id}`,
|
||||
source_table: typeToSourceTable(type),
|
||||
stock_id: item.id
|
||||
})
|
||||
}
|
||||
|
||||
@ -389,6 +424,7 @@ const loadData = async () => {
|
||||
if (res.products) res.products.forEach((i: any) => processItem(i, 'product'))
|
||||
|
||||
allData.value = list
|
||||
await fetchBorrowedQuantities(list)
|
||||
} catch (e) {
|
||||
ElMessage.error('数据加载失败')
|
||||
} finally { loading.value = false }
|
||||
@ -470,34 +506,47 @@ const closeOverlays = () => {
|
||||
const exportToExcel = () => {
|
||||
try {
|
||||
// 1. 已盘点 Sheet
|
||||
const scannedData = allData.value.filter(i => i.scanned).map(item => ({
|
||||
'物品名称': item.name,
|
||||
'类型': item.type || item.material_type || '-',
|
||||
'类别': item.category || '-',
|
||||
'规格型号': item.spec_model || item.standard || '-', // ★ 双重保险
|
||||
'SKU': item.sku,
|
||||
'批次/SN': item.serial_number || item.batch_no || '-',
|
||||
'单位': item.unit || '个',
|
||||
'单价': item.price || item.unit_price || 0,
|
||||
'账面库存': parseFloat(item.qty_stock as any),
|
||||
'实盘数量': item.qty_actual,
|
||||
'盘点结果': item.qty_stock === item.qty_actual ? '相符' : '差异',
|
||||
'差异数': item.qty_actual - item.qty_stock
|
||||
}))
|
||||
const scannedData = allData.value.filter(i => i.scanned).map(item => {
|
||||
const key = item.source_table && item.stock_id ? `${item.source_table}_${item.stock_id}` : ''
|
||||
const borrowedQty = borrowedQuantities.value[key] || 0
|
||||
const actualTotal = item.qty_actual + borrowedQty
|
||||
const diff = actualTotal - item.qty_stock
|
||||
const result = diff === 0 ? '正常' : diff < 0 ? '盘亏/差异' : '盘盈'
|
||||
return {
|
||||
'物品名称': item.name,
|
||||
'类型': item.type || item.material_type || '-',
|
||||
'类别': item.category || '-',
|
||||
'规格型号': item.spec_model || item.standard || '-', // ★ 双重保险
|
||||
'SKU': item.sku,
|
||||
'批次/SN': item.serial_number || item.batch_no || '-',
|
||||
'单位': item.unit || '个',
|
||||
'单价': item.price || item.unit_price || 0,
|
||||
'账面库存': parseFloat(item.qty_stock as any),
|
||||
'实盘数量': item.qty_actual,
|
||||
'借出未还数量': borrowedQty,
|
||||
'盘点结果': result,
|
||||
'差异数': diff
|
||||
}
|
||||
})
|
||||
|
||||
// 2. 未盘点 Sheet
|
||||
const missingData = allData.value.filter(i => !i.scanned).map(item => ({
|
||||
'物品名称': item.name,
|
||||
'类型': item.type || item.material_type || '-',
|
||||
'类别': item.category || '-',
|
||||
'规格型号': item.spec_model || item.standard || '-', // ★ 双重保险
|
||||
'SKU': item.sku,
|
||||
'批次/SN': item.serial_number || item.batch_no || '-',
|
||||
'单位': item.unit || '个',
|
||||
'单价': item.price || item.unit_price || 0,
|
||||
'账面库存': parseFloat(item.qty_stock as any),
|
||||
'状态': '未盘点'
|
||||
}))
|
||||
const missingData = allData.value.filter(i => !i.scanned).map(item => {
|
||||
const key = item.source_table && item.stock_id ? `${item.source_table}_${item.stock_id}` : ''
|
||||
const borrowedQty = borrowedQuantities.value[key] || 0
|
||||
return {
|
||||
'物品名称': item.name,
|
||||
'类型': item.type || item.material_type || '-',
|
||||
'类别': item.category || '-',
|
||||
'规格型号': item.spec_model || item.standard || '-', // ★ 双重保险
|
||||
'SKU': item.sku,
|
||||
'批次/SN': item.serial_number || item.batch_no || '-',
|
||||
'单位': item.unit || '个',
|
||||
'单价': item.price || item.unit_price || 0,
|
||||
'账面库存': parseFloat(item.qty_stock as any),
|
||||
'借出未还数量': borrowedQty,
|
||||
'状态': '未盘点'
|
||||
}
|
||||
})
|
||||
|
||||
const wb = XLSX.utils.book_new()
|
||||
const ws1 = XLSX.utils.json_to_sheet(scannedData)
|
||||
@ -506,7 +555,7 @@ const exportToExcel = () => {
|
||||
const wscols = [
|
||||
{wch: 20}, {wch: 10}, {wch: 10}, {wch: 15},
|
||||
{wch: 15}, {wch: 15}, {wch: 5}, {wch: 8},
|
||||
{wch: 8}, {wch: 8}, {wch: 8}, {wch: 8}
|
||||
{wch: 8}, {wch: 8}, {wch: 8}, {wch: 8}, {wch: 8}
|
||||
]
|
||||
ws1['!cols'] = wscols
|
||||
ws2['!cols'] = wscols
|
||||
@ -690,4 +739,26 @@ const finishStocktake = async () => {
|
||||
.missing-list-header { font-weight: bold; margin-bottom: 8px; font-size: 13px; border-left: 3px solid #f56c6c; padding-left: 8px; }
|
||||
.dialog-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 10px; }
|
||||
.footer-right { display: flex; gap: 10px; }
|
||||
|
||||
/* ★★★ 新增:专为平板优化的超大数字输入框样式 ★★★ */
|
||||
.large-control-input {
|
||||
height: 60px; /* 增加整体输入框高度 */
|
||||
}
|
||||
/* 放大左右两边的加减按钮,并增加点击区域 */
|
||||
:deep(.large-control-input .el-input-number__decrease),
|
||||
:deep(.large-control-input .el-input-number__increase) {
|
||||
width: 70px !important; /* 显著加大按钮宽度 */
|
||||
font-size: 28px !important; /* 放大加号和减号图标 */
|
||||
background-color: #f0f2f5; /* 稍微加深背景色,让触控区更明显 */
|
||||
}
|
||||
/* 给中间的数字输入区留出左右按钮的空间 */
|
||||
:deep(.large-control-input .el-input__wrapper) {
|
||||
padding-left: 80px !important;
|
||||
padding-right: 80px !important;
|
||||
}
|
||||
/* 放大中间数字部分的字体和高度,保持协调 */
|
||||
:deep(.large-control-input .el-input__inner) {
|
||||
font-size: 24px !important;
|
||||
height: 58px !important;
|
||||
}
|
||||
</style>
|
||||
632
inventory-web/src/views/system/PermissionConfig.vue
Normal file
632
inventory-web/src/views/system/PermissionConfig.vue
Normal file
@ -0,0 +1,632 @@
|
||||
<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)
|
||||
|
||||
// 【新增功能】如果没有字段级控制,且存在操作权限,则自动联动勾选可编辑
|
||||
if ((!row.elements || row.elements.length === 0) && row.operationCode) {
|
||||
row.hasWrite = true
|
||||
}
|
||||
}
|
||||
|
||||
// 联动子菜单:如果父级关闭,子级是否关闭?通常不强制,但可以做
|
||||
}
|
||||
|
||||
// 当“可编辑”改变
|
||||
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>
|
||||
@ -4,7 +4,7 @@
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span style="font-weight: bold;">员工账号管理</span>
|
||||
<el-button type="primary" @click="handleCreate">
|
||||
<el-button v-if="userStore.hasPermission('system_user:operation')" type="primary" @click="handleCreate">
|
||||
+ 新增员工
|
||||
</el-button>
|
||||
</div>
|
||||
@ -16,23 +16,33 @@
|
||||
border
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="username" label="用户标识" min-width="180" />
|
||||
<el-table-column v-if="hasColumnPermission('username')" prop="username" label="用户标识" min-width="180" />
|
||||
|
||||
<el-table-column prop="department" label="所属部门" width="150">
|
||||
<el-table-column v-if="hasColumnPermission('department')" prop="department" label="所属部门" width="150">
|
||||
<template #default="scope">
|
||||
<el-tag>{{ scope.row.department }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="role" label="系统角色" width="180">
|
||||
<el-table-column v-if="hasColumnPermission('role')" prop="role" label="系统角色" width="180">
|
||||
<template #default="scope">
|
||||
{{ formatRole(scope.row.role) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="email" label="邮箱" min-width="200" />
|
||||
<el-table-column v-if="hasColumnPermission('email')" prop="email" label="邮箱" min-width="200" />
|
||||
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<el-table-column v-if="hasColumnPermission('status')" prop="status" label="状态" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 'active' ? 'success' : 'danger'">
|
||||
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column v-if="hasColumnPermission('created_at')" prop="created_at" label="创建时间" width="160" />
|
||||
|
||||
<el-table-column v-if="userStore.hasPermission('system_user:operation')" label="操作" width="180" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-popconfirm title="确定要删除该用户吗?" @confirm="handleDelete(scope.row)">
|
||||
@ -57,20 +67,20 @@
|
||||
:rules="rules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="真实姓名" prop="cn_name">
|
||||
<el-form-item label="真实姓名" prop="cn_name" v-if="hasFormFieldPermission('cn_name')">
|
||||
<el-input
|
||||
v-model="form.cn_name"
|
||||
placeholder="请输入中文姓名 (如: 张三)"
|
||||
:disabled="isEdit"
|
||||
:disabled="isEdit || !userStore.hasPermission('system_user:operation')"
|
||||
@input="handleNameInput"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="登录账号" prop="username">
|
||||
<el-form-item label="登录账号" prop="username" v-if="hasFormFieldPermission('username')">
|
||||
<el-input
|
||||
v-model="form.username"
|
||||
placeholder="自动生成,可修改 (如: zhangsan)"
|
||||
:disabled="isEdit"
|
||||
:disabled="isEdit || !userStore.hasPermission('system_user:operation')"
|
||||
>
|
||||
<template #append>
|
||||
<span v-if="!isEdit" style="font-size: 12px; color: #999;">重复自动+1</span>
|
||||
@ -78,16 +88,17 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-form-item label="密码" prop="password" v-if="hasFormFieldPermission('password')">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
show-password
|
||||
:placeholder="isEdit ? '不修改请留空' : '设置初始密码'"
|
||||
:disabled="!userStore.hasPermission('system_user:operation')"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="所属部门" prop="department">
|
||||
<el-form-item label="所属部门" prop="department" v-if="hasFormFieldPermission('department')">
|
||||
<el-select
|
||||
v-model="form.department"
|
||||
placeholder="请输入或选择部门"
|
||||
@ -95,13 +106,14 @@
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
:disabled="!userStore.hasPermission('system_user:operation')"
|
||||
>
|
||||
<el-option v-for="item in departmentOptions" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="系统角色" prop="role">
|
||||
<el-select v-model="form.role" placeholder="授予权限" style="width: 100%">
|
||||
<el-form-item label="系统角色" prop="role" v-if="hasFormFieldPermission('role')">
|
||||
<el-select v-model="form.role" placeholder="授予权限" style="width: 100%" :disabled="!userStore.hasPermission('system_user:operation')">
|
||||
<el-option
|
||||
v-for="option in roleOptions"
|
||||
:key="option.value"
|
||||
@ -111,15 +123,15 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
||||
<el-form-item label="邮箱" prop="email" v-if="hasFormFieldPermission('email')">
|
||||
<el-input v-model="form.email" placeholder="请输入邮箱" :disabled="!userStore.hasPermission('system_user:operation')" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="onSubmit" :loading="submitLoading">
|
||||
<el-button v-if="userStore.hasPermission('system_user:operation')" type="primary" @click="onSubmit" :loading="submitLoading">
|
||||
{{ isEdit ? '确认修改' : '确认创建' }}
|
||||
</el-button>
|
||||
</div>
|
||||
@ -136,6 +148,37 @@ import { ElMessage } from 'element-plus'
|
||||
import { pinyin } from 'pinyin-pro' // ★ 务必安装: npm install pinyin-pro
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 列与权限Code的映射关系(数据库中的code)
|
||||
const permissionMap: Record<string, string> = {
|
||||
username: 'system_user:username',
|
||||
department: 'system_user:department',
|
||||
role: 'system_user:role',
|
||||
email: 'system_user:email',
|
||||
status: 'system_user:status',
|
||||
created_at: 'system_user:created_at',
|
||||
// 表单字段
|
||||
cn_name: 'system_user:username',
|
||||
password: 'system_user:password',
|
||||
}
|
||||
|
||||
// 检查列权限
|
||||
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 hasFormFieldPermission = (fieldName: string) => {
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||||
return true
|
||||
}
|
||||
const code = permissionMap[fieldName]
|
||||
return code ? userStore.hasPermission(code) : false
|
||||
}
|
||||
const tableLoading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
@ -241,6 +284,7 @@ const getList = async () => {
|
||||
tableData.value = res.data || []
|
||||
extractDepartments(tableData.value)
|
||||
} catch (error) {
|
||||
// 错误已由全局拦截器统一处理
|
||||
console.error('Fetch users failed:', error)
|
||||
} finally {
|
||||
tableLoading.value = false
|
||||
@ -317,7 +361,7 @@ const onSubmit = async () => {
|
||||
dialogVisible.value = false
|
||||
getList()
|
||||
} catch (error) {
|
||||
// request 拦截器会处理错误
|
||||
// 错误已由全局拦截器统一处理
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
@ -342,6 +386,7 @@ const handleDelete = async (row: any) => {
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
} catch (error) {
|
||||
// 错误已由全局拦截器统一处理
|
||||
}
|
||||
}
|
||||
|
||||
@ -378,4 +423,4 @@ onMounted(() => {
|
||||
text-align: right;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -13,10 +13,14 @@
|
||||
</template>
|
||||
|
||||
<div class="scan-section">
|
||||
<div class="camera-placeholder" @click="showCamera = true">
|
||||
<div v-if="userStore.hasPermission('op_borrow:operation')" class="camera-placeholder" @click="showCamera = true">
|
||||
<el-icon :size="40" color="#409EFF"><CameraFilled /></el-icon>
|
||||
<span class="text">点击开启全屏扫码</span>
|
||||
</div>
|
||||
<div v-else class="camera-placeholder" style="background-color: #f5f5f5; cursor: not-allowed;">
|
||||
<el-icon :size="40" color="#909399"><CameraFilled /></el-icon>
|
||||
<span class="text">无扫码权限</span>
|
||||
</div>
|
||||
|
||||
<div class="input-box">
|
||||
<el-input
|
||||
@ -26,12 +30,13 @@
|
||||
clearable
|
||||
ref="barcodeRef"
|
||||
size="large"
|
||||
:disabled="!userStore.hasPermission('op_borrow:operation')"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Scissor /></el-icon>
|
||||
</template>
|
||||
<template #append>
|
||||
<el-button @click="handleManualInput">添加</el-button>
|
||||
<el-button @click="handleManualInput" :disabled="!userStore.hasPermission('op_borrow:operation')">添加</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
@ -40,16 +45,16 @@
|
||||
<div class="cart-section">
|
||||
<div v-if="cartItems.length > 0">
|
||||
<el-table :data="cartItems" border stripe style="width: 100%">
|
||||
<el-table-column prop="name" label="物品名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="sku" label="SKU" width="120" show-overflow-tooltip />
|
||||
<el-table-column v-if="hasColumnPermission('name')" prop="name" label="物品名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column v-if="hasColumnPermission('sku')" prop="sku" label="SKU" width="120" show-overflow-tooltip />
|
||||
|
||||
<el-table-column label="可用库存" width="90" align="center">
|
||||
<el-table-column v-if="hasColumnPermission('available_quantity')" label="可用库存" width="90" align="center">
|
||||
<template #default="{row}">
|
||||
<el-tag type="info">{{ parseFloat(row.available_quantity) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="借用数" width="130" align="center">
|
||||
<el-table-column v-if="hasColumnPermission('out_quantity')" label="借用数" width="130" align="center">
|
||||
<template #default="{row}">
|
||||
<el-input-number
|
||||
v-model="row.out_quantity"
|
||||
@ -57,11 +62,12 @@
|
||||
:max="parseFloat(row.available_quantity)"
|
||||
size="small"
|
||||
style="width: 100px"
|
||||
:disabled="!userStore.hasPermission('op_borrow:operation')"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="60" align="center" fixed="right">
|
||||
<el-table-column v-if="userStore.hasPermission('op_borrow:operation')" label="操作" width="60" align="center" fixed="right">
|
||||
<template #default="{$index}">
|
||||
<el-button type="danger" icon="Delete" circle size="small" @click="removeFromCart($index)" />
|
||||
</template>
|
||||
@ -102,7 +108,7 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="领用人签名确认" required>
|
||||
<div class="signature-box" @click="openSignatureDialog">
|
||||
<div class="signature-box" @click="openSignatureDialog" v-if="userStore.hasPermission('op_borrow:operation')">
|
||||
<div v-if="signaturePreviewUrl" class="signed-img">
|
||||
<img :src="signaturePreviewUrl" alt="签名" />
|
||||
<span class="re-sign-tip">点击重签</span>
|
||||
@ -112,11 +118,17 @@
|
||||
<span>点击此处进行全屏签名</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="signature-box" style="background-color: #f5f5f5; cursor: not-allowed;">
|
||||
<div class="unsigned-placeholder">
|
||||
<el-icon :size="24"><EditPen /></el-icon>
|
||||
<span>无签名权限</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<div class="bottom-actions">
|
||||
<el-button @click="clearAll" icon="Refresh">清空</el-button>
|
||||
<el-button type="primary" size="large" :loading="loading" @click="submitForm" icon="Select">
|
||||
<el-button v-if="userStore.hasPermission('op_borrow:operation')" @click="clearAll" icon="Refresh">清空</el-button>
|
||||
<el-button v-if="userStore.hasPermission('op_borrow:operation')" type="primary" size="large" :loading="loading" @click="submitForm" icon="Select">
|
||||
确认借出
|
||||
</el-button>
|
||||
</div>
|
||||
@ -187,6 +199,27 @@ import QrScanner from '@/components/QrScanner/index.vue'
|
||||
import { getStockByBarcode } from '@/api/outbound'
|
||||
import request from '@/utils/request'
|
||||
import { uploadFile } from '@/api/common/upload'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 列与权限Code的映射关系(数据库中的code)
|
||||
const permissionMap: Record<string, string> = {
|
||||
borrower_name: 'op_borrow:borrower_name',
|
||||
sku: 'op_borrow:sku',
|
||||
available_quantity: 'op_borrow:available_quantity',
|
||||
out_quantity: 'op_borrow:out_quantity',
|
||||
// 其他字段可根据需要添加
|
||||
}
|
||||
|
||||
// 检查列权限
|
||||
const hasColumnPermission = (prop: string) => {
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||||
return true
|
||||
}
|
||||
const code = permissionMap[prop]
|
||||
return code ? userStore.hasPermission(code) : false
|
||||
}
|
||||
|
||||
// --- 状态定义 ---
|
||||
const barcodeInput = ref('')
|
||||
@ -564,4 +597,4 @@ onUnmounted(() => {
|
||||
.sidebar-actions { flex-direction: row; width: 100%; gap: 10px; }
|
||||
.sidebar-actions .el-button { flex: 1; height: 40px; }
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -19,12 +19,12 @@
|
||||
v-loading="loading"
|
||||
:row-class-name="tableRowClassName"
|
||||
>
|
||||
<el-table-column prop="borrow_no" label="单号" width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="borrower_name" label="借用人" width="100" />
|
||||
<el-table-column prop="sku" label="SKU" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="borrow_time" label="借出时间" width="160" sortable />
|
||||
<el-table-column v-if="hasColumnPermission('borrow_no')" prop="borrow_no" label="单号" width="180" show-overflow-tooltip />
|
||||
<el-table-column v-if="hasColumnPermission('borrower_name')" prop="borrower_name" label="借用人" width="100" />
|
||||
<el-table-column v-if="hasColumnPermission('sku')" prop="sku" label="SKU" width="120" show-overflow-tooltip />
|
||||
<el-table-column v-if="hasColumnPermission('borrow_time')" prop="borrow_time" label="借出时间" width="160" sortable />
|
||||
|
||||
<el-table-column label="归还时间 / 预计" min-width="200">
|
||||
<el-table-column v-if="hasColumnPermission('expected_return_time') || hasColumnPermission('return_time')" label="归还时间 / 预计" min-width="200">
|
||||
<template #default="{row}">
|
||||
<div v-if="row.status === 'returned'">
|
||||
<el-tag type="success" size="small">实际</el-tag>
|
||||
@ -40,7 +40,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<el-table-column v-if="hasColumnPermission('status')" label="状态" width="100" align="center">
|
||||
<template #default="{row}">
|
||||
<el-tag :type="row.status==='returned'?'success':'warning'">
|
||||
{{ row.status==='returned'?'已还':'借出中' }}
|
||||
@ -48,22 +48,22 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="归还库位" min-width="120">
|
||||
<el-table-column v-if="hasColumnPermission('return_location')" label="归还库位" min-width="120">
|
||||
<template #default="{row}">
|
||||
<span v-if="row.return_location">{{ row.return_location }}</span>
|
||||
<span v-else style="color:#ccc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="电子签名" width="140" align="center">
|
||||
<el-table-column v-if="hasColumnPermission('borrow_signature') || hasColumnPermission('return_signature')" label="电子签名" width="140" align="center">
|
||||
<template #default="{row}">
|
||||
<div style="display:flex; justify-content: center; gap:10px">
|
||||
<el-popover trigger="hover" placement="top" v-if="row.borrow_signature" width="220">
|
||||
<el-popover trigger="hover" placement="top" v-if="row.borrow_signature && hasColumnPermission('borrow_signature')" width="220">
|
||||
<template #reference><el-tag size="small">借</el-tag></template>
|
||||
<img :src="row.borrow_signature" style="width:200px; border:1px solid #eee" />
|
||||
</el-popover>
|
||||
|
||||
<el-popover trigger="hover" placement="top" v-if="row.return_signature" width="220">
|
||||
<el-popover trigger="hover" placement="top" v-if="row.return_signature && hasColumnPermission('return_signature')" width="220">
|
||||
<template #reference><el-tag type="success" size="small">还</el-tag></template>
|
||||
<img :src="row.return_signature" style="width:200px; border:1px solid #eee" />
|
||||
</el-popover>
|
||||
@ -88,6 +88,32 @@ import request from '@/utils/request'
|
||||
import dayjs from 'dayjs' // 建议使用 dayjs 处理日期,如果没有安装,可以用原生 Date
|
||||
import 'dayjs/locale/zh-cn' // 导入中文包
|
||||
dayjs.locale('zh-cn')
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 列与权限Code的映射关系(数据库中的code)
|
||||
const permissionMap: Record<string, string> = {
|
||||
borrow_no: 'op_records:borrow_no',
|
||||
borrower_name: 'op_records:borrower_name',
|
||||
sku: 'op_records:sku',
|
||||
borrow_time: 'op_records:borrow_time',
|
||||
return_time: 'op_records:return_time',
|
||||
status: 'op_records:status',
|
||||
expected_return_time: 'op_records:expected_return_time',
|
||||
return_location: 'op_records:return_location',
|
||||
borrow_signature: 'op_records:borrow_signature',
|
||||
return_signature: 'op_records:return_signature',
|
||||
}
|
||||
|
||||
// 检查列权限
|
||||
const hasColumnPermission = (prop: string) => {
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||||
return true
|
||||
}
|
||||
const code = permissionMap[prop]
|
||||
return code ? userStore.hasPermission(code) : false
|
||||
}
|
||||
|
||||
const list = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
@ -195,4 +221,4 @@ onMounted(fetchData)
|
||||
.text-normal {
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -13,10 +13,14 @@
|
||||
</template>
|
||||
|
||||
<div class="scan-section">
|
||||
<div class="camera-placeholder" @click="showCamera = true">
|
||||
<div v-if="userStore.hasPermission('op_return:operation')" class="camera-placeholder" @click="showCamera = true">
|
||||
<el-icon :size="40" color="#409EFF"><CameraFilled /></el-icon>
|
||||
<span class="text">点击开启全屏扫码</span>
|
||||
</div>
|
||||
<div v-else class="camera-placeholder" style="background-color: #f5f5f5; cursor: not-allowed;">
|
||||
<el-icon :size="40" color="#909399"><CameraFilled /></el-icon>
|
||||
<span class="text">无扫码权限</span>
|
||||
</div>
|
||||
|
||||
<div class="input-box">
|
||||
<el-input
|
||||
@ -26,12 +30,13 @@
|
||||
clearable
|
||||
ref="barcodeRef"
|
||||
size="large"
|
||||
:disabled="!userStore.hasPermission('op_return:operation')"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Scissor /></el-icon>
|
||||
</template>
|
||||
<template #append>
|
||||
<el-button @click="scanItem">识别</el-button>
|
||||
<el-button @click="scanItem" :disabled="!userStore.hasPermission('op_return:operation')">识别</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
@ -40,16 +45,17 @@
|
||||
<div class="cart-section">
|
||||
<div v-if="returnList.length > 0">
|
||||
<el-table :data="returnList" border stripe style="width: 100%">
|
||||
<el-table-column prop="borrower_name" label="借用人" width="90" show-overflow-tooltip />
|
||||
<el-table-column prop="sku" label="SKU" width="120" show-overflow-tooltip />
|
||||
<el-table-column v-if="hasColumnPermission('borrower_name')" prop="borrower_name" label="借用人" width="90" show-overflow-tooltip />
|
||||
<el-table-column v-if="hasColumnPermission('sku')" prop="sku" label="SKU" width="120" show-overflow-tooltip />
|
||||
|
||||
<el-table-column label="归还库位(可改)" min-width="160">
|
||||
<el-table-column v-if="hasColumnPermission('return_location')" label="归还库位(可改)" min-width="160">
|
||||
<template #default="{row}">
|
||||
<el-input
|
||||
v-model="row.return_location"
|
||||
:placeholder="`原: ${row.current_location || '无'}`"
|
||||
clearable
|
||||
size="small"
|
||||
:disabled="!userStore.hasPermission('op_return:operation')"
|
||||
>
|
||||
<template #append v-if="row.return_location !== row.current_location">
|
||||
<span style="color: #E6A23C; font-size: 12px;">变更</span>
|
||||
@ -58,7 +64,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="60" align="center" fixed="right">
|
||||
<el-table-column v-if="userStore.hasPermission('op_return:operation')" label="操作" width="60" align="center" fixed="right">
|
||||
<template #default="{$index}">
|
||||
<el-button type="danger" icon="Delete" circle size="small" @click="returnList.splice($index, 1)" />
|
||||
</template>
|
||||
@ -77,7 +83,7 @@
|
||||
|
||||
<el-form label-position="top">
|
||||
<el-form-item required>
|
||||
<div class="signature-box" @click="openSignatureDialog">
|
||||
<div class="signature-box" @click="openSignatureDialog" v-if="userStore.hasPermission('op_return:operation')">
|
||||
<div v-if="signaturePreviewUrl" class="signed-img">
|
||||
<img :src="signaturePreviewUrl" alt="签名" />
|
||||
<span class="re-sign-tip">点击重签</span>
|
||||
@ -87,12 +93,18 @@
|
||||
<span>点击此处进行库管签名</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="signature-box" style="background-color: #f5f5f5; cursor: not-allowed;">
|
||||
<div class="unsigned-placeholder">
|
||||
<el-icon :size="24"><EditPen /></el-icon>
|
||||
<span>无签名权限</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="bottom-actions">
|
||||
<el-button @click="clearAll" icon="Refresh">清空</el-button>
|
||||
<el-button type="success" size="large" :loading="loading" @click="preSubmitCheck" icon="Select">
|
||||
<el-button v-if="userStore.hasPermission('op_return:operation')" @click="clearAll" icon="Refresh">清空</el-button>
|
||||
<el-button v-if="userStore.hasPermission('op_return:operation')" type="success" size="large" :loading="loading" @click="preSubmitCheck" icon="Select">
|
||||
确认归还
|
||||
</el-button>
|
||||
</div>
|
||||
@ -161,6 +173,25 @@ import { uploadFile } from '@/api/common/upload'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Scissor, EditPen, Delete, CameraFilled, Close, Refresh, Select } from '@element-plus/icons-vue'
|
||||
import QrScanner from '@/components/QrScanner/index.vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 列与权限Code的映射关系(数据库中的code)
|
||||
const permissionMap: Record<string, string> = {
|
||||
borrower_name: 'op_return:borrower_name',
|
||||
sku: 'op_return:sku',
|
||||
return_location: 'op_return:return_location',
|
||||
}
|
||||
|
||||
// 检查列权限
|
||||
const hasColumnPermission = (prop: string) => {
|
||||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||||
return true
|
||||
}
|
||||
const code = permissionMap[prop]
|
||||
return code ? userStore.hasPermission(code) : false
|
||||
}
|
||||
|
||||
// --- 状态 ---
|
||||
const barcode = ref('')
|
||||
@ -507,4 +538,4 @@ onUnmounted(() => {
|
||||
.sidebar-actions { flex-direction: row; width: 100%; gap: 10px; }
|
||||
.sidebar-actions .el-button { flex: 1; height: 40px; }
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user