做权限管理模块
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
/**
|
||||
* 获取所有菜单权限列表(用于分配权限)
|
||||
*/
|
||||
export function getAllMenuPermissions() {
|
||||
return request({
|
||||
url: '/api/permissions/menus',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定角色的权限
|
||||
* @param {number} roleId - 角色ID
|
||||
*/
|
||||
export function getRolePermissions(roleId) {
|
||||
return request({
|
||||
url: `/api/permissions/role/${roleId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 为角色分配权限
|
||||
* @param {number} roleId - 角色ID
|
||||
* @param {Array<number>} menuIds - 菜单ID列表
|
||||
*/
|
||||
export function assignRolePermissions(roleId, menuIds) {
|
||||
return request({
|
||||
url: `/api/permissions/role/${roleId}`,
|
||||
method: 'post',
|
||||
timeout: 60000, // 设置60秒超时,因为批量保存可能需要较长时间
|
||||
data: {
|
||||
menu_ids: menuIds,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户的权限
|
||||
*/
|
||||
export function getUserPermissions() {
|
||||
return request({
|
||||
url: '/api/permissions/user',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户有权限访问的菜单树
|
||||
*/
|
||||
export function getUserMenuTree() {
|
||||
return request({
|
||||
url: '/api/permissions/user/menus',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否拥有指定权限
|
||||
* @param {string} permission - 权限标识
|
||||
*/
|
||||
export function checkPermission(permission) {
|
||||
return request({
|
||||
url: '/api/permissions/check',
|
||||
method: 'get',
|
||||
params: {
|
||||
permission,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,112 +1,602 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>权限管理</h2>
|
||||
<el-button type="primary" @click="handleAddPermission = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加权限
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="permissions-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="title">
|
||||
<el-icon><Key /></el-icon>
|
||||
权限管理
|
||||
</span>
|
||||
<span class="subtitle">为角色分配菜单和API访问权限</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-divider></el-divider>
|
||||
<el-row :gutter="20">
|
||||
<!-- 左侧:角色列表 -->
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover" class="role-card">
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
<span>角色列表</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-input
|
||||
v-model="roleSearchQuery"
|
||||
placeholder="搜索角色..."
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
class="search-input"
|
||||
/>
|
||||
|
||||
<el-table :data="permissions" style="width: 100%">
|
||||
<el-table-column prop="name" label="权限名称" width="180" align="center" />
|
||||
<el-table-column prop="code" label="权限代码" width="200" align="center" />
|
||||
<el-table-column prop="description" label="权限描述" align="center" />
|
||||
<el-table-column prop="type" label="权限类型" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.type === 'menu' ? 'primary' : 'warning'">
|
||||
{{ scope.row.type === "menu" ? "菜单权限" : "功能权限" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handleEdit(scope.row)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button size="small" type="danger" @click="handleDelete(scope.row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
background
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
@current-change="handlePageChange"
|
||||
layout="total, prev, pager, next"
|
||||
/>
|
||||
</div>
|
||||
<el-scrollbar height="600px" class="role-list">
|
||||
<div
|
||||
v-for="role in filteredRoles"
|
||||
:key="role.roleId"
|
||||
:class="['role-item', { active: selectedRole?.roleId === role.roleId }]"
|
||||
@click="selectRole(role)"
|
||||
>
|
||||
<div class="role-info">
|
||||
<div class="role-name">{{ role.roleName }}</div>
|
||||
<div class="role-code">{{ role.roleCode }}</div>
|
||||
</div>
|
||||
<el-icon v-if="selectedRole?.roleId === role.roleId" class="check-icon">
|
||||
<Check />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="filteredRoles.length === 0" description="暂无角色数据" />
|
||||
</el-scrollbar>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 右侧:权限分配 -->
|
||||
<el-col :span="16">
|
||||
<el-card shadow="hover" class="permission-card">
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<el-icon><Menu /></el-icon>
|
||||
<span>权限分配</span>
|
||||
<span v-if="selectedRole" class="selected-role-name">
|
||||
({{ selectedRole.roleName }})
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="!selectedRole" class="empty-state">
|
||||
<el-empty description="请先选择一个角色" />
|
||||
</div>
|
||||
|
||||
<div v-else class="permission-content">
|
||||
<!-- 搜索和操作按钮 -->
|
||||
<div class="toolbar">
|
||||
<el-input
|
||||
v-model="permissionSearchQuery"
|
||||
placeholder="搜索菜单..."
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
style="width: 300px;"
|
||||
/>
|
||||
|
||||
<div class="actions">
|
||||
<el-button @click="expandAll">全部展开</el-button>
|
||||
<el-button @click="collapseAll">全部折叠</el-button>
|
||||
<el-button @click="checkAll">全选</el-button>
|
||||
<el-button @click="uncheckAll">取消全选</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 权限树 -->
|
||||
<el-scrollbar height="550px" class="tree-container">
|
||||
<el-tree
|
||||
ref="permissionTree"
|
||||
:data="menuTreeData"
|
||||
:props="treeProps"
|
||||
:filter-node-method="filterNode"
|
||||
node-key="menu_id"
|
||||
show-checkbox
|
||||
:default-expand-all="false"
|
||||
:check-strictly="false"
|
||||
class="permission-tree"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="custom-tree-node">
|
||||
<div class="node-content">
|
||||
<el-icon v-if="data.menu_type === 1" class="menu-icon">
|
||||
<Folder />
|
||||
</el-icon>
|
||||
<el-icon v-else class="api-icon">
|
||||
<Link />
|
||||
</el-icon>
|
||||
<span class="node-label">{{ node.label }}</span>
|
||||
</div>
|
||||
<div class="node-info">
|
||||
<el-tag v-if="data.menu_type === 1" type="primary" size="small">
|
||||
页面
|
||||
</el-tag>
|
||||
<el-tag v-else type="success" size="small">
|
||||
API
|
||||
</el-tag>
|
||||
<el-tag v-if="data.permission" type="info" size="small" class="permission-tag">
|
||||
{{ data.permission }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-scrollbar>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div class="footer">
|
||||
<el-button type="primary" @click="savePermissions" :loading="saving">
|
||||
<el-icon><Select /></el-icon>
|
||||
保存权限设置
|
||||
</el-button>
|
||||
<el-button @click="resetPermissions">
|
||||
<el-icon><RefreshLeft /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import {
|
||||
Key,
|
||||
UserFilled,
|
||||
Menu,
|
||||
Search,
|
||||
Check,
|
||||
Folder,
|
||||
Link,
|
||||
Select,
|
||||
RefreshLeft,
|
||||
} from '@element-plus/icons-vue';
|
||||
import { getRoleByTenantId } from '@/api/role';
|
||||
import {
|
||||
getAllMenuPermissions,
|
||||
getRolePermissions,
|
||||
assignRolePermissions,
|
||||
} from '@/api/permission';
|
||||
|
||||
// const loading = ref(false);
|
||||
// const error = ref("");
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
// 角色相关
|
||||
const roleList = ref([]);
|
||||
const roleSearchQuery = ref('');
|
||||
const selectedRole = ref(null);
|
||||
|
||||
interface Permission {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
description: string;
|
||||
type: string;
|
||||
}
|
||||
// 权限相关
|
||||
const allMenus = ref([]);
|
||||
const menuTreeData = ref([]);
|
||||
const permissionSearchQuery = ref('');
|
||||
const permissionTree = ref(null);
|
||||
const saving = ref(false);
|
||||
|
||||
const permissions = ref<Permission[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: "用户管理",
|
||||
code: "user:manage",
|
||||
description: "用户的增删改查权限",
|
||||
type: "menu",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "角色管理",
|
||||
code: "role:manage",
|
||||
description: "角色的增删改查权限",
|
||||
type: "menu",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "数据导出",
|
||||
code: "data:export",
|
||||
description: "数据导出功能权限",
|
||||
type: "function",
|
||||
},
|
||||
]);
|
||||
|
||||
const handleAddPermission = () => {
|
||||
// 添加权限功能
|
||||
// 树配置
|
||||
const treeProps = {
|
||||
children: 'children',
|
||||
label: 'menu_name',
|
||||
};
|
||||
|
||||
const handleEdit = (permission: Permission) => {
|
||||
// 编辑权限功能
|
||||
// 过滤后的角色列表
|
||||
const filteredRoles = computed(() => {
|
||||
if (!roleSearchQuery.value) {
|
||||
return roleList.value;
|
||||
}
|
||||
const query = roleSearchQuery.value.toLowerCase();
|
||||
return roleList.value.filter(
|
||||
(role) =>
|
||||
role.roleName.toLowerCase().includes(query) ||
|
||||
role.roleCode.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
// 获取当前租户ID
|
||||
const getCurrentTenantId = () => {
|
||||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
|
||||
return userInfo.tenant_id || 1;
|
||||
};
|
||||
|
||||
const handleDelete = (permission: Permission) => {
|
||||
// 删除权限功能
|
||||
// 加载角色列表
|
||||
const loadRoles = async () => {
|
||||
try {
|
||||
const tenantId = getCurrentTenantId();
|
||||
const res = await getRoleByTenantId(tenantId);
|
||||
|
||||
// 兼容两种响应格式:success 和 code
|
||||
const isSuccess = res.success || res.code === 0;
|
||||
if (isSuccess && res.data) {
|
||||
roleList.value = res.data;
|
||||
} else {
|
||||
ElMessage.warning(res.message || '未获取到角色数据');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载角色列表失败:', error);
|
||||
ElMessage.error('加载角色列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 加载所有菜单权限
|
||||
const loadAllMenus = async () => {
|
||||
try {
|
||||
const res = await getAllMenuPermissions();
|
||||
|
||||
if (res.success && res.data) {
|
||||
allMenus.value = res.data;
|
||||
buildMenuTree();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载菜单列表失败:', error);
|
||||
ElMessage.error('加载菜单列表失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 构建菜单树
|
||||
const buildMenuTree = () => {
|
||||
const tree = [];
|
||||
const map = new Map();
|
||||
|
||||
// 先创建所有节点的映射
|
||||
allMenus.value.forEach((menu) => {
|
||||
map.set(menu.menu_id, { ...menu, children: [] });
|
||||
});
|
||||
|
||||
// 构建树形结构
|
||||
allMenus.value.forEach((menu) => {
|
||||
const node = map.get(menu.menu_id);
|
||||
if (menu.parent_id === 0) {
|
||||
tree.push(node);
|
||||
} else {
|
||||
const parent = map.get(menu.parent_id);
|
||||
if (parent) {
|
||||
parent.children.push(node);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
menuTreeData.value = tree;
|
||||
};
|
||||
|
||||
// 选择角色
|
||||
const selectRole = async (role) => {
|
||||
selectedRole.value = role;
|
||||
await loadRolePermissions(role.roleId);
|
||||
};
|
||||
|
||||
// 加载角色权限
|
||||
const loadRolePermissions = async (roleId) => {
|
||||
try {
|
||||
const res = await getRolePermissions(roleId);
|
||||
|
||||
if (res.success && res.data) {
|
||||
// 等待树渲染完成
|
||||
await nextTick();
|
||||
|
||||
// 设置选中的节点
|
||||
if (permissionTree.value) {
|
||||
permissionTree.value.setCheckedKeys(res.data.menu_ids || []);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载角色权限失败:', error);
|
||||
ElMessage.error('加载角色权限失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 保存权限
|
||||
const savePermissions = async () => {
|
||||
if (!selectedRole.value) {
|
||||
ElMessage.warning('请先选择角色');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 只获取完全选中的节点(不包括半选中的父节点)
|
||||
// 因为半选中的父节点表示部分子节点被选中,父节点本身不需要权限记录
|
||||
const checkedKeys = permissionTree.value.getCheckedKeys();
|
||||
const menuIds = checkedKeys;
|
||||
|
||||
saving.value = true;
|
||||
const res = await assignRolePermissions(selectedRole.value.roleId, menuIds);
|
||||
|
||||
if (res.success) {
|
||||
ElMessage.success('权限保存成功');
|
||||
await loadRolePermissions(selectedRole.value.roleId);
|
||||
} else {
|
||||
ElMessage.error(res.message || '权限保存失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存权限失败:', error);
|
||||
if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) {
|
||||
ElMessage.error('请求超时,请重试。如果数据量较大,可能需要更长时间');
|
||||
} else {
|
||||
ElMessage.error(error.message || '保存权限失败,请稍后重试');
|
||||
}
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 重置权限
|
||||
const resetPermissions = async () => {
|
||||
if (!selectedRole.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要重置权限设置吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
|
||||
await loadRolePermissions(selectedRole.value.roleId);
|
||||
ElMessage.success('已重置');
|
||||
} catch (error) {
|
||||
// 用户取消操作
|
||||
}
|
||||
};
|
||||
|
||||
// 全部展开
|
||||
const expandAll = () => {
|
||||
if (permissionTree.value) {
|
||||
const allKeys = allMenus.value.map((m) => m.menu_id);
|
||||
allKeys.forEach((key) => {
|
||||
const node = permissionTree.value.getNode(key);
|
||||
if (node) {
|
||||
node.expanded = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 全部折叠
|
||||
const collapseAll = () => {
|
||||
if (permissionTree.value) {
|
||||
const allKeys = allMenus.value.map((m) => m.menu_id);
|
||||
allKeys.forEach((key) => {
|
||||
const node = permissionTree.value.getNode(key);
|
||||
if (node) {
|
||||
node.expanded = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 全选
|
||||
const checkAll = () => {
|
||||
if (permissionTree.value) {
|
||||
const allKeys = allMenus.value.map((m) => m.menu_id);
|
||||
permissionTree.value.setCheckedKeys(allKeys);
|
||||
}
|
||||
};
|
||||
|
||||
// 取消全选
|
||||
const uncheckAll = () => {
|
||||
if (permissionTree.value) {
|
||||
permissionTree.value.setCheckedKeys([]);
|
||||
}
|
||||
};
|
||||
|
||||
// 过滤节点
|
||||
const filterNode = (value, data) => {
|
||||
if (!value) return true;
|
||||
return (
|
||||
data.menu_name.toLowerCase().includes(value.toLowerCase()) ||
|
||||
(data.path && data.path.toLowerCase().includes(value.toLowerCase())) ||
|
||||
(data.permission && data.permission.toLowerCase().includes(value.toLowerCase()))
|
||||
);
|
||||
};
|
||||
|
||||
// 监听搜索框变化
|
||||
watch(permissionSearchQuery, (val) => {
|
||||
if (permissionTree.value) {
|
||||
permissionTree.value.filter(val);
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化
|
||||
const init = async () => {
|
||||
await loadRoles();
|
||||
await loadAllMenus();
|
||||
};
|
||||
|
||||
init();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="less" scoped>
|
||||
.permissions-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.role-card,
|
||||
.permission-card {
|
||||
height: 100%;
|
||||
|
||||
.card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
|
||||
.selected-role-name {
|
||||
font-size: 14px;
|
||||
color: var(--el-color-primary);
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.role-list {
|
||||
margin-top: 16px;
|
||||
|
||||
.role-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 8px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #e6f0ff;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--el-color-primary-light-9);
|
||||
border-left: 3px solid var(--el-color-primary);
|
||||
|
||||
.role-name {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.role-info {
|
||||
flex: 1;
|
||||
|
||||
.role-name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.role-code {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: var(--el-color-primary);
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.permission-content {
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-container {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.permission-tree {
|
||||
background: transparent;
|
||||
|
||||
:deep(.el-tree-node__content) {
|
||||
height: auto;
|
||||
padding: 8px 0;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&:hover {
|
||||
background: #e6f0ff;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-tree-node {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-right: 16px;
|
||||
|
||||
.node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.menu-icon {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.api-icon {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.node-label {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.node-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.permission-tag {
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user