添加角色页面.

This commit is contained in:
lijiahang
2023-08-15 18:32:05 +08:00
parent 2d50079e24
commit fa8dc6d8fa
11 changed files with 891 additions and 34 deletions

View File

@@ -0,0 +1,114 @@
import axios from 'axios';
import { DataGrid, Pagination } from '@/types/global';
/**
* 角色创建请求
*/
export interface RoleCreateRequest {
name?: string;
code?: string;
status?: number;
}
/**
* 角色更新请求
*/
export interface RoleUpdateRequest extends RoleCreateRequest {
id: number;
}
/**
* 角色 菜单绑定请求
*/
export interface RoleMenuBindRequest extends RoleCreateRequest {
roleId: number;
menuIdList: Array<number>;
}
/**
* 角色查询请求
*/
export interface RoleQueryRequest extends Pagination {
id?: number;
name?: string;
code?: string;
status?: number;
}
/**
* 角色查询响应
*/
export interface RoleQueryResponse {
id?: number;
name?: string;
code?: string;
status?: number;
createTime: number;
updateTime: number;
creator: string;
updater: string;
}
/**
* 创建角色
*/
export function createRole(request: RoleCreateRequest) {
return axios.post('/infra/system-role/create', request);
}
/**
* 通过 id 更新角色
*/
export function updateRole(request: RoleUpdateRequest) {
return axios.put('/infra/system-role/update', request);
}
/**
* 通过 id 更新角色状态
*/
export function updateRoleStatus(request: RoleUpdateRequest) {
return axios.put('/infra/system-role/update-status', request);
}
/**
* 通过 id 查询角色
*/
export function getRole(id: number) {
return axios.get<RoleQueryResponse>('/infra/system-role/get', { params: { id } });
}
/**
* 查询所有角色
*/
export function getRoleList() {
return axios.get<RoleQueryResponse[]>('/infra/system-role/list');
}
/**
* 分页查询角色
*/
export function getRolePage(request: RoleQueryRequest) {
return axios.post<DataGrid<RoleQueryResponse>>('/infra/system-role/query', request);
}
/**
* 通过 id 删除角色
*/
export function deleteRole(id: number) {
return axios.delete('/infra/system-role/delete', { params: { id } });
}
/**
* 绑定角色菜单
*/
export function bindRoleMenu(request: RoleMenuBindRequest) {
return axios.put('/infra/system-role/bind', request);
}
/**
* 获取角色菜单id
*/
export function getRoleMenuId(roleId: number) {
return axios.get<Array<number>>('/infra/system-role/get-menu-id', { params: { roleId } });
}

View File

