权限管理,没有页面修改之前版本
This commit is contained in:
@ -80,7 +80,6 @@ def create_app():
|
|||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 2.4 注册业务操作模块 (Transactions - 借还/维修/报废)
|
# 2.4 注册业务操作模块 (Transactions - 借还/维修/报废)
|
||||||
# ★★★ 关键修改:将前缀改为 /api/v1/transactions 以匹配前端请求 ★★★
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
try:
|
try:
|
||||||
from app.api.v1.transactions import trans_bp
|
from app.api.v1.transactions import trans_bp
|
||||||
@ -90,8 +89,7 @@ def create_app():
|
|||||||
app.register_blueprint(trans_bp, url_prefix='/api/transactions', name='trans_legacy')
|
app.register_blueprint(trans_bp, url_prefix='/api/transactions', name='trans_legacy')
|
||||||
print("✅ Transactions 模块注册成功")
|
print("✅ Transactions 模块注册成功")
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
# 允许模块不存在时不崩溃,但在开发借还功能时这里报错说明 trans_bp 定义有问题
|
print(f"⚠️ 提示: Transaction 模块导入失败: {e}")
|
||||||
print(f"⚠️ 提示: Transaction 模块导入失败 (请检查 app/api/v1/transactions.py): {e}")
|
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 2.5 注册出库模块 (Outbound)
|
# 2.5 注册出库模块 (Outbound)
|
||||||
@ -119,6 +117,19 @@ def create_app():
|
|||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
print(f"❌ 错误: BOM 模块导入失败: {e}")
|
print(f"❌ 错误: BOM 模块导入失败: {e}")
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 2.7 注册权限管理模块 (Permission) - [新增]
|
||||||
|
# -----------------------------------------------------
|
||||||
|
try:
|
||||||
|
from app.api.v1.permission import permission_bp
|
||||||
|
# 标准: /api/v1/permissions/tree
|
||||||
|
app.register_blueprint(permission_bp, url_prefix='/api/v1/permissions')
|
||||||
|
# 兼容: /api/permissions/tree
|
||||||
|
app.register_blueprint(permission_bp, url_prefix='/api/permissions', name='permission_legacy')
|
||||||
|
print("✅ Permission 模块注册成功")
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"❌ 错误: Permission 模块导入失败 (请检查 app/api/v1/permission.py 是否存在): {e}")
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# 3. 预加载数据模型
|
# 3. 预加载数据模型
|
||||||
# =========================================================
|
# =========================================================
|
||||||
@ -133,8 +144,8 @@ def create_app():
|
|||||||
# 出库模型
|
# 出库模型
|
||||||
from app.models.outbound import TransOutbound
|
from app.models.outbound import TransOutbound
|
||||||
|
|
||||||
# 系统与业务模型
|
# 系统与业务模型 (SysRolePermission 等在 models.system 中)
|
||||||
from app.models.system import SysUser, SysLog
|
from app.models.system import SysUser, SysLog, SysMenu, SysElement, SysRolePermission
|
||||||
# 确保借还模型被加载
|
# 确保借还模型被加载
|
||||||
from app.models.transaction import TransBorrow, TransRepair, TransScrap
|
from app.models.transaction import TransBorrow, TransRepair, TransScrap
|
||||||
|
|
||||||
|
|||||||
@ -88,3 +88,20 @@ def delete_user(user_id):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"Delete User Failed: {str(e)}")
|
current_app.logger.error(f"Delete User Failed: {str(e)}")
|
||||||
return jsonify({'msg': str(e)}), 400
|
return jsonify({'msg': str(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route('/my-permissions', methods=['GET'])
|
||||||
|
@jwt_required()
|
||||||
|
def get_my_permissions():
|
||||||
|
"""获取当前登录用户的权限列表"""
|
||||||
|
try:
|
||||||
|
claims = get_jwt()
|
||||||
|
role = claims.get('role')
|
||||||
|
|
||||||
|
# 调用 Service 获取权限
|
||||||
|
permissions = AuthService.get_user_permissions(role)
|
||||||
|
|
||||||
|
return jsonify({'msg': '获取成功', 'data': permissions}), 200
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Get Permissions Failed: {str(e)}")
|
||||||
|
return jsonify({'msg': '获取权限失败'}), 500
|
||||||
48
inventory-backend/app/api/v1/permission.py
Normal file
48
inventory-backend/app/api/v1/permission.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# inventory-backend/app/api/v1/permission.py
|
||||||
|
from flask import Blueprint, request, jsonify, current_app
|
||||||
|
from flask_jwt_extended import jwt_required
|
||||||
|
from app.services.permission_service import PermissionService
|
||||||
|
|
||||||
|
permission_bp = Blueprint('permission', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@permission_bp.route('/tree', methods=['GET'])
|
||||||
|
@jwt_required()
|
||||||
|
def get_tree():
|
||||||
|
"""获取权限树"""
|
||||||
|
try:
|
||||||
|
data = PermissionService.get_permission_tree()
|
||||||
|
return jsonify({'code': 200, 'msg': '获取成功', 'data': data}), 200
|
||||||
|
except Exception as e:
|
||||||
|
# 打印详细错误到控制台,方便调试
|
||||||
|
current_app.logger.error(f"Get Tree Failed: {str(e)}")
|
||||||
|
# 返回 500 时带上错误信息
|
||||||
|
return jsonify({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@permission_bp.route('/role/<string:role_code>', methods=['GET'])
|
||||||
|
@jwt_required()
|
||||||
|
def get_role_perms(role_code):
|
||||||
|
"""获取某个角色的权限列表"""
|
||||||
|
try:
|
||||||
|
data = PermissionService.get_role_permissions(role_code)
|
||||||
|
return jsonify({'code': 200, 'msg': '获取成功', 'data': data}), 200
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Get Role Perms Failed: {str(e)}")
|
||||||
|
return jsonify({'code': 500, 'msg': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@permission_bp.route('/assign', methods=['POST'])
|
||||||
|
@jwt_required()
|
||||||
|
def assign_perms():
|
||||||
|
"""保存权限分配"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
role_code = data.get('role_code')
|
||||||
|
permissions = data.get('permissions', []) # list of codes
|
||||||
|
|
||||||
|
PermissionService.assign_permissions(role_code, permissions)
|
||||||
|
return jsonify({'code': 200, 'msg': '保存成功'}), 200
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Assign Perms Failed: {str(e)}")
|
||||||
|
return jsonify({'code': 500, 'msg': str(e)}), 500
|
||||||
@ -1,14 +1,17 @@
|
|||||||
# app/models/system.py
|
# inventory-backend/app/models/system.py
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 1. 系统用户表
|
||||||
|
# ==========================================
|
||||||
class SysUser(db.Model):
|
class SysUser(db.Model):
|
||||||
"""
|
"""
|
||||||
系统用户表
|
系统用户表
|
||||||
对应数据库: sys_user
|
对应数据库: sys_user
|
||||||
username 字段存储格式约定: "真实姓名/登录账号" (例如: 张三/zhangsan)
|
username 字段存储格式约定: "真实姓名/登录账号" (例如: 张三/zhangsan01)
|
||||||
"""
|
"""
|
||||||
__tablename__ = 'sys_user'
|
__tablename__ = 'sys_user'
|
||||||
|
|
||||||
@ -19,8 +22,7 @@ class SysUser(db.Model):
|
|||||||
role = db.Column(db.String(50))
|
role = db.Column(db.String(50))
|
||||||
status = db.Column(db.String(20), default='active')
|
status = db.Column(db.String(20), default='active')
|
||||||
password_hash = db.Column(db.Text)
|
password_hash = db.Column(db.Text)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||||
# created_at 已在数据库脚本中移除,此处不再定义
|
|
||||||
|
|
||||||
def set_password(self, password):
|
def set_password(self, password):
|
||||||
"""生成加密密码"""
|
"""生成加密密码"""
|
||||||
@ -45,23 +47,27 @@ class SysUser(db.Model):
|
|||||||
parts = raw_name.split('/')
|
parts = raw_name.split('/')
|
||||||
real_name = parts[0]
|
real_name = parts[0]
|
||||||
acc_id = parts[1]
|
acc_id = parts[1]
|
||||||
# 格式化为前端展示格式: 张三(zhangsan)
|
# 格式化为前端展示格式: 张三(zhangsan01)
|
||||||
display_name = f"{real_name}({acc_id})"
|
display_name = f"{real_name}({acc_id})"
|
||||||
# 单独提取账号ID (如果前端需要单独用)
|
# 单独提取账号ID (如果前端需要单独用)
|
||||||
account_id = acc_id
|
account_id = acc_id
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': self.id,
|
'id': self.id,
|
||||||
'username': display_name, # 列表显示: 张三(zhangsan)
|
'username': display_name, # 列表显示: 张三(zhangsan01)
|
||||||
'raw_username': self.username, # 原始数据
|
'raw_username': self.username, # 原始数据
|
||||||
'account_id': account_id, # 纯账号ID: zhangsan
|
'account_id': account_id, # 纯账号ID: zhangsan01
|
||||||
'email': self.email,
|
'email': self.email,
|
||||||
'department': self.department,
|
'department': self.department,
|
||||||
'role': self.role,
|
'role': self.role,
|
||||||
'status': self.status
|
'status': self.status,
|
||||||
|
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 2. 系统日志表
|
||||||
|
# ==========================================
|
||||||
class SysLog(db.Model):
|
class SysLog(db.Model):
|
||||||
"""
|
"""
|
||||||
系统操作日志表
|
系统操作日志表
|
||||||
@ -89,3 +95,57 @@ class SysLog(db.Model):
|
|||||||
'action_type': self.action_type,
|
'action_type': self.action_type,
|
||||||
'description': self.description
|
'description': self.description
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 3. 权限管理模型 (RBAC) - [新增]
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
class SysMenu(db.Model):
|
||||||
|
"""系统菜单/页面表"""
|
||||||
|
__tablename__ = 'sys_menu'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
parent_id = db.Column(db.Integer, default=0)
|
||||||
|
name = db.Column(db.String(50), nullable=False)
|
||||||
|
code = db.Column(db.String(100), unique=True, nullable=False)
|
||||||
|
path = db.Column(db.String(200))
|
||||||
|
sort_order = db.Column(db.Integer, default=0)
|
||||||
|
is_visible = db.Column(db.Boolean, default=True)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'name': self.name,
|
||||||
|
'code': self.code,
|
||||||
|
'path': self.path,
|
||||||
|
'type': 'menu' # 前端树形控件图标判断用
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SysElement(db.Model):
|
||||||
|
"""页面元素/列定义表"""
|
||||||
|
__tablename__ = 'sys_element'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
menu_code = db.Column(db.String(100), db.ForeignKey('sys_menu.code'))
|
||||||
|
name = db.Column(db.String(100), nullable=False)
|
||||||
|
code = db.Column(db.String(100), nullable=False) # 如: unit_price
|
||||||
|
element_type = db.Column(db.String(20), default='column')
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'name': self.name,
|
||||||
|
'code': self.code,
|
||||||
|
'menu_code': self.menu_code,
|
||||||
|
'type': 'element',
|
||||||
|
'element_type': self.element_type
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SysRolePermission(db.Model):
|
||||||
|
"""角色权限关联表"""
|
||||||
|
__tablename__ = 'sys_role_permission'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
role_code = db.Column(db.String(50), nullable=False)
|
||||||
|
target_code = db.Column(db.String(100), nullable=False) # menu_code 或 element_code
|
||||||
|
type = db.Column(db.String(20), nullable=False) # 'menu' 或 'element'
|
||||||
@ -1,11 +1,10 @@
|
|||||||
# app/services/auth_service.py
|
# app/services/auth_service.py
|
||||||
from app.models.system import SysUser
|
from app.models.system import SysUser, SysRolePermission # <== 引入 SysRolePermission
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from flask_jwt_extended import create_access_token
|
from flask_jwt_extended import create_access_token
|
||||||
from app.utils.constants import UserRole
|
from app.utils.constants import UserRole
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|
||||||
class AuthService:
|
class AuthService:
|
||||||
# 硬编码的超级管理员凭证
|
# 硬编码的超级管理员凭证
|
||||||
SUPER_ADMIN_USER = "IRIS"
|
SUPER_ADMIN_USER = "IRIS"
|
||||||
@ -212,3 +211,36 @@ class AuthService:
|
|||||||
db.session.delete(user)
|
db.session.delete(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_user_permissions(role_code):
|
||||||
|
"""
|
||||||
|
获取指定角色的所有权限代码列表
|
||||||
|
返回格式: {
|
||||||
|
'menus': ['inbound_buy', 'system_user'],
|
||||||
|
'elements': ['inbound_buy:unit_price', ...]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
# 1. 查菜单权限
|
||||||
|
menu_perms = SysRolePermission.query.filter_by(
|
||||||
|
role_code=role_code,
|
||||||
|
type='menu'
|
||||||
|
).all()
|
||||||
|
menu_codes = [p.target_code for p in menu_perms]
|
||||||
|
|
||||||
|
# 2. 查元素(列)权限
|
||||||
|
# 注意:这里我们只返回用户拥有的。前端逻辑是:"如果列配置了Key且用户没这个Key,则隐藏"
|
||||||
|
element_perms = SysRolePermission.query.filter_by(
|
||||||
|
role_code=role_code,
|
||||||
|
type='element'
|
||||||
|
).all()
|
||||||
|
|
||||||
|
# 这里的 target_code 就是列的 code (如 unit_price)
|
||||||
|
# 为了防止不同页面有相同列名导致的混淆,我们之前数据库设计是做了隔离的
|
||||||
|
# 但为了前端处理方便,我们直接返回列的 code 集合
|
||||||
|
element_codes = [p.target_code for p in element_perms]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'menus': menu_codes,
|
||||||
|
'elements': element_codes
|
||||||
|
}
|
||||||
89
inventory-backend/app/services/permission_service.py
Normal file
89
inventory-backend/app/services/permission_service.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
# inventory-backend/app/services/permission_service.py
|
||||||
|
from app.models.system import SysMenu, SysElement, SysRolePermission
|
||||||
|
from app.extensions import db
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionService:
|
||||||
|
@staticmethod
|
||||||
|
def get_permission_tree():
|
||||||
|
"""
|
||||||
|
获取完整的权限树(菜单 -> 元素)
|
||||||
|
供前端权限配置页面展示
|
||||||
|
"""
|
||||||
|
# 1. 获取所有菜单
|
||||||
|
menus = SysMenu.query.order_by(SysMenu.sort_order).all()
|
||||||
|
# 2. 获取所有元素
|
||||||
|
elements = SysElement.query.all()
|
||||||
|
|
||||||
|
# 3. 组装树结构
|
||||||
|
tree_data = []
|
||||||
|
for menu in menus:
|
||||||
|
menu_dict = menu.to_dict()
|
||||||
|
|
||||||
|
# 找该菜单下的所有元素
|
||||||
|
children = []
|
||||||
|
for el in elements:
|
||||||
|
if el.menu_code == menu.code:
|
||||||
|
children.append(el.to_dict())
|
||||||
|
|
||||||
|
# 如果有子元素,加到 children
|
||||||
|
if children:
|
||||||
|
menu_dict['children'] = children
|
||||||
|
|
||||||
|
tree_data.append(menu_dict)
|
||||||
|
|
||||||
|
return tree_data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_role_permissions(role_code):
|
||||||
|
"""获取指定角色拥有的所有权限Code"""
|
||||||
|
perms = SysRolePermission.query.filter_by(role_code=role_code).all()
|
||||||
|
|
||||||
|
# 将结果分为 menus 和 elements (虽然前端目前合并处理,但分开更清晰)
|
||||||
|
menu_codes = []
|
||||||
|
element_codes = []
|
||||||
|
|
||||||
|
for p in perms:
|
||||||
|
if p.type == 'menu':
|
||||||
|
menu_codes.append(p.target_code)
|
||||||
|
else:
|
||||||
|
element_codes.append(p.target_code)
|
||||||
|
|
||||||
|
# 返回结构适配前端
|
||||||
|
return {
|
||||||
|
'menus': menu_codes,
|
||||||
|
'elements': element_codes
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def assign_permissions(role_code, permission_codes):
|
||||||
|
"""
|
||||||
|
保存角色的权限
|
||||||
|
permission_codes: 前端传来的 list,包含 menu_code 和 element_code
|
||||||
|
"""
|
||||||
|
if not role_code:
|
||||||
|
raise ValueError("角色代码不能为空")
|
||||||
|
|
||||||
|
# 1. 删除该角色旧的所有权限
|
||||||
|
SysRolePermission.query.filter_by(role_code=role_code).delete()
|
||||||
|
|
||||||
|
# 2. 批量添加新权限
|
||||||
|
if permission_codes and len(permission_codes) > 0:
|
||||||
|
# 预先获取所有菜单代码,用于判断类型
|
||||||
|
all_menu_codes = {m.code for m in SysMenu.query.all()}
|
||||||
|
|
||||||
|
new_records = []
|
||||||
|
for code in permission_codes:
|
||||||
|
# 简单判断:如果在菜单表里有,就是 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
|
||||||
|
))
|
||||||
|
|
||||||
|
db.session.add_all(new_records)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
26
inventory-web/src/api/system/permission.ts
Normal file
26
inventory-web/src/api/system/permission.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 获取所有可用的权限树(菜单+列)
|
||||||
|
export function getAllPermissionTree() {
|
||||||
|
return request({
|
||||||
|
url: '/v1/permissions/tree',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取某个角色已拥有的权限列表
|
||||||
|
export function getRolePermissions(roleCode: string) {
|
||||||
|
return request({
|
||||||
|
url: '/v1/permissions/role/' + roleCode,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存角色的权限配置
|
||||||
|
export function saveRolePermissions(data: any) {
|
||||||
|
return request({
|
||||||
|
url: '/v1/permissions/assign',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
134
inventory-web/src/components/BaseTable/index.vue
Normal file
134
inventory-web/src/components/BaseTable/index.vue
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
<template>
|
||||||
|
<div class="base-table">
|
||||||
|
<el-table
|
||||||
|
v-bind="$attrs"
|
||||||
|
:data="data"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
v-loading="loading"
|
||||||
|
header-cell-class-name="table-header-gray"
|
||||||
|
>
|
||||||
|
<template v-for="col in visibleColumns" :key="col.prop">
|
||||||
|
|
||||||
|
<el-table-column
|
||||||
|
v-if="!col.slot"
|
||||||
|
v-bind="col"
|
||||||
|
:prop="col.prop"
|
||||||
|
:label="col.label"
|
||||||
|
:min-width="col.minWidth || '120'"
|
||||||
|
:width="col.width"
|
||||||
|
:fixed="col.fixed"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-table-column
|
||||||
|
v-else
|
||||||
|
v-bind="col"
|
||||||
|
:prop="col.prop"
|
||||||
|
:label="col.label"
|
||||||
|
:min-width="col.minWidth || '120'"
|
||||||
|
:width="col.width"
|
||||||
|
:fixed="col.fixed"
|
||||||
|
>
|
||||||
|
<template #default="scope">
|
||||||
|
<slot :name="col.prop" :row="scope.row" :index="scope.$index"></slot>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #empty>
|
||||||
|
<el-empty description="暂无数据" />
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div v-if="showPagination" class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-bind="paginationConfig"
|
||||||
|
v-model:current-page="localPage"
|
||||||
|
v-model:page-size="localLimit"
|
||||||
|
:total="total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { usePermissionStore } from '@/stores/permission'
|
||||||
|
|
||||||
|
// --- Props 定义 ---
|
||||||
|
const props = defineProps({
|
||||||
|
// 数据源
|
||||||
|
data: { type: Array, default: () => [] },
|
||||||
|
// 列配置 (核心)
|
||||||
|
columns: { type: Array as () => any[], required: true },
|
||||||
|
// 页面编码 (用于权限隔离,如果列名全局唯一可不传,但建议传)
|
||||||
|
pageCode: { type: String, default: '' },
|
||||||
|
|
||||||
|
loading: { type: Boolean, default: false },
|
||||||
|
total: { type: Number, default: 0 },
|
||||||
|
showPagination: { type: Boolean, default: true },
|
||||||
|
page: { type: Number, default: 1 },
|
||||||
|
limit: { type: Number, default: 10 }
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:page', 'update:limit', 'pagination'])
|
||||||
|
|
||||||
|
const permStore = usePermissionStore()
|
||||||
|
|
||||||
|
// --- 核心逻辑:计算当前可见的列 ---
|
||||||
|
const visibleColumns = computed(() => {
|
||||||
|
return props.columns.filter(col => {
|
||||||
|
// 1. 获取该列在数据库中对应的 code
|
||||||
|
// 如果列配置里显式写了 code,就用写的;如果没有,默认认为 prop 就是 code
|
||||||
|
const permissionKey = col.code || col.prop
|
||||||
|
|
||||||
|
// 2. 如果这个列不需要权限控制 (比如序号 index),可以在配置里加个 ignoreAuth: true
|
||||||
|
if (col.ignoreAuth) return true
|
||||||
|
|
||||||
|
// 3. 问 Store:我有这个权限吗?
|
||||||
|
// 注意:我们在 PermissionStore 里存的是全局唯一的 code
|
||||||
|
return permStore.hasColumnPermission(props.pageCode, permissionKey)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- 分页逻辑处理 ---
|
||||||
|
const localPage = ref(props.page)
|
||||||
|
const localLimit = ref(props.limit)
|
||||||
|
|
||||||
|
watch(() => props.page, (val) => localPage.value = val)
|
||||||
|
watch(() => props.limit, (val) => localLimit.value = val)
|
||||||
|
|
||||||
|
const handleSizeChange = (val: number) => {
|
||||||
|
emit('update:limit', val)
|
||||||
|
emit('pagination', { page: localPage.value, limit: val })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (val: number) => {
|
||||||
|
emit('update:page', val)
|
||||||
|
emit('pagination', { page: val, limit: localLimit.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const paginationConfig = {
|
||||||
|
pageSizes: [10, 20, 50, 100],
|
||||||
|
background: true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 15px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
:deep(.table-header-gray th) {
|
||||||
|
background-color: #f8f9fb !important;
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 600;
|
||||||
|
height: 45px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -195,9 +195,19 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
meta: {
|
meta: {
|
||||||
title: '账号开通',
|
title: '账号开通',
|
||||||
icon: 'User',
|
icon: 'User',
|
||||||
// 子路由也建议加上权限限制
|
|
||||||
roles: ['SUPER_ADMIN', 'SUPERVISOR']
|
roles: ['SUPER_ADMIN', 'SUPERVISOR']
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
// [新增] 权限分配页面,只有超级管理员可进
|
||||||
|
{
|
||||||
|
path: 'permission',
|
||||||
|
name: 'PermissionConfig',
|
||||||
|
component: () => import('@/views/system/PermissionConfig.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '权限分配',
|
||||||
|
icon: 'Lock',
|
||||||
|
roles: ['SUPER_ADMIN']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -224,11 +234,10 @@ router.beforeEach((to, from, next) => {
|
|||||||
const token = userStore.token || localStorage.getItem('token')
|
const token = userStore.token || localStorage.getItem('token')
|
||||||
|
|
||||||
// [修复] 优先从 user 对象获取,并统一转大写,防止大小写不一致导致权限失效
|
// [修复] 优先从 user 对象获取,并统一转大写,防止大小写不一致导致权限失效
|
||||||
// 注意:Store 中存储的可能是 user.role 或者直接是 role,根据你之前的 store 结构适配
|
|
||||||
const rawRole = userStore.user?.role || userStore.role || localStorage.getItem('role') || 'user'
|
const rawRole = userStore.user?.role || userStore.role || localStorage.getItem('role') || 'user'
|
||||||
const userRole = String(rawRole).toUpperCase()
|
const userRole = String(rawRole).toUpperCase()
|
||||||
|
|
||||||
// 调试日志:如果跳转有问题,请按 F12 查看控制台输出
|
// 调试日志
|
||||||
if (to.path.includes('/system')) {
|
if (to.path.includes('/system')) {
|
||||||
console.log(`路由守卫检查: Path=${to.path}, UserRole=${userRole}, Required=${to.meta.roles}`)
|
console.log(`路由守卫检查: Path=${to.path}, UserRole=${userRole}, Required=${to.meta.roles}`)
|
||||||
}
|
}
|
||||||
@ -249,7 +258,6 @@ router.beforeEach((to, from, next) => {
|
|||||||
|
|
||||||
// 权限检查逻辑
|
// 权限检查逻辑
|
||||||
if (to.meta.roles && Array.isArray(to.meta.roles)) {
|
if (to.meta.roles && Array.isArray(to.meta.roles)) {
|
||||||
// [修复] to.meta.roles 里已经是大写了,userRole 也转大写了,现在可以安全比对
|
|
||||||
if (to.meta.roles.includes(userRole)) {
|
if (to.meta.roles.includes(userRole)) {
|
||||||
next()
|
next()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
49
inventory-web/src/stores/permission.ts
Normal file
49
inventory-web/src/stores/permission.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export const usePermissionStore = defineStore('permission', () => {
|
||||||
|
// 存储我能看到的页面代码 (如 ['inbound_buy', ...])
|
||||||
|
const menuPermissions = ref<string[]>([])
|
||||||
|
|
||||||
|
// 存储我能看到的列代码 (如 ['unit_price', 'sale_price'])
|
||||||
|
const elementPermissions = ref<string[]>([])
|
||||||
|
|
||||||
|
// 初始化加载权限 (登录后调用)
|
||||||
|
const loadPermissions = async () => {
|
||||||
|
try {
|
||||||
|
const res: any = await request({
|
||||||
|
url: '/v1/auth/my-permissions',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
menuPermissions.value = res.data.menus || []
|
||||||
|
elementPermissions.value = res.data.elements || []
|
||||||
|
console.log('权限字典加载完成:', elementPermissions.value.length, '个列权限')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载权限失败', e)
|
||||||
|
// 失败时清空,防止残留
|
||||||
|
menuPermissions.value = []
|
||||||
|
elementPermissions.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ 核心判断函数:判断当前用户是否拥有某个列/按钮的权限
|
||||||
|
// page: 页面代码 (预留字段,目前全局唯一code,暂不使用page隔离)
|
||||||
|
// code: 权限标识 (如 'unit_price')
|
||||||
|
const hasColumnPermission = (page: string, code: string) => {
|
||||||
|
// 1. 如果列没有配置 permissionKey,说明是公开列,直接放行
|
||||||
|
if (!code) return true
|
||||||
|
|
||||||
|
// 2. 检查权限池里是否有这个 code
|
||||||
|
return elementPermissions.value.includes(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
menuPermissions,
|
||||||
|
elementPermissions,
|
||||||
|
loadPermissions,
|
||||||
|
hasColumnPermission
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -52,11 +52,13 @@
|
|||||||
import { ref, reactive } from 'vue'
|
import { ref, reactive } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { ElMessageBox } from 'element-plus' // 引入 ElMessageBox
|
import { usePermissionStore } from '@/stores/permission' // [新增] 引入权限Store
|
||||||
|
import { ElMessageBox } from 'element-plus'
|
||||||
import { User, Lock } from '@element-plus/icons-vue'
|
import { User, Lock } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const permissionStore = usePermissionStore() // [新增]
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const loginFormRef = ref()
|
const loginFormRef = ref()
|
||||||
|
|
||||||
@ -74,23 +76,25 @@ const onLogin = async () => {
|
|||||||
if (valid) {
|
if (valid) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
// 执行登录请求
|
// 1. 执行登录请求
|
||||||
const success = await userStore.handleLogin(loginForm)
|
const success = await userStore.handleLogin(loginForm)
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
// 成功:跳转
|
// [新增] 2. 登录成功后,立即拉取当前用户的权限字典
|
||||||
|
// 这样进入 Dashboard 时,所有按钮/列的显示状态就已经确定了
|
||||||
|
await permissionStore.loadPermissions()
|
||||||
|
|
||||||
|
// 3. 跳转
|
||||||
router.push('/dashboard')
|
router.push('/dashboard')
|
||||||
} else {
|
} else {
|
||||||
// 失败(业务逻辑拒绝,如账号密码错):弹出模态框
|
// 失败(业务逻辑拒绝):弹出模态框
|
||||||
showLoginFailAlert('用户名或密码错误')
|
showLoginFailAlert('用户名或密码错误')
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// 失败(系统错误,如网络断开/500报错):弹出模态框
|
// 失败(系统错误):弹出模态框
|
||||||
// 优先取后端的报错信息,没有则显示默认
|
|
||||||
const msg = error.response?.data?.msg || error.message || '登录遇到未知错误'
|
const msg = error.response?.data?.msg || error.message || '登录遇到未知错误'
|
||||||
showLoginFailAlert(msg)
|
showLoginFailAlert(msg)
|
||||||
} finally {
|
} finally {
|
||||||
// 停止转圈,让用户可以看清弹窗
|
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -103,8 +107,6 @@ const showLoginFailAlert = (msg: string) => {
|
|||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
type: 'error',
|
type: 'error',
|
||||||
callback: () => {
|
callback: () => {
|
||||||
// 点击确定后,清空密码框,让用户重试
|
|
||||||
// 页面绝对不会刷新,光标还在
|
|
||||||
loginForm.password = ''
|
loginForm.password = ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
627
inventory-web/src/views/system/PermissionConfig.vue
Normal file
627
inventory-web/src/views/system/PermissionConfig.vue
Normal file
@ -0,0 +1,627 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-card class="permission-card" shadow="never">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<div class="title-block">
|
||||||
|
<span class="main-title">权限配置中心</span>
|
||||||
|
<span class="sub-title">设定各角色的系统访问级别与数据可见性</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="saving"
|
||||||
|
@click="handleSave"
|
||||||
|
:disabled="!currentRole"
|
||||||
|
size="large"
|
||||||
|
icon="Check"
|
||||||
|
class="save-btn"
|
||||||
|
>
|
||||||
|
保存配置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="main-layout">
|
||||||
|
<div class="left-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<el-icon><User /></el-icon> 角色列表
|
||||||
|
</div>
|
||||||
|
<el-scrollbar>
|
||||||
|
<ul class="role-list">
|
||||||
|
<li
|
||||||
|
v-for="role in roleList"
|
||||||
|
:key="role.value"
|
||||||
|
:class="['role-item', { active: currentRole === role.value }]"
|
||||||
|
@click="handleRoleSelect(role.value)"
|
||||||
|
>
|
||||||
|
<div class="role-icon">
|
||||||
|
<el-icon v-if="role.value === 'SUPER_ADMIN'"><Avatar /></el-icon>
|
||||||
|
<el-icon v-else><UserFilled /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="role-info">
|
||||||
|
<span class="role-name">{{ role.label }}</span>
|
||||||
|
<span class="role-code">{{ role.value }}</span>
|
||||||
|
</div>
|
||||||
|
<el-icon v-if="currentRole === role.value" class="arrow-icon"><ArrowRight /></el-icon>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right-panel" v-loading="loading">
|
||||||
|
<div v-if="!currentRole" class="empty-state">
|
||||||
|
<img src="https://gw.alipayobjects.com/zos/antfincdn/ZHrcdLPrvN/empty.svg" alt="empty" style="width: 200px; opacity: 0.5;" />
|
||||||
|
<p>请在左侧选择一个角色开始配置</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="config-content">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>权限明细</span>
|
||||||
|
<el-tag type="warning" effect="plain" round>当前配置: {{ getRoleLabel(currentRole) }}</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
:data="tableData"
|
||||||
|
row-key="id"
|
||||||
|
border
|
||||||
|
default-expand-all
|
||||||
|
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||||
|
class="permission-table"
|
||||||
|
>
|
||||||
|
<el-table-column prop="name" label="页面 / 模块" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span style="font-weight: 500;">{{ row.name }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="访问权限" width="150" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-checkbox
|
||||||
|
v-model="row.hasRead"
|
||||||
|
@change="(val) => handleReadChange(val, row)"
|
||||||
|
class="custom-checkbox"
|
||||||
|
>
|
||||||
|
<span :class="{ 'text-active': row.hasRead }">可见 (Read)</span>
|
||||||
|
</el-checkbox>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作权限" width="180" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div v-if="row.operationCode">
|
||||||
|
<el-checkbox
|
||||||
|
v-model="row.hasWrite"
|
||||||
|
:disabled="!row.hasRead"
|
||||||
|
@change="(val) => handleWriteChange(val, row)"
|
||||||
|
class="custom-checkbox"
|
||||||
|
>
|
||||||
|
<span :class="{ 'text-active': row.hasWrite, 'text-disabled': !row.hasRead }">可编辑 (Write)</span>
|
||||||
|
</el-checkbox>
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-gray">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="字段级控制 (敏感列)" min-width="300">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div v-if="row.elements && row.elements.length > 0">
|
||||||
|
<el-popover placement="left" :width="300" trigger="click">
|
||||||
|
<template #reference>
|
||||||
|
<el-button link type="primary" :disabled="!row.hasRead">
|
||||||
|
<el-icon style="margin-right: 4px"><Setting /></el-icon>
|
||||||
|
配置 {{ getCheckedCount(row) }}/{{ row.elements.length }} 个字段
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="field-config-box">
|
||||||
|
<div class="field-header">
|
||||||
|
<span>勾选可见字段</span>
|
||||||
|
<el-checkbox
|
||||||
|
v-model="row.checkAllElements"
|
||||||
|
:indeterminate="row.isIndeterminate"
|
||||||
|
@change="(val) => handleCheckAllElements(val, row)"
|
||||||
|
size="small"
|
||||||
|
>全选</el-checkbox>
|
||||||
|
</div>
|
||||||
|
<el-checkbox-group v-model="row.checkedElements" @change="(val) => handleCheckedElementsChange(val, row)">
|
||||||
|
<el-row :gutter="10">
|
||||||
|
<el-col :span="12" v-for="col in row.elements" :key="col.code">
|
||||||
|
<el-checkbox :label="col.code" :title="col.name" style="width: 100%; overflow: hidden; text-overflow: ellipsis;">
|
||||||
|
{{ col.name }}
|
||||||
|
</el-checkbox>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-checkbox-group>
|
||||||
|
</div>
|
||||||
|
</el-popover>
|
||||||
|
|
||||||
|
<span class="preview-tags">
|
||||||
|
<span v-for="code in row.checkedElements.slice(0, 2)" :key="code" class="mini-tag">{{ getElementName(row, code) }}</span>
|
||||||
|
<span v-if="row.checkedElements.length > 2" class="mini-tag">...</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { User, UserFilled, ArrowRight, Setting, Check, Avatar } from '@element-plus/icons-vue'
|
||||||
|
import { getAllPermissionTree, getRolePermissions, saveRolePermissions } from '@/api/system/permission'
|
||||||
|
|
||||||
|
// --- 类型定义 ---
|
||||||
|
interface PermissionNode {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
type: string
|
||||||
|
children?: PermissionNode[]
|
||||||
|
|
||||||
|
// 前端辅助字段
|
||||||
|
hasRead: boolean // 是否勾选了页面
|
||||||
|
hasWrite: boolean // 是否勾选了 operation
|
||||||
|
operationCode: string | null // 存储 operation 的 code
|
||||||
|
elements: any[] // 存储该页面的普通列
|
||||||
|
checkedElements: string[] // 已勾选的列code
|
||||||
|
checkAllElements: boolean
|
||||||
|
isIndeterminate: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 状态定义 ---
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const currentRole = ref('')
|
||||||
|
const roleList = [
|
||||||
|
{ label: '超级管理员', value: 'SUPER_ADMIN' },
|
||||||
|
{ label: '主管', value: 'SUPERVISOR' },
|
||||||
|
{ label: '财务', value: 'FINANCE' },
|
||||||
|
{ label: '库管', value: 'WAREHOUSE_MGR' },
|
||||||
|
{ label: '入库员', value: 'INBOUND' },
|
||||||
|
{ label: '出库员', value: 'OUTBOUND' },
|
||||||
|
{ label: '采购员', value: 'PURCHASER' },
|
||||||
|
{ label: '销售', value: 'SALES' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 原始树数据
|
||||||
|
const rawTreeData = ref<any[]>([])
|
||||||
|
// 表格展示数据 (经过处理)
|
||||||
|
const tableData = ref<PermissionNode[]>([])
|
||||||
|
|
||||||
|
// --- 方法 ---
|
||||||
|
|
||||||
|
// 1. 初始化:获取权限树结构
|
||||||
|
const fetchTree = async () => {
|
||||||
|
try {
|
||||||
|
const res: any = await getAllPermissionTree()
|
||||||
|
if (res.code === 200) {
|
||||||
|
rawTreeData.value = res.data
|
||||||
|
// 初始化表格结构(此时没有勾选状态)
|
||||||
|
tableData.value = transformData(res.data)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('加载权限配置失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ★ 核心:将后端嵌套的 Tree 数据转换为适合表格展示的结构
|
||||||
|
// 分离 "页面"、"操作列" 和 "普通列"
|
||||||
|
const transformData = (nodes: any[]): PermissionNode[] => {
|
||||||
|
return nodes.map(node => {
|
||||||
|
// 找出所有子节点中的 element
|
||||||
|
const allChildren = node.children || []
|
||||||
|
|
||||||
|
// 1. 找操作列 (作为 Write 权限)
|
||||||
|
const opNode = allChildren.find((c: any) => c.type === 'element' && (c.code === 'operation' || c.code.endsWith('_op')))
|
||||||
|
const operationCode = opNode ? opNode.code : null
|
||||||
|
|
||||||
|
// 2. 找普通列 (作为字段权限)
|
||||||
|
const columns = allChildren.filter((c: any) => c.type === 'element' && c.code !== 'operation' && !c.code.endsWith('_op'))
|
||||||
|
|
||||||
|
// 3. 找子菜单 (如果有)
|
||||||
|
const subMenus = allChildren.filter((c: any) => c.type === 'menu')
|
||||||
|
const childrenTransformed = subMenus.length > 0 ? transformData(subMenus) : undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: node.id,
|
||||||
|
name: node.name,
|
||||||
|
code: node.code,
|
||||||
|
type: node.type,
|
||||||
|
children: childrenTransformed,
|
||||||
|
|
||||||
|
// 状态字段初始化
|
||||||
|
hasRead: false,
|
||||||
|
hasWrite: false,
|
||||||
|
operationCode: operationCode,
|
||||||
|
elements: columns,
|
||||||
|
checkedElements: [],
|
||||||
|
checkAllElements: false,
|
||||||
|
isIndeterminate: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 切换角色:回显权限
|
||||||
|
const handleRoleSelect = async (roleCode: string) => {
|
||||||
|
currentRole.value = roleCode
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取该角色拥有的所有 code
|
||||||
|
const res: any = await getRolePermissions(roleCode)
|
||||||
|
if (res.code === 200) {
|
||||||
|
const perms = new Set([...(res.data.menus || []), ...(res.data.elements || [])])
|
||||||
|
// 递归设置表格每一行的状态
|
||||||
|
setRowStatus(tableData.value, perms)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('获取角色权限失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归回显状态
|
||||||
|
const setRowStatus = (rows: PermissionNode[], perms: Set<any>) => {
|
||||||
|
rows.forEach(row => {
|
||||||
|
// 1. 设置可读 (页面Code是否存在)
|
||||||
|
row.hasRead = perms.has(row.code)
|
||||||
|
|
||||||
|
// 2. 设置可编辑 (操作列Code是否存在)
|
||||||
|
if (row.operationCode) {
|
||||||
|
row.hasWrite = perms.has(row.operationCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 设置已选字段
|
||||||
|
if (row.elements && row.elements.length > 0) {
|
||||||
|
row.checkedElements = row.elements
|
||||||
|
.filter(el => perms.has(el.code))
|
||||||
|
.map(el => el.code)
|
||||||
|
|
||||||
|
updateCheckAllStatus(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 递归子菜单
|
||||||
|
if (row.children) {
|
||||||
|
setRowStatus(row.children, perms)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 交互逻辑 ---
|
||||||
|
|
||||||
|
// 当“可读”改变
|
||||||
|
const handleReadChange = (val: boolean, row: PermissionNode) => {
|
||||||
|
// 如果关闭可读,强制关闭可编辑,并清空字段选择
|
||||||
|
if (!val) {
|
||||||
|
row.hasWrite = false
|
||||||
|
// row.checkedElements = [] // 可选:是否同时也清空字段勾选?通常建议保留,方便误触恢复,但这里为了逻辑严谨先不清空,只在保存时过滤
|
||||||
|
} else {
|
||||||
|
// 如果开启可读,默认全选字段 (提升体验)
|
||||||
|
// row.checkedElements = row.elements.map(e => e.code)
|
||||||
|
// updateCheckAllStatus(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 联动子菜单:如果父级关闭,子级是否关闭?通常不强制,但可以做
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当“可编辑”改变
|
||||||
|
const handleWriteChange = (val: boolean, row: PermissionNode) => {
|
||||||
|
// 如果开启编辑,必须开启可读
|
||||||
|
if (val && !row.hasRead) {
|
||||||
|
row.hasRead = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字段全选逻辑
|
||||||
|
const handleCheckAllElements = (val: boolean, row: PermissionNode) => {
|
||||||
|
row.checkedElements = val ? row.elements.map(e => e.code) : []
|
||||||
|
row.isIndeterminate = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字段单选逻辑
|
||||||
|
const handleCheckedElementsChange = (val: string[], row: PermissionNode) => {
|
||||||
|
updateCheckAllStatus(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCheckAllStatus = (row: PermissionNode) => {
|
||||||
|
const checkedCount = row.checkedElements.length
|
||||||
|
row.checkAllElements = checkedCount === row.elements.length && row.elements.length > 0
|
||||||
|
row.isIndeterminate = checkedCount > 0 && checkedCount < row.elements.length
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 保存逻辑 ---
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!currentRole.value) return
|
||||||
|
|
||||||
|
const permissions: string[] = []
|
||||||
|
|
||||||
|
// 递归收集所有状态为 true 的 code
|
||||||
|
const collectPermissions = (rows: PermissionNode[]) => {
|
||||||
|
rows.forEach(row => {
|
||||||
|
// 1. 页面权限
|
||||||
|
if (row.hasRead) {
|
||||||
|
permissions.push(row.code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 操作权限 (只有当页面可读时才有效)
|
||||||
|
if (row.hasRead && row.hasWrite && row.operationCode) {
|
||||||
|
permissions.push(row.operationCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 字段权限 (只有当页面可读时才有效)
|
||||||
|
if (row.hasRead && row.checkedElements.length > 0) {
|
||||||
|
permissions.push(...row.checkedElements)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归
|
||||||
|
if (row.children) {
|
||||||
|
collectPermissions(row.children)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
collectPermissions(tableData.value)
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const res: any = await saveRolePermissions({
|
||||||
|
role_code: currentRole.value,
|
||||||
|
permissions: permissions
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('权限保存成功!')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '保存失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('保存失败')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 辅助 ---
|
||||||
|
const getRoleLabel = (val: string) => {
|
||||||
|
const found = roleList.find(r => r.value === val)
|
||||||
|
return found ? found.label : val
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCheckedCount = (row: PermissionNode) => row.checkedElements.length
|
||||||
|
|
||||||
|
const getElementName = (row: PermissionNode, code: string) => {
|
||||||
|
const found = row.elements.find(e => e.code === code)
|
||||||
|
return found ? found.name : code
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchTree()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-container {
|
||||||
|
padding: 15px;
|
||||||
|
height: calc(100vh - 84px);
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission-card {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__header) {
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-title {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 布局 */
|
||||||
|
.main-layout {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧样式 */
|
||||||
|
.left-panel {
|
||||||
|
width: 260px;
|
||||||
|
background: #fff;
|
||||||
|
border-right: 1px solid #f0f0f0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
padding: 15px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #606266;
|
||||||
|
background: #fafafa;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 15px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-item:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-item.active {
|
||||||
|
background-color: #e6f7ff;
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-icon {
|
||||||
|
margin-right: 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-name {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-code {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧样式 */
|
||||||
|
.right-panel {
|
||||||
|
flex: 1;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-content {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-content .panel-header {
|
||||||
|
background: #fff;
|
||||||
|
padding: 0 0 15px 0;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission-table {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格内样式 */
|
||||||
|
.custom-checkbox {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-active {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-disabled {
|
||||||
|
color: #c0c4cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray {
|
||||||
|
color: #d9d9d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 字段配置 Popover 样式 */
|
||||||
|
.field-config-box {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-tags {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 8px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-tag {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user