Merge pull request #1 from L1yp/main

feat: 新增角色管理、岗位管理
This commit is contained in:
L1yp 2023-07-23 21:14:04 +08:00 committed by GitHub
commit 754447baa6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 950 additions and 292 deletions

View File

@ -1,4 +1,5 @@
import { http } from "@/utils/http";
import { Tree } from "@/utils/tree";
export interface MenuQuery {
isButton: boolean;
@ -7,7 +8,7 @@ export interface MenuQuery {
/**
* MenuDTO
*/
export interface MenuDTO {
export interface MenuDTO extends Tree {
createTime?: Date;
isButton?: number;
id?: number;

70
src/api/system/post.ts Normal file
View File

@ -0,0 +1,70 @@
import { http } from "@/utils/http";
export interface PostListCommand extends BasePageQuery {
postCode?: string;
postName?: string;
status?: number;
}
export interface PostPageResponse {
createTime: string;
postCode: string;
postId: number;
postName: string;
postSort: number;
remark: string;
status: number;
statusStr: string;
}
export function getPostListApi(params: PostListCommand) {
return http.request<ResponseData<PageDTO<PostPageResponse>>>(
"get",
"/system/post/list",
{
params
}
);
}
export const exportPostExcelApi = (
params: PostListCommand,
fileName: string
) => {
return http.download("/system/post/excel", fileName, {
params
});
};
export const deletePostApi = (data: Array<number>) => {
return http.request<ResponseData<void>>("delete", "/system/post", {
params: {
// 需要将数组转换为字符串 否则Axios会将参数变成 noticeIds[0]:1 noticeIds[1]:2 这种格式,后端接收参数不成功
ids: data.toString()
}
});
};
export interface AddPostCommand {
postCode: string;
postName: string;
postSort: number;
remark?: string;
status?: string;
}
export const addPostApi = (data: AddPostCommand) => {
return http.request<ResponseData<void>>("post", "/system/post", {
data
});
};
export interface UpdatePostCommand extends AddPostCommand {
postId: number;
}
export const updatePostApi = (data: UpdatePostCommand) => {
return http.request<ResponseData<void>>("put", "/system/post", {
data
});
};

65
src/api/system/role.ts Normal file
View File

@ -0,0 +1,65 @@
import { http } from "@/utils/http";
export interface RoleQuery extends BasePageQuery {
roleKey?: string;
roleName?: string;
status?: string;
timeRangeColumn?: string;
}
export interface RoleDTO {
createTime: Date;
dataScope: number;
remark: string;
roleId: number;
roleKey: string;
roleName: string;
roleSort: number;
selectedDeptList: number[];
selectedMenuList: number[];
status: number;
}
export function getRoleListApi(params: RoleQuery) {
return http.request<ResponseData<PageDTO<RoleDTO>>>(
"get",
"/system/role/list",
{
params
}
);
}
export function getRoleInfoApi(roleId: number) {
return http.request<ResponseData<RoleDTO>>("get", "/system/role/" + roleId);
}
export interface AddRoleCommand {
dataScope?: string;
menuIds: number[];
remark?: string;
roleKey: string;
roleName: string;
roleSort: number;
status?: string;
}
export function addRoleApi(data: AddRoleCommand) {
return http.request<void>("post", "/system/role", {
data
});
}
export interface UpdateRoleCommand extends AddRoleCommand {
roleId: number;
}
export function updateRoleApi(data: UpdateRoleCommand) {
return http.request<void>("put", "/system/role", {
data
});
}
export function deleteRoleApi(roleId: number) {
return http.request<void>("delete", "/system/role/" + roleId);
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 20 20"><g fill="none"><path d="M3 5a2 2 0 0 1 2-2h2a.5.5 0 0 1 0 1H5a1 1 0 0 0-1 1v2a.5.5 0 0 1-1 0V5zm9.5-1.5A.5.5 0 0 1 13 3h2a2 2 0 0 1 2 2v2a.5.5 0 0 1-1 0V5a1 1 0 0 0-1-1h-2a.5.5 0 0 1-.5-.5zm-9 9a.5.5 0 0 1 .5.5v2a1 1 0 0 0 1 1h2a.5.5 0 0 1 0 1H5a2 2 0 0 1-2-2v-2a.5.5 0 0 1 .5-.5zm13 0a.5.5 0 0 1 .5.5v2a2 2 0 0 1-2 2h-2a.5.5 0 0 1 0-1h2a1 1 0 0 0 1-1v-2a.5.5 0 0 1 .5-.5z" fill="currentColor"/></g></svg>

After

Width:  |  Height:  |  Size: 508 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24"><g fill="none"><path d="M8.5 3.75a.75.75 0 0 0-1.5 0v2.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 0 0 1.5h2.5A2.25 2.25 0 0 0 8.5 6.25v-2.5zm0 16.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 0-.75-.75h-2.5a.75.75 0 0 1 0-1.5h2.5a2.25 2.25 0 0 1 2.25 2.25v2.5zM16.25 3a.75.75 0 0 0-.75.75v2.5a2.25 2.25 0 0 0 2.25 2.25h2.5a.75.75 0 0 0 0-1.5h-2.5a.75.75 0 0 1-.75-.75v-2.5a.75.75 0 0 0-.75-.75zm-.75 17.25a.75.75 0 0 0 1.5 0v-2.5a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 0 0-1.5h-2.5a2.25 2.25 0 0 0-2.25 2.25v2.5z" fill="currentColor"/></g></svg>

After

Width:  |  Height:  |  Size: 631 B

View File

@ -0,0 +1,173 @@
<template>
<el-dialog
v-model="visible"
:fullscreen="fullScreen"
class="v-dialog"
:class="dialogClazz"
:draggable="props.draggable"
:show-close="false"
v-bind="$attrs"
>
<template #header>
<slot name="header">
<div
style="
position: relative;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 24px;
font-weight: bold;
color: rgb(118 131 164);
"
>
<div v-text="props.title" />
<div
style="
position: absolute;
right: 0;
display: flex;
flex-direction: row;
justify-content: center;
"
>
<el-button
v-if="props.showFullScreen"
:icon="fullScreen ? FullScreenMinimize : FullScreenMaximize"
link
@click="requestFullScreen"
class="header-btn"
/>
<el-button
:icon="Close"
link
@click="handleCloseClick"
class="header-btn"
style="margin-left: 0"
/>
</div>
</div>
</slot>
</template>
<template v-if="useBodyScrolling">
<el-scrollbar :max-height="bodyHeight" always>
<slot name="default" />
</el-scrollbar>
</template>
<template v-else>
<slot name="default" />
</template>
<template #footer>
<slot name="footer" v-if="!props.hiddenFooter">
<div style="display: flex; justify-content: center">
<el-button
:loading="props.loading"
type="primary"
@click="handleConfirm"
>{{ props.confirmText }}</el-button
>
<el-button :loading="props.loading" @click="handleCancel">{{
cancelText
}}</el-button>
</div>
</slot>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { computed, ref } from "vue";
import { ElDialog, ElButton, ElScrollbar } from "element-plus";
import { DialogEmits, DialogProps } from "./dialog";
import { Close } from "@element-plus/icons-vue";
import FullScreenMaximize from "@/assets/svg/FullScreenMaximize.svg?component";
import FullScreenMinimize from "@/assets/svg/FullScreenMinimize.svg?component";
const props = withDefaults(defineProps<DialogProps>(), {
fullScreen: undefined,
confirmText: "确定",
cancelText: "取消",
disableFooter: false,
useBodyScrolling: false,
fixedBodyHeight: true,
draggable: true,
loading: false
});
const emits = defineEmits<DialogEmits>();
const visible = computed<boolean>({
get: () => {
return props.modelValue;
},
set: v => emits("update:modelValue", v)
});
const fullScreenState = ref(!!props.initFullScreen);
const fullScreen = computed<boolean>({
get: () => {
console.log("fullScreen getter", props.fullScreen, fullScreenState.value);
//
if (props.fullScreen === undefined) {
return fullScreenState.value;
} else {
return props.fullScreen;
}
},
set: v => {
fullScreenState.value = v;
console.log("fullScreen setter", v, props.fullScreen);
//
if (props.fullScreen !== undefined) {
emits("update:fullScreen", v);
}
}
});
// const fullScreen = ref<boolean>(false)
function requestFullScreen() {
fullScreen.value = !fullScreen.value;
}
const bodyHeight = computed(() => {
const footerHeight = props.hiddenFooter ? "0" : "52px";
if (props.fullScreen) {
// footerHeight=52,headerHeight=44,padding=12
return `calc(100dvh - ${footerHeight} - 44px)`;
} else {
return `calc(70dvh - ${footerHeight} - 44px)`;
}
});
const dialogClazz = computed(() => {
const classList: string[] = ["v-dialog"];
if (!props.fixedBodyHeight) {
classList.push("flex-body");
}
if (props.hiddenFooter) {
classList.push("hidden-footer");
}
return classList;
});
function handleConfirm() {
emits("confirm");
}
function handleCancel() {
emits("cancel");
}
function handleCloseClick() {
visible.value = false;
}
</script>
<style scoped>
.header-btn :deep(.el-icon),
.header-btn :deep(.el-icon svg) {
width: 24px;
height: 24px;
}
</style>

View File

@ -0,0 +1,62 @@
html.dark .v-dialog {
--header-bg-color: #171d1e;
--footer-bg-color: #171d1e;
}
.v-dialog {
--header-bg-color: #f5f7fa;
--footer-bg-color: #f5f7fa;
}
.v-dialog.el-dialog.hidden-footer .el-dialog__footer {
padding: 0;
border: none;
}
.el-dialog__footer {
padding: 10px;
/*border-top: 1px solid var(--el-border-color);*/
/*border-bottom: 1px solid var(--el-border-color);*/
box-sizing: border-box;
background-color: var(--header-bg-color);
position: relative; /* 防止被表单覆盖底部 */
z-index: calc(var(--el-index-normal) + 1);
}
.v-dialog.el-dialog {
box-sizing: border-box;
margin: 15dvh auto;
}
.v-dialog.el-dialog.is-fullscreen {
box-sizing: border-box;
margin: auto;
}
.v-dialog.el-dialog .el-dialog__header {
padding: 10px 16px;
/*border-bottom: 1px solid var(--el-border-color);*/
margin-right: 0;
box-sizing: border-box;
background-color: var(--header-bg-color);
}
.v-dialog.el-dialog--center .el-dialog__body,
.el-dialog__body {
padding: 16px 20px;
box-sizing: border-box;
}
.v-dialog.el-dialog.is-fullscreen .el-dialog__body {
height: calc(100dvh - 44px - 52px);
max-height: calc(100dvh - 44px - 52px);
}
.v-dialog.el-dialog .el-dialog__body {
height: calc(70dvh - 44px - 52px);
}
.v-dialog.el-dialog.flex-body:not(.is-fullscreen) .el-dialog__body {
height: initial;
max-height: calc(70dvh - 44px - 52px);
}

View File

@ -0,0 +1,47 @@
export interface DialogProps {
/**
*
*/
title: string;
/**
*
*/
modelValue: boolean;
/**
*
*/
initFullScreen?: boolean | undefined;
/**
*
*/
showFullScreen?: boolean;
/**
*
*/
fullScreen?: boolean | undefined;
confirmText?: string;
cancelText?: string;
loading?: boolean;
/**
* 使el-scrollbar包裹对话框body
*/
useBodyScrolling?: boolean;
/**
* body高度
*/
fixedBodyHeight?: boolean;
draggable?: boolean;
hiddenFooter?: boolean;
}
export type DialogEmits = {
"update:modelValue": [val: boolean];
"update:fullScreen": [val: boolean];
confirm: [];
cancel: [];
};

View File

@ -21,6 +21,7 @@ import "element-plus/dist/index.css";
// 导入字体图标
import "./assets/iconfont/iconfont.js";
import "./assets/iconfont/iconfont.css";
import "@/components/VDialog/dialog.css";
const app = createApp(App);

View File

@ -42,5 +42,5 @@ export type userType = {
/** 字典ListMap 用于下拉框直接展示 */
dictionaryList: Map<String, Array<DictionaryData>>;
/** 字典MapMap 用于匹配值展示 */
dictionaryMap: Map<String, Map<String, DictionaryData>>;
dictionaryMap: Record<string, Record<string, DictionaryData>>;
};

View File

@ -205,3 +205,25 @@ export const handleTree = (
}
return tree;
};
export interface Tree {
children?: this[];
}
export function toTree<T extends Tree>(
src: T[],
keyField: keyof T,
parentField: keyof T
): T[] {
const map = new Map<unknown, T>(src.map(it => [it[keyField], it]));
src.forEach(it => {
if (map.has(it[parentField])) {
const parent = map.get(it[parentField])!;
if (!parent.children) {
parent.children = [];
}
parent.children.push(it);
}
});
return src.filter(it => !it[parentField]);
}

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref } from "vue";
import { useLoginLogHook } from "./utils/hook";
import { usePostHook } from "./utils/hook";
import { PureTableBar } from "@/components/RePureTableBar";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
@ -10,14 +10,17 @@ import Refresh from "@iconify-icons/ep/refresh";
import { useUserStoreHook } from "@/store/modules/user";
// TODO
import { CommonUtils } from "@/utils/common";
import PostFormModal from "@/views/system/post/post-form-modal.vue";
import EditPen from "@iconify-icons/ep/edit-pen";
import { PostPageResponse } from "@/api/system/post";
import AddFill from "@iconify-icons/ri/add-circle-line";
/** 组件name最好和菜单表中的router_name一致 */
defineOptions({
name: "SystemOperationLog"
name: "Post"
});
const loginLogStatusList =
useUserStoreHook().dictionaryList["sysLoginLog.status"];
const loginLogStatusList = useUserStoreHook().dictionaryList["common.status"];
const tableRef = ref();
@ -33,11 +36,21 @@ const {
multipleSelection,
onSearch,
resetForm,
onSortChanged,
exportAllExcel,
getLoginLogList,
getPostList,
handleDelete,
handleBulkDelete
} = useLoginLogHook();
} = usePostHook();
const opType = ref<"add" | "update">("add");
const modalVisible = ref(false);
const opRow = ref<PostPageResponse>();
function openDialog(type: "add" | "update", row?: PostPageResponse) {
opType.value = type;
opRow.value = row;
modalVisible.value = true;
}
</script>
<template>
@ -49,18 +62,18 @@ const {
:model="searchFormParams"
class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]"
>
<el-form-item label="登录IP" prop="ipAddress">
<el-form-item label="岗位编码" prop="postCode">
<el-input
v-model="searchFormParams.ipAddress"
placeholder="请输入IP地址"
v-model="searchFormParams.postCode"
placeholder="请输入岗位编码"
clearable
class="!w-[200px]"
/>
</el-form-item>
<el-form-item label="用户名:" prop="username">
<el-form-item label="岗位名称" prop="postName">
<el-input
v-model="searchFormParams.username"
placeholder="请选择用户名称"
v-model="searchFormParams.postName"
placeholder="请选择岗位名称"
clearable
class="!w-[200px]"
/>
@ -81,11 +94,7 @@ const {
/>
</el-select>
</el-form-item>
<el-form-item>
<label class="el-form-item__label is-required font-bold"
>登录时间</label
>
<!-- TODO 如何消除这个v-model的warning -->
<el-form-item label="创建时间">
<el-date-picker
class="!w-[240px]"
v-model="timeRange"
@ -115,9 +124,16 @@ const {
</el-form>
<!-- table bar 包裹 table -->
<PureTableBar title="登录日志列表" :columns="columns" @refresh="onSearch">
<PureTableBar title="岗位列表" :columns="columns" @refresh="onSearch">
<!-- 表格操作栏 -->
<template #buttons>
<el-button
type="primary"
:icon="useRenderIcon(AddFill)"
@click="openDialog('add')"
>
新增岗位
</el-button>
<el-button
type="danger"
:icon="useRenderIcon(Delete)"
@ -127,7 +143,7 @@ const {
</el-button>
<el-button
type="primary"
@click="CommonUtils.exportExcel(columns, dataList, '登录日志列表')"
@click="CommonUtils.exportExcel(columns, dataList, '岗位列表')"
>单页导出</el-button
>
<el-button type="primary" @click="exportAllExcel">全部导出</el-button>
@ -151,16 +167,26 @@ const {
background: 'var(--el-table-row-hover-bg-color)',
color: 'var(--el-text-color-primary)'
}"
@page-size-change="getLoginLogList"
@page-current-change="getLoginLogList"
@sort-change="getLoginLogList"
@page-size-change="getPostList"
@page-current-change="getPostList"
@sort-change="onSortChanged"
@selection-change="
rows => (multipleSelection = rows.map(item => item.logId))
rows => (multipleSelection = rows.map(item => item.postId))
"
>
<template #operation="{ row }">
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(EditPen)"
@click="openDialog('update', row)"
>
编辑
</el-button>
<el-popconfirm
:title="`是否确认删除编号为${row.logId}的这条日志`"
:title="`是否确认删除编号为${row.postId}的这个岗位`"
@confirm="handleDelete(row)"
>
<template #reference>
@ -179,6 +205,13 @@ const {
</pure-table>
</template>
</PureTableBar>
<post-form-modal
v-model="modalVisible"
:type="opType"
:row="opRow"
@success="onSearch"
/>
</div>
</template>

View File

@ -0,0 +1,131 @@
<script setup lang="ts">
import VDialog from "@/components/VDialog/VDialog.vue";
import { computed, reactive, ref } from "vue";
import {
AddPostCommand,
PostPageResponse,
UpdatePostCommand,
addPostApi,
updatePostApi
} from "@/api/system/post";
import { useUserStoreHook } from "@/store/modules/user";
import { ElMessage, FormInstance, FormRules } from "element-plus";
interface Props {
type: "add" | "update";
modelValue: boolean;
row?: PostPageResponse;
}
const props = defineProps<Props>();
const emits = defineEmits<{
(e: "update:modelValue", v: boolean): void;
(e: "success"): void;
}>();
const visible = computed({
get: () => props.modelValue,
set(v) {
emits("update:modelValue", v);
}
});
const formData = reactive<AddPostCommand | UpdatePostCommand>({
postId: 0,
postCode: "",
postName: "",
postSort: 1,
remark: "",
status: ""
});
const statusList = useUserStoreHook().dictionaryMap["common.status"];
const rules: FormRules = {
postName: [
{
required: true,
message: "岗位名称不能为空"
}
],
postCode: [
{
required: true,
message: "岗位编码不能为空"
}
],
postSort: [
{
required: true,
message: "岗位序号不能为空"
}
]
};
const formRef = ref<FormInstance>();
function handleOpened() {
if (props.row) {
Object.assign(formData, props.row);
} else {
formRef.value?.resetFields();
}
}
const loading = ref(false);
async function handleConfirm() {
try {
loading.value = true;
if (props.type === "add") {
await addPostApi(formData);
} else if (props.type === "update") {
await updatePostApi(formData as UpdatePostCommand);
}
ElMessage.info("提交成功");
visible.value = false;
emits("success");
} catch (e) {
console.error(e);
ElMessage.error((e as Error)?.message || "提交失败");
} finally {
loading.value = false;
}
}
</script>
<template>
<v-dialog
show-full-screen
:fixed-body-height="false"
use-body-scrolling
:title="type === 'add' ? '新增岗位' : '更新岗位'"
v-model="visible"
:loading="loading"
@confirm="handleConfirm"
@cancel="visible = false"
@opened="handleOpened"
>
<el-form :model="formData" label-width="120px" :rules="rules" ref="formRef">
<el-form-item prop="postName" label="岗位名称" required inline-message>
<el-input v-model="formData.postName" />
</el-form-item>
<el-form-item prop="postCode" label="岗位编码" required>
<el-input v-model="formData.postCode" />
</el-form-item>
<el-form-item prop="postSort" label="岗位顺序" required>
<el-input-number :min="1" v-model="formData.postSort" />
</el-form-item>
<el-form-item prop="status" label="岗位状态">
<el-radio-group v-model="formData.status">
<el-radio
v-for="item in Object.keys(statusList)"
:key="item"
:label="statusList[item].value"
>{{ statusList[item].label }}</el-radio
>
</el-radio-group>
</el-form-item>
<el-form-item prop="remark" label="备注" style="margin-bottom: 0">
<el-input type="textarea" v-model="formData.remark" />
</el-form-item>
</el-form>
</v-dialog>
</template>

View File

@ -1,23 +1,22 @@
import dayjs from "dayjs";
import { message } from "@/utils/message";
import { ElMessageBox, Sort } from "element-plus";
import {
getLoginLogListApi,
deleteLoginLogApi,
exportLoginLogExcelApi,
LoginLogQuery
} from "@/api/system/log";
import { reactive, ref, onMounted, toRaw } from "vue";
import { reactive, ref, onMounted, toRaw, computed } from "vue";
import { useUserStoreHook } from "@/store/modules/user";
import { CommonUtils } from "@/utils/common";
import { PaginationProps } from "@pureadmin/table";
import {
PostListCommand,
getPostListApi,
exportPostExcelApi,
deletePostApi
} from "@/api/system/post";
const loginLogStatusMap =
useUserStoreHook().dictionaryMap["sysLoginLog.status"];
const statusMap = useUserStoreHook().dictionaryMap["common.status"];
export function useLoginLogHook() {
export function usePostHook() {
const defaultSort: Sort = {
prop: "loginTime",
prop: "createTime",
order: "descending"
};
@ -28,20 +27,35 @@ export function useLoginLogHook() {
background: true
};
const timeRange = ref([]);
const timeRange = computed<[string, string] | null>({
get() {
if (searchFormParams.beginTime && searchFormParams.endTime) {
return [searchFormParams.beginTime, searchFormParams.endTime];
} else {
return null;
}
},
set(v) {
if (v?.length === 2) {
searchFormParams.beginTime = v[0];
searchFormParams.endTime = v[1];
} else {
searchFormParams.beginTime = undefined;
searchFormParams.endTime = undefined;
}
}
});
const searchFormParams = reactive<LoginLogQuery>({
ipAddress: undefined,
username: undefined,
status: undefined,
beginTime: undefined,
endTime: undefined,
timeRangeColumn: defaultSort.prop
const searchFormParams = reactive<PostListCommand>({
postCode: "",
postName: "",
status: undefined
});
const dataList = ref([]);
const pageLoading = ref(true);
const multipleSelection = ref([]);
const sortState = ref<Sort>(defaultSort);
const columns: TableColumnList = [
{
@ -49,34 +63,23 @@ export function useLoginLogHook() {
align: "left"
},
{
label: "日志编号",
prop: "logId",
label: "岗位编号",
prop: "postId",
minWidth: 100
},
{
label: "用户名",
prop: "username",
minWidth: 120,
sortable: "custom"
},
{
label: "IP地址",
prop: "ipAddress",
label: "岗位编码",
prop: "postCode",
minWidth: 120
},
{
label: "登录地点",
prop: "loginLocation",
label: "岗位名称",
prop: "postName",
minWidth: 120
},
{
label: "操作系统",
prop: "operationSystem",
minWidth: 120
},
{
label: "浏览器",
prop: "browser",
label: "岗位排序",
prop: "postSort",
minWidth: 120
},
{
@ -86,26 +89,20 @@ export function useLoginLogHook() {
cellRenderer: ({ row, props }) => (
<el-tag
size={props.size}
type={loginLogStatusMap[row.status].cssTag}
type={statusMap[row.status].cssTag}
effect="plain"
>
{loginLogStatusMap[row.status].label}
{statusMap[row.status].label}
</el-tag>
)
},
{
label: "状态名",
prop: "statusStr",
minWidth: 120,
hide: true
},
{
label: "登录时间",
label: "创建时间",
minWidth: 160,
prop: "loginTime",
prop: "createTime",
sortable: "custom",
formatter: ({ loginTime }) =>
dayjs(loginTime).format("YYYY-MM-DD HH:mm:ss")
formatter: ({ createTime }) =>
dayjs(createTime).format("YYYY-MM-DD HH:mm:ss")
},
{
label: "操作",
@ -115,10 +112,15 @@ export function useLoginLogHook() {
}
];
function onSortChanged(sort: Sort) {
sortState.value = sort;
onSearch();
}
async function onSearch() {
// 点击搜索的时候 需要重置分页
pagination.currentPage = 1;
getLoginLogList();
getPostList();
}
function resetForm(formEl, tableRef) {
@ -130,7 +132,6 @@ export function useLoginLogHook() {
searchFormParams.orderDirection = undefined;
// 清空时间查询 TODO 这块有点繁琐 有可以优化的地方吗?
// Form组件的resetFields方法无法清除datepicker里面的数据。
timeRange.value = [];
searchFormParams.beginTime = undefined;
searchFormParams.endTime = undefined;
tableRef.getTableRef().clearSort();
@ -138,15 +139,14 @@ export function useLoginLogHook() {
onSearch();
}
async function getLoginLogList(sort: Sort = defaultSort) {
async function getPostList(sort: Sort = defaultSort) {
pageLoading.value = true;
if (sort != null) {
CommonUtils.fillSortParams(searchFormParams, sort);
}
CommonUtils.fillPaginationParams(searchFormParams, pagination);
CommonUtils.fillTimeRangeParams(searchFormParams, timeRange.value);
const { data } = await getLoginLogListApi(toRaw(searchFormParams)).finally(
const { data } = await getPostListApi(toRaw(searchFormParams)).finally(
() => {
pageLoading.value = false;
}
@ -155,23 +155,23 @@ export function useLoginLogHook() {
pagination.total = data.total;
}
async function exportAllExcel(sort: Sort = defaultSort) {
if (sort != null) {
CommonUtils.fillSortParams(searchFormParams, sort);
async function exportAllExcel() {
if (sortState.value != null) {
CommonUtils.fillSortParams(searchFormParams, sortState.value);
}
CommonUtils.fillPaginationParams(searchFormParams, pagination);
CommonUtils.fillTimeRangeParams(searchFormParams, timeRange.value);
exportLoginLogExcelApi(toRaw(searchFormParams), "登录日志.xls");
exportPostExcelApi(toRaw(searchFormParams), "岗位数据.xls");
}
async function handleDelete(row) {
await deleteLoginLogApi([row.logId]).then(() => {
await deletePostApi([row.logId]).then(() => {
message(`您删除了操作编号为${row.logId}的这条数据`, {
type: "success"
});
// 刷新列表
getLoginLogList();
getPostList();
});
}
@ -193,12 +193,12 @@ export function useLoginLogHook() {
}
)
.then(async () => {
await deleteLoginLogApi(multipleSelection.value).then(() => {
await deletePostApi(multipleSelection.value).then(() => {
message(`您删除了日志编号为[ ${multipleSelection.value} ]的数据`, {
type: "success"
});
// 刷新列表
getLoginLogList();
getPostList();
});
})
.catch(() => {
@ -210,9 +210,7 @@ export function useLoginLogHook() {
});
}
onMounted(() => {
getLoginLogList();
});
onMounted(getPostList);
return {
searchFormParams,
@ -224,9 +222,10 @@ export function useLoginLogHook() {
timeRange,
multipleSelection,
onSearch,
onSortChanged,
exportAllExcel,
// exportExcel,
getLoginLogList,
getPostList,
resetForm,
handleDelete,
handleBulkDelete

View File

@ -1,55 +0,0 @@
<script setup lang="ts">
import { ref } from "vue";
import { formRules } from "./utils/rule";
import { FormProps } from "./utils/types";
const props = withDefaults(defineProps<FormProps>(), {
formInline: () => ({
name: "",
code: "",
remark: ""
})
});
const ruleFormRef = ref();
const newFormInline = ref(props.formInline);
function getRef() {
return ruleFormRef.value;
}
defineExpose({ getRef });
</script>
<template>
<el-form
ref="ruleFormRef"
:model="newFormInline"
:rules="formRules"
label-width="82px"
>
<el-form-item label="角色名称" prop="name">
<el-input
v-model="newFormInline.name"
clearable
placeholder="请输入角色名称"
/>
</el-form-item>
<el-form-item label="角色标识" prop="code">
<el-input
v-model="newFormInline.code"
clearable
placeholder="请输入角色标识"
/>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="newFormInline.remark"
placeholder="请输入备注信息"
type="textarea"
/>
</el-form-item>
</el-form>
</template>

View File

@ -4,17 +4,17 @@ import { useRole } from "./utils/hook";
import { PureTableBar } from "@/components/RePureTableBar";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
// import Database from "@iconify-icons/ri/database-2-line";
// import More from "@iconify-icons/ep/more-filled";
import Delete from "@iconify-icons/ep/delete";
import EditPen from "@iconify-icons/ep/edit-pen";
import Search from "@iconify-icons/ep/search";
import Refresh from "@iconify-icons/ep/refresh";
import Menu from "@iconify-icons/ep/menu";
import AddFill from "@iconify-icons/ri/add-circle-line";
import { getRoleInfoApi, RoleDTO } from "@/api/system/role";
import RoleFormModal from "@/views/system/role/role-form-modal.vue";
import { ElMessage } from "element-plus";
defineOptions({
name: "Role"
name: "SystemRole"
});
const formRef = ref();
@ -24,19 +24,34 @@ const {
columns,
dataList,
pagination,
// buttonClass,
onSearch,
resetForm,
openDialog,
handleMenu,
handleDelete,
// handleDatabase,
handleSizeChange,
handleCurrentChange,
handleSelectionChange
menuTree,
getMenuTree,
handleDelete
} = useRole();
</script>
const opType = ref<"add" | "update">("add");
const modalVisible = ref(false);
const opRow = ref<RoleDTO>();
async function openDialog(type: "add" | "update", row?: RoleDTO) {
debugger;
try {
await getMenuTree();
if (row) {
const { data } = await getRoleInfoApi(row.roleId);
row.selectedMenuList = data.selectedMenuList;
row.selectedDeptList = data.selectedDeptList;
}
} catch (e) {
console.error(e);
ElMessage.error((e as Error)?.message || "加载菜单失败");
}
opType.value = type;
opRow.value = row;
modalVisible.value = true;
}
</script>
<template>
<div class="main">
<el-form
@ -47,7 +62,7 @@ const {
>
<el-form-item label="角色名称:" prop="name">
<el-input
v-model="form.name"
v-model="form.roleName"
placeholder="请输入角色名称"
clearable
class="!w-[200px]"
@ -55,7 +70,7 @@ const {
</el-form-item>
<el-form-item label="角色标识:" prop="code">
<el-input
v-model="form.code"
v-model="form.roleKey"
placeholder="请输入角色标识"
clearable
class="!w-[180px]"
@ -96,7 +111,7 @@ const {
<el-button
type="primary"
:icon="useRenderIcon(AddFill)"
@click="openDialog()"
@click="openDialog('add')"
>
新增角色
</el-button>
@ -129,22 +144,12 @@ const {
type="primary"
:size="size"
:icon="useRenderIcon(EditPen)"
@click="openDialog('编辑', row)"
@click="openDialog('update', row)"
>
修改
</el-button>
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(Menu)"
@click="handleMenu"
>
菜单权限
</el-button>
<el-popconfirm
:title="`是否确认删除角色名称为${row.name}的这条数据`"
:title="`是否确认删除角色名称为${row.roleName}的这条数据`"
@confirm="handleDelete(row)"
>
<template #reference>
@ -200,6 +205,13 @@ const {
</pure-table>
</template>
</PureTableBar>
<role-form-modal
v-model="modalVisible"
:type="opType"
:row="opRow"
:menu-options="menuTree"
/>
</div>
</template>

View File

@ -0,0 +1,158 @@
<script setup lang="ts">
import VDialog from "@/components/VDialog/VDialog.vue";
import { computed, reactive, ref } from "vue";
import { useUserStoreHook } from "@/store/modules/user";
import { ElMessage, FormInstance, FormRules } from "element-plus";
import {
AddRoleCommand,
RoleDTO,
UpdateRoleCommand,
addRoleApi,
updateRoleApi
} from "@/api/system/role";
import { MenuDTO } from "@/api/system/menu";
interface Props {
type: "add" | "update";
modelValue: boolean;
row?: RoleDTO;
menuOptions: MenuDTO[];
}
const props = defineProps<Props>();
const emits = defineEmits<{
(e: "update:modelValue", v: boolean): void;
(e: "success"): void;
}>();
const visible = computed({
get: () => props.modelValue,
set(v) {
emits("update:modelValue", v);
}
});
const formData = reactive<AddRoleCommand | UpdateRoleCommand>({
roleId: 0,
dataScope: "",
menuIds: [],
remark: "",
roleKey: "",
roleName: "",
roleSort: 1,
status: ""
});
const statusList = useUserStoreHook().dictionaryMap["common.status"];
const rules: FormRules = {
roleName: [
{
required: true,
message: "角色名称不能为空"
}
],
roleKey: [
{
required: true,
message: "权限标识不能为空"
}
],
roleSort: [
{
required: true,
message: "角色序号不能为空"
}
]
};
const formRef = ref<FormInstance>();
function handleOpened() {
console.log("opened", props.row);
if (props.row) {
Object.assign(formData, props.row);
formData.menuIds = props.row.selectedMenuList;
} else {
formRef.value?.resetFields();
}
}
const treeRef = ref<InstanceType<typeof ElTree>>();
function handleCheckChange() {
formData.menuIds = treeRef.value.getCheckedKeys(false) as number[];
}
const loading = ref(false);
async function handleConfirm() {
try {
loading.value = true;
if (props.type === "add") {
await addRoleApi(formData);
} else if (props.type === "update") {
await updateRoleApi(formData as UpdateRoleCommand);
}
ElMessage.info("提交成功");
visible.value = false;
emits("success");
} catch (e) {
console.error(e);
ElMessage.error((e as Error)?.message || "提交失败");
} finally {
loading.value = false;
}
}
</script>
<template>
<v-dialog
show-full-screen
fixed-body-height
use-body-scrolling
:title="type === 'add' ? '新增角色' : '更新角色'"
v-model="visible"
:loading="loading"
@confirm="handleConfirm"
@cancel="visible = false"
@opened="handleOpened"
>
<el-form :model="formData" label-width="120px" :rules="rules" ref="formRef">
<el-form-item prop="roleName" label="角色名称" required inline-message>
<el-input v-model="formData.roleName" />
</el-form-item>
<el-form-item prop="roleKey" label="权限字符" required>
<el-input v-model="formData.roleKey" />
</el-form-item>
<el-form-item prop="roleSort" label="角色顺序" required>
<el-input-number :min="1" v-model="formData.roleSort" />
</el-form-item>
<el-form-item prop="status" label="角色状态">
<el-radio-group v-model="formData.status">
<el-radio
v-for="item in Object.keys(statusList)"
:key="item"
:label="statusList[item].value"
>{{ statusList[item].label }}</el-radio
>
</el-radio-group>
</el-form-item>
<el-form-item label="菜单权限" prop="menuIds">
<el-tree
ref="treeRef"
:props="{ label: 'menuName', children: 'children' }"
:data="props.menuOptions"
node-key="id"
check-strictly
show-checkbox
default-expand-all
check-on-click-node
:expand-on-click-node="false"
:default-checked-keys="formData.menuIds"
@check-change="handleCheckChange"
style="width: 100%"
/>
</el-form-item>
<el-form-item prop="remark" label="备注" style="margin-bottom: 0">
<el-input type="textarea" v-model="formData.remark" />
</el-form-item>
</el-form>
</v-dialog>
</template>

View File

@ -1,21 +1,24 @@
import dayjs from "dayjs";
import editForm from "../form.vue";
import { message } from "@/utils/message";
import { getRoleList } from "@/api/system";
import { ElMessageBox } from "element-plus";
import {
deleteRoleApi,
getRoleListApi,
RoleDTO,
RoleQuery
} from "@/api/system/role";
import { getMenuListApi, MenuDTO } from "@/api/system/menu";
import { ElMessage, ElMessageBox } from "element-plus";
import { usePublicHooks } from "../../hooks";
import { addDialog } from "@/components/ReDialog";
import { type FormItemProps } from "../utils/types";
import { type PaginationProps } from "@pureadmin/table";
import { reactive, ref, onMounted, h, toRaw } from "vue";
import { reactive, ref, onMounted, toRaw } from "vue";
import { toTree } from "@/utils/tree";
export function useRole() {
const form = reactive({
name: "",
code: "",
status: ""
const form = reactive<RoleQuery>({
roleKey: "",
roleName: "",
status: undefined
});
const formRef = ref();
const dataList = ref([]);
const loading = ref(true);
const switchLoadMap = ref({});
@ -29,17 +32,17 @@ export function useRole() {
const columns: TableColumnList = [
{
label: "角色编号",
prop: "id",
prop: "roleId",
minWidth: 100
},
{
label: "角色名称",
prop: "name",
prop: "roleName",
minWidth: 120
},
{
label: "角色标识",
prop: "code",
prop: "roleKey",
minWidth: 150
},
{
@ -131,34 +134,33 @@ export function useRole() {
});
}
function handleDelete(row) {
message(`您删除了角色名称为${row.name}的这条数据`, { type: "success" });
onSearch();
}
function handleSizeChange(val: number) {
console.log(`${val} items per page`);
}
function handleCurrentChange(val: number) {
console.log(`current page: ${val}`);
}
function handleSelectionChange(val) {
console.log("handleSelectionChange", val);
async function handleDelete(row: RoleDTO) {
try {
loading.value = true;
await deleteRoleApi(row.roleId);
message(`您删除了角色名称为${row.roleName}的这条数据`, { type: "info" });
onSearch();
} catch (e) {
console.error(e);
message((e as Error)?.message || "删除失败", { type: "error" });
} finally {
loading.value = false;
}
}
async function onSearch() {
loading.value = true;
const { data } = await getRoleList(toRaw(form));
dataList.value = data.list;
pagination.total = data.total;
pagination.pageSize = data.pageSize;
pagination.currentPage = data.currentPage;
setTimeout(() => {
try {
loading.value = true;
const { data } = await getRoleListApi(toRaw(form));
console.log("role list", data);
dataList.value = data.rows;
pagination.total = data.total;
} catch (e) {
console.error(e);
ElMessage.error((e as Error)?.message || "加载失败");
} finally {
loading.value = false;
}, 500);
}
}
const resetForm = formEl => {
@ -167,59 +169,22 @@ export function useRole() {
onSearch();
};
function openDialog(title = "新增", row?: FormItemProps) {
addDialog({
title: `${title}角色`,
props: {
formInline: {
name: row?.name ?? "",
code: row?.code ?? "",
remark: row?.remark ?? ""
}
},
width: "40%",
draggable: true,
fullscreenIcon: true,
closeOnClickModal: false,
contentRenderer: () => h(editForm, { ref: formRef }),
beforeSure: (done, { options }) => {
const FormRef = formRef.value.getRef();
const curData = options.props.formInline as FormItemProps;
function chores() {
message(`${title}了角色名称为${curData.name}的这条数据`, {
type: "success"
});
done(); // 关闭弹框
onSearch(); // 刷新表格数据
}
FormRef.validate(valid => {
if (valid) {
console.log("curData", curData);
// 表单规则校验通过
if (title === "新增") {
// 实际开发先调用新增接口,再进行下面操作
chores();
} else {
// 实际开发先调用编辑接口,再进行下面操作
chores();
}
}
});
}
});
}
const menuTree = ref<MenuDTO[]>([]);
/** 菜单权限 */
function handleMenu() {
message("等菜单管理页面开发后完善");
async function getMenuTree() {
if (menuTree.value?.length) {
return menuTree.value;
}
const { data } = await getMenuListApi({ isButton: false });
console.log("menu data", data);
menuTree.value = toTree(data, "id", "parentId");
return menuTree.value;
}
/** 数据权限 可自行开发 */
// function handleDatabase() {}
onMounted(() => {
onSearch();
});
onMounted(onSearch);
return {
form,
@ -227,15 +192,10 @@ export function useRole() {
columns,
dataList,
pagination,
// buttonClass,
onSearch,
resetForm,
openDialog,
handleMenu,
handleDelete,
// handleDatabase,
handleSizeChange,
handleCurrentChange,
handleSelectionChange
menuTree,
getMenuTree,
handleDelete
};
}

View File

@ -1,8 +0,0 @@
import { reactive } from "vue";
import type { FormRules } from "element-plus";
/** 自定义表单规则校验 */
export const formRules = reactive(<FormRules>{
name: [{ required: true, message: "角色名称为必填项", trigger: "blur" }],
code: [{ required: true, message: "角色标识为必填项", trigger: "blur" }]
});

View File

@ -1,15 +0,0 @@
// 虽然字段很少 但是抽离出来 后续有扩展字段需求就很方便了
interface FormItemProps {
/** 角色名称 */
name: string;
/** 角色编号 */
code: string;
/** 备注 */
remark: string;
}
interface FormProps {
formInline: FormItemProps;
}
export type { FormItemProps, FormProps };