@@ -0,0 +1,147 @@
<template>
<div>
<!-- 按钮组 -->
<a-button-group class="mb4">
<!-- 全选 -->
<a-button type="text" size="mini" @click="toggleChecked">
{{ checkedKeys?.length === allCheckedKeys?.length ? '反选' : '全选' }}
</a-button>
<!-- 展开 -->
<a-button type="text" size="mini" @click="toggleExpanded">
{{ expandedKeys?.length ? '折叠' : '展开' }}
</a-button>
</a-button-group>
<!-- 菜单树 -->
<a-tree
checked-strategy="child"
:checkable="true"
:animation="false"
v-model:checked-keys="checkedKeys"
v-model:expanded-keys="expandedKeys"
:data="treeData" />
</div>
</template>
<script lang="ts">
export default {
name: 'menu-selector-tree'
};
</script>
<script lang="ts" setup>
import { ref } from 'vue';
import { TreeNodeData } from '@arco-design/web-vue';
import { useCacheStore } from '@/store';
const treeData = ref<Array<TreeNodeData>>([]);
const allCheckedKeys = ref<Array<number>>([]);
const allExpandedKeys = ref<Array<number>>([]);
const checkedKeys = ref<Array<number>>([]);
const expandedKeys = ref<Array<number>>([]);
// 修改选中状态
const toggleChecked = () => {
checkedKeys.value = checkedKeys.value.length === allCheckedKeys.value.length ? [] : allCheckedKeys.value;
};
// 修改折叠状态
const toggleExpanded = () => {
expandedKeys.value = expandedKeys?.value.length ? [] : allExpandedKeys.value;
};
// 循环选中的 key
const eachAllCheckKeys = (arr: Array<any>) => {
arr.forEach((item) => {
allCheckedKeys.value.push(item.key);
if (item.children && item.children.length) {
eachAllCheckKeys(item.children);
}
});
};
// 循环展开的 key
const eachAllExpandKeys = (arr: Array<any>) => {
arr.forEach((item) => {
if (item.children && item.children.length) {
allExpandedKeys.value.push(item.key);
eachAllExpandKeys(item.children);
}
});
};
// 渲染数据
const init = (keys: Array<number>) => {
// 初始化数据
allCheckedKeys.value = [];
allExpandedKeys.value = [];
checkedKeys.value = keys;
expandedKeys.value = [];
// 渲染菜单
const cacheStore = useCacheStore();
let render = (arr: any[]): TreeNodeData[] => {
return arr.map((s) => {
// 当前节点
const node = {
key: s.id,
title: s.name,
children: undefined as unknown
} as TreeNodeData;
// 子节点
if (s.children && s.children.length) {
node.children = render(s.children);
}
return node;
}).filter(Boolean);
};
// 加载菜单
treeData.value = render([...cacheStore.menus]);
// 加载所有选中的key
eachAllCheckKeys(treeData.value);
// 加载所有展开的key
eachAllExpandKeys(treeData.value);
};
init();
// 获取值
const getValue = () => {
if (!checkedKeys.value.length) {
return [];
}
// 查询子节点上级父节点
const mixed: number[] = [];
const findParent = (arr: Array<TreeNodeData>, key: number) => {
for (let node of arr) {
// 是子节点 并且相同
if (node.key === key) {
mixed.push(key);
return true;
}
if (node.children?.length) {
const isFind = findParent(node.children, key);
if (isFind) {
mixed.push(node.key as number);
return true;
}
}
}
return false;
};
// 设置所有节点
for (let key of checkedKeys.value) {
findParent(treeData.value, key);
}
return new Set(mixed);
};
defineExpose({ init, getValue });
</script>
<style scoped>
</style>;

View File

@@ -1,15 +0,0 @@
<template>
<div>
<p>UserChild 1</p>
</div>
</template>
<script>
export default {
name: 'UserChild1',
};
</script>
<style scoped>
</style>

View File

@@ -1,19 +0,0 @@
<template>
<div>
<p>UserChild 2</p>
<h1 v-permission="['admin']">123</h1>
<button @click="red">red</button>
</div>
</template>
<script lang="ts" setup>
import router from '@/router';
function red() {
router.push({ name: 'workplace' });
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,138 @@
<template>
<a-modal v-model:visible="visible"
body-class="modal-form"
title-align="start"
:title="title"
:top="120"
:align-center="false"
:draggable="true"
:mask-closable="false"
:unmount-on-close="true"
:ok-button-props="{ disabled: loading }"
:cancel-button-props="{ disabled: loading }"
:on-before-ok="handlerOk"
@close="handleClose">
<a-spin :loading="loading">
<a-form :model="formModel"
ref="formRef"
label-align="right"
:style="{ width: '460px' }"
:label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 18 }"
:rules="formRules">
<!-- 角色名称 -->
<a-form-item field="name" label="角色名称">
<a-input v-model="formModel.name" placeholder="请输入角色名称" />
</a-form-item>
<!-- 角色编码 -->
<a-form-item field="code" label="角色编码" v-if="isAddHandle">
<a-input v-model="formModel.code" placeholder="请输入角色编码" />
</a-form-item>
</a-form>
</a-spin>
</a-modal>
</template>
<script lang="ts">
export default {
name: 'user-role-form-modal'
};
</script>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import formRules from '../types/form.rules';
import { createRole, updateRole } from '@/api/user/role';
import { Message } from '@arco-design/web-vue';
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();
const title = ref<string>();
const isAddHandle = ref<boolean>(true);
const defaultForm = () => {
return {
id: undefined,
name: undefined,
code: undefined,
};
};
const formRef = ref<any>();
const formModel = reactive<Record<string, any>>(defaultForm());
const emits = defineEmits(['added', 'updated']);
// 打开新增
const openAdd = () => {
title.value = '添加角色';
isAddHandle.value = true;
renderForm({ ...defaultForm() });
setVisible(true);
};
// 打开修改
const openUpdate = (record: any) => {
title.value = '修改角色';
isAddHandle.value = false;
renderForm({ ...defaultForm(), ...record });
setVisible(true);
};
// 渲染表单
const renderForm = (record: any) => {
Object.keys(formModel).forEach(k => {
formModel[k] = record[k];
});
};
defineExpose({ openAdd, openUpdate });
// 确定
const handlerOk = async () => {
setLoading(true);
try {
// 验证参数
const error = await formRef.value.validate();
if (error) {
return false;
}
if (isAddHandle.value) {
// 新增
await createRole(formModel as any);
Message.success('创建成功');
emits('added');
} else {
// 修改
await updateRole(formModel as any);
Message.success('修改成功');
emits('updated');
}
// 清空
handlerClear();
} catch (e) {
return false;
} finally {
setLoading(false);
}
};
// 关闭
const handleClose = () => {
handlerClear();
};
// 清空
const handlerClear = () => {
setLoading(false);
setVisible(false);
};
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,151 @@
<template>
<a-modal v-model:visible="visible"
body-class="modal-form"
title-align="start"
title="绑定菜单"
:top="80"
:width="480"
:body-style="{padding: '16px 16px 0 16px'}"
:align-center="false"
:draggable="true"
:mask-closable="false"
:unmount-on-close="true"
:ok-button-props="{ disabled: loading }"
:cancel-button-props="{ disabled: loading }"
:on-before-ok="handlerOk"
@close="handleClose">
<a-spin :loading="loading">
<div class="role-menu-wrapper">
<!-- 角色名称 -->
<div class="item-wrapper">
<span class="item-label">角色名称</span>
<span class="item-value ml4">{{ roleRecord.name }}</span>
</div>
<!-- 角色编码 -->
<div class="item-wrapper">
<span class="item-label">角色编码</span>
<a-tag class="item-value">{{ roleRecord.code }}</a-tag>
</div>
<!-- 菜单 -->
<div class="menu-tree-wrapper item-wrapper">
<span class="item-label">菜单选择</span>
<menu-selector-tree ref="tree" class="item-value" />
</div>
</div>
</a-spin>
</a-modal>
</template>
<script lang="ts">
export default {
name: 'role-menu-bind-modal'
};
</script>
<script lang="ts" setup>
import MenuSelectorTree from '@/components/menu/selector/menu-selector-tree.vue';
import { reactive, ref } from 'vue';
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import { getRoleMenuId, bindRoleMenu, RoleMenuBindRequest } from '@/api/user/role';
import { Message } from '@arco-design/web-vue';
import { useCacheStore } from '@/store';
import { getMenuList } from '@/api/system/menu';
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();
const tree = ref<any>();
const roleRecord = reactive<Record<string, any>>({
id: undefined,
name: undefined,
code: undefined,
});
// 打开新增
const open = async (record: any) => {
renderRecord(record);
setVisible(true);
try {
setLoading(true);
// 获取菜单列表
const cacheStore = useCacheStore();
if (!cacheStore.menus?.length) {
// 加载菜单
const { data: menuData } = await getMenuList({});
cacheStore.updateMenu(menuData);
}
// 获取角色菜单
const { data: roleMenuIdList } = await getRoleMenuId(record.id);
tree.value.init(roleMenuIdList);
} finally {
setLoading(false);
}
};
// 渲染对象
const renderRecord = (record: any) => {
Object.keys(roleRecord).forEach(k => {
roleRecord[k] = record[k];
});
};
defineExpose({ open });
// 确定
const handlerOk = async () => {
setLoading(true);
try {
// 修改
await bindRoleMenu({
roleId: roleRecord.id,
menuIdList: [...tree.value.getValue()]
} as RoleMenuBindRequest);
Message.success('绑定成功');
// 清空
handlerClear();
} catch (e) {
return false;
} finally {
setLoading(false);
}
};
// 关闭
const handleClose = () => {
handlerClear();
};
// 清空
const handlerClear = () => {
setLoading(false);
setVisible(false);
};
</script>
<style lang="less" scoped>
.role-menu-wrapper {
min-width: 400px;
max-height: calc(100vh - 285px);
.item-wrapper {
display: flex;
margin-bottom: 16px;
align-items: baseline;
.item-label {
display: inline-block;
text-align: right;
width: 80px;
height: 32px;
margin-right: 12px;
line-height: 32px;
color: var(--color-text-2);
}
}
}
</style>

View File

@@ -0,0 +1,215 @@
<template>
<!-- 搜索 -->
<a-card class="general-card table-search-card">
<a-query-header :model="formModel"
label-align="left"
@submit="fetchTableData"
@reset="fetchTableData">
<!-- 角色名称 -->
<a-form-item field="name" label="角色名称" label-col-flex="50px">
<a-input v-model="formModel.name" placeholder="请输入角色名称" allow-clear />
</a-form-item>
<!-- 角色编码 -->
<a-form-item field="code" label="角色编码" label-col-flex="50px">
<a-input v-model="formModel.code" placeholder="请输入角色编码" allow-clear />
</a-form-item>
<!-- 角色状态 -->
<a-form-item field="status" label="角色状态" label-col-flex="50px">
<a-select v-model="formModel.status"
placeholder="请选择角色状态"
:options="toOptions(ROLE_STATUS_ENUM)"
allow-clear />
</a-form-item>
</a-query-header>
</a-card>
<!-- 表格 -->
<a-card class="general-card table-card">
<template #title>
<!-- 左侧标题 -->
<div class="table-title">
角色列表
</div>
<!-- 右侧按钮 -->
<div class="table-bar-handle">
<a-space>
<!-- 新增 -->
<a-button type="primary"
v-permission="['infra:system-role:create']"
@click="emits('openAdd')">
新增
<template #icon>
<icon-plus />
</template>
</a-button>
</a-space>
</div>
</template>
<!-- table -->
<a-table row-key="id"
class="table-wrapper-8"
ref="tableRef"
label-align="left"
:loading="loading"
:columns="columns"
:data="tableRenderData"
:pagination="pagination"
@page-change="(page) => fetchTableData(page, pagination.pageSize)"
@page-size-change="(size) => fetchTableData(pagination.current, size)"
:bordered="false">
<!-- 编码 -->
<template #code="{ record }">
<a-tag>{{ record.code }}</a-tag>
</template>
<!-- 状态 -->
<template #status="{ record }">
<a-tag :color="getEnumValue(record.status, ROLE_STATUS_ENUM,'color')">
{{ getEnumValue(record.status, ROLE_STATUS_ENUM) }}
</a-tag>
</template>
<!-- 操作 -->
<template #handle="{ record }">
<div class="table-handle-wrapper">
<!-- 修改状态 -->
<a-popconfirm :content="`确定要${toggleEnumValue(record.status, ROLE_STATUS_ENUM, 'label')}当前角色吗?`"
position="left"
type="warning"
@ok="toggleRoleStatus(record)">
<a-button v-permission="['infra:system-role:delete']"
:disabled="record.code === 'admin'"
:status="toggleEnumValue(record.status, ROLE_STATUS_ENUM, 'status')"
type="text"
size="mini">
{{ toggleEnumValue(record.status, ROLE_STATUS_ENUM, 'label') }}
</a-button>
</a-popconfirm>
<!-- 绑定菜单 -->
<a-button v-permission="['infra:system-menu:bind']"
:disabled="record.code === 'admin'"
type="text"
size="mini"
@click="emits('openBind', record)">
绑定菜单
</a-button>
<!-- 修改 -->
<a-button v-permission="['infra:system-role:update']"
type="text"
size="mini"
@click="emits('openUpdate', record)">
修改
</a-button>
<!-- 删除 -->
<a-popconfirm content="确认删除这条记录吗?"
position="left"
type="warning"
@ok="deleteRow(record)">
<a-button v-permission="['infra:system-role:delete']"
:disabled="record.code === 'admin'"
type="text"
size="mini"
status="danger">
删除
</a-button>
</a-popconfirm>
</div>
</template>
</a-table>
</a-card>
</template>
<script lang="ts">
export default {
name: 'user-role-table'
};
</script>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { deleteRole, getRolePage, updateRoleStatus, RoleQueryRequest, RoleQueryResponse } from '@/api/user/role';
import { Message } from '@arco-design/web-vue';
import useLoading from '@/hooks/loading';
import columns from '../types/table.columns';
import { ROLE_STATUS_ENUM } from '../types/enum.types';
import { toOptions, getEnumValue, toggleEnumValue, toggleEnum } from '@/utils/enum';
import { defaultPagination } from '@/types/table';
const tableRenderData = ref<RoleQueryResponse[]>();
const { loading, setLoading } = useLoading();
const emits = defineEmits(['openAdd', 'openUpdate', 'openBind']);
const pagination = reactive(defaultPagination());
const formModel = reactive<RoleQueryRequest>({
id: undefined,
name: undefined,
code: undefined,
status: undefined,
});
// 修改状态
const toggleRoleStatus = async (record: any) => {
try {
setLoading(true);
const toggleStatus = toggleEnum(record.status, ROLE_STATUS_ENUM);
// 调用修改接口
await updateRoleStatus({ id: record.id, status: toggleStatus.value });
Message.success(`${toggleStatus.label}成功`);
// 修改行状态
record.status = toggleStatus.value;
} finally {
setLoading(false);
}
};
// 删除当前行
const deleteRow = async ({ id }: { id: number }) => {
try {
setLoading(true);
// 调用删除接口
await deleteRole(id);
Message.success('删除成功');
// 重新加载数据
await fetchTableData();
} finally {
setLoading(false);
}
};
// 添加后回调
const addedCallback = () => {
fetchTableData();
};
// 更新后回调
const updatedCallback = () => {
fetchTableData();
};
defineExpose({
addedCallback, updatedCallback
});
// 加载数据
const doFetchTableData = async (request: RoleQueryRequest) => {
try {
setLoading(true);
const { data } = await getRolePage(request);
tableRenderData.value = data.rows;
pagination.total = data.total;
pagination.current = request.page;
pagination.pageSize = request.limit;
} finally {
setLoading(false);
}
};
// 切换页码
const fetchTableData = (page = 1, limit = pagination.pageSize, form = formModel) => {
doFetchTableData({ page, limit, ...form });
};
fetchTableData();
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,44 @@
<template>
<div class="layout-container">
<!-- 表格 -->
<role-table ref="table"
@openAdd="() => modal.openAdd()"
@openUpdate="(e) => modal.openUpdate(e)"
@openBind="(e) => bindModal.open(e)" />
<!-- 添加修改模态框 -->
<role-form-modal ref="modal"
@added="() => table.addedCallback()"
@updated="() => table.updatedCallback()" />
<!-- 角色菜单绑定模态框 -->
<role-menu-bind-modal ref="bindModal" />
</div>
</template>
<script lang="ts">
export default {
name: 'userRole'
};
</script>
<script lang="ts" setup>
import RoleTable from './components/role-table.vue';
import RoleFormModal from './components/role-form-modal.vue';
import RoleMenuBindModal from '@/views/user/role/components/role-menu-bind-modal.vue';
import { onUnmounted, ref } from 'vue';
import { useCacheStore } from '@/store';
const table = ref();
const modal = ref();
const bindModal = ref();
// 卸载时清除 menu cache
onUnmounted(() => {
const cacheStore = useCacheStore();
cacheStore.resetMenu();
});
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,18 @@
/**
* 角色状态
*/
export const ROLE_STATUS_ENUM = {
DISABLED: {
value: 0,
label: '停用',
color: 'orange',
status: 'danger',
},
ENABLED: {
value: 1,
label: '启用',
color: 'blue',
status: 'default',
},
};

View File

@@ -0,0 +1,22 @@
import { FieldRule } from '@arco-design/web-vue';
export const name = [{
required: true,
message: '请输入角色名称'
}, {
maxLength: 32,
message: '角色名称长度不能大于32位'
}] as FieldRule[];
export const code = [{
required: true,
message: '请输入角色编码'
}, {
maxLength: 32,
message: '角色编码长度不能大于32位'
}] as FieldRule[];
export default {
name,
code,
} as Record<string, FieldRule | FieldRule[]>;

View File

@@ -0,0 +1,42 @@
import { TableColumnData } from '@arco-design/web-vue/es/table/interface';
import { dateFormat } from '@/utils';
const columns = [
{
title: 'id',
dataIndex: 'id',
slotName: 'id',
width: 70,
align: 'left',
fixed: 'left',
}, {
title: '角色名称',
dataIndex: 'name',
slotName: 'name',
}, {
title: '角色编码',
dataIndex: 'code',
slotName: 'code',
}, {
title: '角色状态',
dataIndex: 'status',
slotName: 'status',
align: 'center',
}, {
title: '创建时间',
dataIndex: 'createTime',
slotName: 'createTime',
align: 'center',
render: ({ record }) => {
return dateFormat(new Date(record.createTime));
},
}, {
title: '操作',
slotName: 'handle',
width: 240,
align: 'center',
fixed: 'right',
},
] as TableColumnData[];
export default columns;