perf: 系统管理
@ -33,6 +33,16 @@ menus:
|
|||||||
permissionPage: Page Permission
|
permissionPage: Page Permission
|
||||||
permissionButton: Button Permission
|
permissionButton: Button Permission
|
||||||
hsAbout: About
|
hsAbout: About
|
||||||
|
hssysManagement: System Manage
|
||||||
|
hsUser: User Manage
|
||||||
|
hsRole: Role Manage
|
||||||
|
hsSystemMenu: Menu Manage
|
||||||
|
hsDept: Dept Manage
|
||||||
|
hssysMonitor: System Monitor
|
||||||
|
hsOnlineUser: Online User
|
||||||
|
hsLoginLog: Login Log
|
||||||
|
hsOperationLog: Operation Log
|
||||||
|
hsSystemLog: System Log
|
||||||
status:
|
status:
|
||||||
hsLoad: Loading...
|
hsLoad: Loading...
|
||||||
login:
|
login:
|
||||||
|
@ -33,6 +33,16 @@ menus:
|
|||||||
permissionPage: 页面权限
|
permissionPage: 页面权限
|
||||||
permissionButton: 按钮权限
|
permissionButton: 按钮权限
|
||||||
hsAbout: 关于
|
hsAbout: 关于
|
||||||
|
hssysManagement: 系统管理
|
||||||
|
hsUser: 用户管理
|
||||||
|
hsRole: 角色管理
|
||||||
|
hsSystemMenu: 菜单管理
|
||||||
|
hsDept: 部门管理
|
||||||
|
hssysMonitor: 系统监控
|
||||||
|
hsOnlineUser: 在线用户
|
||||||
|
hsLoginLog: 登录日志
|
||||||
|
hsOperationLog: 操作日志
|
||||||
|
hsSystemLog: 系统日志
|
||||||
status:
|
status:
|
||||||
hsLoad: 加载中...
|
hsLoad: 加载中...
|
||||||
login:
|
login:
|
||||||
|
@ -1,11 +1,111 @@
|
|||||||
// 模拟后端动态生成路由
|
// 模拟后端动态生成路由
|
||||||
import { defineFakeRoute } from "vite-plugin-fake-server/client";
|
import { defineFakeRoute } from "vite-plugin-fake-server/client";
|
||||||
|
import { system, monitor } from "@/router/enums";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* roles:页面级别权限,这里模拟二种 "admin"、"common"
|
* roles:页面级别权限,这里模拟二种 "admin"、"common"
|
||||||
* admin:管理员角色
|
* admin:管理员角色
|
||||||
* common:普通角色
|
* common:普通角色
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const systemManagementRouter = {
|
||||||
|
path: "/system",
|
||||||
|
meta: {
|
||||||
|
icon: "ri:settings-3-line",
|
||||||
|
title: "menus.hssysManagement",
|
||||||
|
rank: system
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/system/user/index",
|
||||||
|
name: "SystemUser",
|
||||||
|
meta: {
|
||||||
|
icon: "ri:admin-line",
|
||||||
|
title: "menus.hsUser",
|
||||||
|
roles: ["admin"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/system/role/index",
|
||||||
|
name: "SystemRole",
|
||||||
|
meta: {
|
||||||
|
icon: "ri:admin-fill",
|
||||||
|
title: "menus.hsRole",
|
||||||
|
roles: ["admin"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/system/menu/index",
|
||||||
|
name: "SystemMenu",
|
||||||
|
meta: {
|
||||||
|
icon: "ep:menu",
|
||||||
|
title: "menus.hsSystemMenu",
|
||||||
|
roles: ["admin"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/system/dept/index",
|
||||||
|
name: "SystemDept",
|
||||||
|
meta: {
|
||||||
|
icon: "ri:git-branch-line",
|
||||||
|
title: "menus.hsDept",
|
||||||
|
roles: ["admin"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const systemMonitorRouter = {
|
||||||
|
path: "/monitor",
|
||||||
|
meta: {
|
||||||
|
icon: "ep:monitor",
|
||||||
|
title: "menus.hssysMonitor",
|
||||||
|
rank: monitor
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/monitor/online-user",
|
||||||
|
component: "monitor/online/index",
|
||||||
|
name: "OnlineUser",
|
||||||
|
meta: {
|
||||||
|
icon: "ri:user-voice-line",
|
||||||
|
title: "menus.hsOnlineUser",
|
||||||
|
roles: ["admin"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/monitor/login-logs",
|
||||||
|
component: "monitor/logs/login/index",
|
||||||
|
name: "LoginLog",
|
||||||
|
meta: {
|
||||||
|
icon: "ri:window-line",
|
||||||
|
title: "menus.hsLoginLog",
|
||||||
|
roles: ["admin"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/monitor/operation-logs",
|
||||||
|
component: "monitor/logs/operation/index",
|
||||||
|
name: "OperationLog",
|
||||||
|
meta: {
|
||||||
|
icon: "ri:history-fill",
|
||||||
|
title: "menus.hsOperationLog",
|
||||||
|
roles: ["admin"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/monitor/system-logs",
|
||||||
|
component: "monitor/logs/system/index",
|
||||||
|
name: "SystemLog",
|
||||||
|
meta: {
|
||||||
|
icon: "ri:file-search-line",
|
||||||
|
title: "menus.hsSystemLog",
|
||||||
|
roles: ["admin"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
const permissionRouter = {
|
const permissionRouter = {
|
||||||
path: "/permission",
|
path: "/permission",
|
||||||
meta: {
|
meta: {
|
||||||
@ -45,7 +145,7 @@ export default defineFakeRoute([
|
|||||||
response: () => {
|
response: () => {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: [permissionRouter]
|
data: [systemManagementRouter, systemMonitorRouter, permissionRouter]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,8 +53,10 @@
|
|||||||
"@pureadmin/utils": "^2.4.5",
|
"@pureadmin/utils": "^2.4.5",
|
||||||
"@vueuse/core": "^10.9.0",
|
"@vueuse/core": "^10.9.0",
|
||||||
"@vueuse/motion": "^2.1.0",
|
"@vueuse/motion": "^2.1.0",
|
||||||
|
"@zxcvbn-ts/core": "^3.0.4",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
|
"cropperjs": "^1.6.1",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"echarts": "^5.5.0",
|
"echarts": "^5.5.0",
|
||||||
"element-plus": "^2.6.0",
|
"element-plus": "^2.6.0",
|
||||||
|
17
pnpm-lock.yaml
generated
@ -20,12 +20,18 @@ dependencies:
|
|||||||
'@vueuse/motion':
|
'@vueuse/motion':
|
||||||
specifier: ^2.1.0
|
specifier: ^2.1.0
|
||||||
version: 2.1.0(rollup@2.79.1)(vue@3.4.21)
|
version: 2.1.0(rollup@2.79.1)(vue@3.4.21)
|
||||||
|
'@zxcvbn-ts/core':
|
||||||
|
specifier: ^3.0.4
|
||||||
|
version: 3.0.4
|
||||||
animate.css:
|
animate.css:
|
||||||
specifier: ^4.1.1
|
specifier: ^4.1.1
|
||||||
version: 4.1.1
|
version: 4.1.1
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.6.7
|
specifier: ^1.6.7
|
||||||
version: 1.6.7
|
version: 1.6.7
|
||||||
|
cropperjs:
|
||||||
|
specifier: ^1.6.1
|
||||||
|
version: 1.6.1
|
||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.10
|
specifier: ^1.11.10
|
||||||
version: 1.11.10
|
version: 1.11.10
|
||||||
@ -1931,6 +1937,12 @@ packages:
|
|||||||
uuid: 8.3.2
|
uuid: 8.3.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@zxcvbn-ts/core@3.0.4:
|
||||||
|
resolution: {integrity: sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw==}
|
||||||
|
dependencies:
|
||||||
|
fastest-levenshtein: 1.0.16
|
||||||
|
dev: false
|
||||||
|
|
||||||
/JSONStream@1.3.5:
|
/JSONStream@1.3.5:
|
||||||
resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==}
|
resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -2487,6 +2499,10 @@ packages:
|
|||||||
typescript: 5.3.3
|
typescript: 5.3.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/cropperjs@1.6.1:
|
||||||
|
resolution: {integrity: sha512-F4wsi+XkDHCOMrHMYjrTEE4QBOrsHHN5/2VsVAaRq8P7E5z7xQpT75S+f/9WikmBEailas3+yo+6zPIomW+NOA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/cross-spawn@7.0.3:
|
/cross-spawn@7.0.3:
|
||||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@ -3259,7 +3275,6 @@ packages:
|
|||||||
/fastest-levenshtein@1.0.16:
|
/fastest-levenshtein@1.0.16:
|
||||||
resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==}
|
resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==}
|
||||||
engines: {node: '>= 4.9.1'}
|
engines: {node: '>= 4.9.1'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/fastq@1.17.1:
|
/fastq@1.17.1:
|
||||||
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
|
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
|
||||||
|
70
src/api/system.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { http } from "@/utils/http";
|
||||||
|
|
||||||
|
type Result = {
|
||||||
|
success: boolean;
|
||||||
|
data?: Array<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResultTable = {
|
||||||
|
success: boolean;
|
||||||
|
data?: {
|
||||||
|
/** 列表数据 */
|
||||||
|
list: Array<any>;
|
||||||
|
/** 总条目数 */
|
||||||
|
total?: number;
|
||||||
|
/** 每页显示条目个数 */
|
||||||
|
pageSize?: number;
|
||||||
|
/** 当前页数 */
|
||||||
|
currentPage?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取系统管理-用户管理列表 */
|
||||||
|
export const getUserList = (data?: object) => {
|
||||||
|
return http.request<ResultTable>("post", "/user", { data });
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 系统管理-用户管理-获取所有角色列表 */
|
||||||
|
export const getAllRoleList = () => {
|
||||||
|
return http.request<Result>("get", "/list-all-role");
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 系统管理-用户管理-根据userId,获取对应角色id列表(userId:用户id) */
|
||||||
|
export const getRoleIds = (data?: object) => {
|
||||||
|
return http.request<Result>("post", "/list-role-ids", { data });
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取系统管理-角色管理列表 */
|
||||||
|
export const getRoleList = (data?: object) => {
|
||||||
|
return http.request<ResultTable>("post", "/role", { data });
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取系统管理-菜单管理列表 */
|
||||||
|
export const getMenuList = (data?: object) => {
|
||||||
|
return http.request<Result>("post", "/menu", { data });
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取系统管理-部门管理列表 */
|
||||||
|
export const getDeptList = (data?: object) => {
|
||||||
|
return http.request<Result>("post", "/dept", { data });
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取系统监控-在线用户列表 */
|
||||||
|
export const getOnlineLogsList = (data?: object) => {
|
||||||
|
return http.request<ResultTable>("post", "/online-logs", { data });
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取系统监控-登录日志列表 */
|
||||||
|
export const getLoginLogsList = (data?: object) => {
|
||||||
|
return http.request<ResultTable>("post", "/login-logs", { data });
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取系统监控-操作日志列表 */
|
||||||
|
export const getOperationLogsList = (data?: object) => {
|
||||||
|
return http.request<ResultTable>("post", "/operation-logs", { data });
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取系统监控-系统日志列表 */
|
||||||
|
export const getSystemLogsList = (data?: object) => {
|
||||||
|
return http.request<ResultTable>("post", "/system-logs", { data });
|
||||||
|
};
|
7
src/components/ReCropper/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import reCropper from "./src";
|
||||||
|
import { withInstall } from "@pureadmin/utils";
|
||||||
|
|
||||||
|
/** 图片裁剪组件 */
|
||||||
|
export const ReCropper = withInstall(reCropper);
|
||||||
|
|
||||||
|
export default ReCropper;
|
8
src/components/ReCropper/src/circled.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
@import "cropperjs/dist/cropper.css";
|
||||||
|
|
||||||
|
.re-circled {
|
||||||
|
.cropper-view-box,
|
||||||
|
.cropper-face {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
457
src/components/ReCropper/src/index.tsx
Normal file
@ -0,0 +1,457 @@
|
|||||||
|
import "./circled.css";
|
||||||
|
import Cropper from "cropperjs";
|
||||||
|
import { ElUpload } from "element-plus";
|
||||||
|
import type { CSSProperties } from "vue";
|
||||||
|
import { useEventListener } from "@vueuse/core";
|
||||||
|
import { longpress } from "@/directives/longpress";
|
||||||
|
import { useTippy, directive as tippy } from "vue-tippy";
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
unref,
|
||||||
|
computed,
|
||||||
|
type PropType,
|
||||||
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
|
defineComponent
|
||||||
|
} from "vue";
|
||||||
|
import {
|
||||||
|
delay,
|
||||||
|
debounce,
|
||||||
|
isArray,
|
||||||
|
downloadByBase64,
|
||||||
|
useResizeObserver
|
||||||
|
} from "@pureadmin/utils";
|
||||||
|
import {
|
||||||
|
Reload,
|
||||||
|
Upload,
|
||||||
|
ArrowH,
|
||||||
|
ArrowV,
|
||||||
|
ArrowUp,
|
||||||
|
ArrowDown,
|
||||||
|
ArrowLeft,
|
||||||
|
ChangeIcon,
|
||||||
|
ArrowRight,
|
||||||
|
RotateLeft,
|
||||||
|
SearchPlus,
|
||||||
|
RotateRight,
|
||||||
|
SearchMinus,
|
||||||
|
DownloadIcon
|
||||||
|
} from "./svg";
|
||||||
|
|
||||||
|
type Options = Cropper.Options;
|
||||||
|
|
||||||
|
const defaultOptions: Options = {
|
||||||
|
aspectRatio: 1,
|
||||||
|
zoomable: true,
|
||||||
|
zoomOnTouch: true,
|
||||||
|
zoomOnWheel: true,
|
||||||
|
cropBoxMovable: true,
|
||||||
|
cropBoxResizable: true,
|
||||||
|
toggleDragModeOnDblclick: true,
|
||||||
|
autoCrop: true,
|
||||||
|
background: true,
|
||||||
|
highlight: true,
|
||||||
|
center: true,
|
||||||
|
responsive: true,
|
||||||
|
restore: true,
|
||||||
|
checkCrossOrigin: true,
|
||||||
|
checkOrientation: true,
|
||||||
|
scalable: true,
|
||||||
|
modal: true,
|
||||||
|
guides: true,
|
||||||
|
movable: true,
|
||||||
|
rotatable: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
src: { type: String, required: true },
|
||||||
|
alt: { type: String },
|
||||||
|
circled: { type: Boolean, default: false },
|
||||||
|
/** 是否可以通过点击裁剪区域关闭右键弹出的功能菜单,默认 `true` */
|
||||||
|
isClose: { type: Boolean, default: true },
|
||||||
|
realTimePreview: { type: Boolean, default: true },
|
||||||
|
height: { type: [String, Number], default: "360px" },
|
||||||
|
crossorigin: {
|
||||||
|
type: String as PropType<"" | "anonymous" | "use-credentials" | undefined>,
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
|
||||||
|
options: { type: Object as PropType<Options>, default: () => ({}) }
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "ReCropper",
|
||||||
|
props,
|
||||||
|
setup(props, { attrs, emit }) {
|
||||||
|
const tippyElRef = ref<ElRef<HTMLImageElement>>();
|
||||||
|
const imgElRef = ref<ElRef<HTMLImageElement>>();
|
||||||
|
const cropper = ref<Nullable<Cropper>>();
|
||||||
|
const inCircled = ref(props.circled);
|
||||||
|
const isInClose = ref(props.isClose);
|
||||||
|
const inSrc = ref(props.src);
|
||||||
|
const isReady = ref(false);
|
||||||
|
const imgBase64 = ref();
|
||||||
|
|
||||||
|
let scaleX = 1;
|
||||||
|
let scaleY = 1;
|
||||||
|
|
||||||
|
const debounceRealTimeCroppered = debounce(realTimeCroppered, 80);
|
||||||
|
|
||||||
|
const getImageStyle = computed((): CSSProperties => {
|
||||||
|
return {
|
||||||
|
height: props.height,
|
||||||
|
maxWidth: "100%",
|
||||||
|
...props.imageStyle
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const getClass = computed(() => {
|
||||||
|
return [
|
||||||
|
attrs.class,
|
||||||
|
{
|
||||||
|
["re-circled"]: inCircled.value
|
||||||
|
}
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const iconClass = computed(() => {
|
||||||
|
return [
|
||||||
|
"p-[6px]",
|
||||||
|
"h-[30px]",
|
||||||
|
"w-[30px]",
|
||||||
|
"outline-none",
|
||||||
|
"rounded-[4px]",
|
||||||
|
"cursor-pointer",
|
||||||
|
"hover:bg-[rgba(0,0,0,0.06)]"
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const getWrapperStyle = computed((): CSSProperties => {
|
||||||
|
return { height: `${props.height}`.replace(/px/, "") + "px" };
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(init);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
cropper.value?.destroy();
|
||||||
|
isReady.value = false;
|
||||||
|
cropper.value = null;
|
||||||
|
imgBase64.value = "";
|
||||||
|
scaleX = 1;
|
||||||
|
scaleY = 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
useResizeObserver(tippyElRef, () => handCropper("reset"));
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
const imgEl = unref(imgElRef);
|
||||||
|
if (!imgEl) return;
|
||||||
|
cropper.value = new Cropper(imgEl, {
|
||||||
|
...defaultOptions,
|
||||||
|
ready: () => {
|
||||||
|
isReady.value = true;
|
||||||
|
realTimeCroppered();
|
||||||
|
delay(400).then(() => emit("readied", cropper.value));
|
||||||
|
},
|
||||||
|
crop() {
|
||||||
|
debounceRealTimeCroppered();
|
||||||
|
},
|
||||||
|
zoom() {
|
||||||
|
debounceRealTimeCroppered();
|
||||||
|
},
|
||||||
|
cropmove() {
|
||||||
|
debounceRealTimeCroppered();
|
||||||
|
},
|
||||||
|
...props.options
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function realTimeCroppered() {
|
||||||
|
props.realTimePreview && croppered();
|
||||||
|
}
|
||||||
|
|
||||||
|
function croppered() {
|
||||||
|
if (!cropper.value) return;
|
||||||
|
const canvas = inCircled.value
|
||||||
|
? getRoundedCanvas()
|
||||||
|
: cropper.value.getCroppedCanvas();
|
||||||
|
// https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLCanvasElement/toBlob
|
||||||
|
canvas.toBlob(blob => {
|
||||||
|
if (!blob) return;
|
||||||
|
const fileReader: FileReader = new FileReader();
|
||||||
|
fileReader.readAsDataURL(blob);
|
||||||
|
fileReader.onloadend = e => {
|
||||||
|
if (!e.target?.result || !blob) return;
|
||||||
|
imgBase64.value = e.target.result;
|
||||||
|
emit("cropper", {
|
||||||
|
base64: e.target.result,
|
||||||
|
blob,
|
||||||
|
info: { size: blob.size, ...cropper.value.getData() }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
fileReader.onerror = () => {
|
||||||
|
emit("error");
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRoundedCanvas() {
|
||||||
|
const sourceCanvas = cropper.value!.getCroppedCanvas();
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
const context = canvas.getContext("2d")!;
|
||||||
|
const width = sourceCanvas.width;
|
||||||
|
const height = sourceCanvas.height;
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
context.imageSmoothingEnabled = true;
|
||||||
|
context.drawImage(sourceCanvas, 0, 0, width, height);
|
||||||
|
context.globalCompositeOperation = "destination-in";
|
||||||
|
context.beginPath();
|
||||||
|
context.arc(
|
||||||
|
width / 2,
|
||||||
|
height / 2,
|
||||||
|
Math.min(width, height) / 2,
|
||||||
|
0,
|
||||||
|
2 * Math.PI,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
context.fill();
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handCropper(event: string, arg?: number | Array<number>) {
|
||||||
|
if (event === "scaleX") {
|
||||||
|
scaleX = arg = scaleX === -1 ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event === "scaleY") {
|
||||||
|
scaleY = arg = scaleY === -1 ? 1 : -1;
|
||||||
|
}
|
||||||
|
arg && isArray(arg)
|
||||||
|
? cropper.value?.[event]?.(...arg)
|
||||||
|
: cropper.value?.[event]?.(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function beforeUpload(file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
inSrc.value = "";
|
||||||
|
reader.onload = e => {
|
||||||
|
inSrc.value = e.target?.result as string;
|
||||||
|
};
|
||||||
|
reader.onloadend = () => {
|
||||||
|
init();
|
||||||
|
};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuContent = defineComponent({
|
||||||
|
directives: {
|
||||||
|
tippy,
|
||||||
|
longpress
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return () => (
|
||||||
|
<div class="flex flex-wrap w-[60px] justify-between">
|
||||||
|
<ElUpload
|
||||||
|
accept="image/*"
|
||||||
|
show-file-list={false}
|
||||||
|
before-upload={beforeUpload}
|
||||||
|
>
|
||||||
|
<Upload
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "上传",
|
||||||
|
placement: "left-start"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ElUpload>
|
||||||
|
<DownloadIcon
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "下载",
|
||||||
|
placement: "right-start"
|
||||||
|
}}
|
||||||
|
onClick={() => downloadByBase64(imgBase64.value, "cropping.png")}
|
||||||
|
/>
|
||||||
|
<ChangeIcon
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "圆形、矩形裁剪",
|
||||||
|
placement: "left-start"
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
inCircled.value = !inCircled.value;
|
||||||
|
realTimeCroppered();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Reload
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "重置",
|
||||||
|
placement: "right-start"
|
||||||
|
}}
|
||||||
|
onClick={() => handCropper("reset")}
|
||||||
|
/>
|
||||||
|
<ArrowUp
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "上移(可长按)",
|
||||||
|
placement: "left-start"
|
||||||
|
}}
|
||||||
|
v-longpress={[() => handCropper("move", [0, -10]), "0:100"]}
|
||||||
|
/>
|
||||||
|
<ArrowDown
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "下移(可长按)",
|
||||||
|
placement: "right-start"
|
||||||
|
}}
|
||||||
|
v-longpress={[() => handCropper("move", [0, 10]), "0:100"]}
|
||||||
|
/>
|
||||||
|
<ArrowLeft
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "左移(可长按)",
|
||||||
|
placement: "left-start"
|
||||||
|
}}
|
||||||
|
v-longpress={[() => handCropper("move", [-10, 0]), "0:100"]}
|
||||||
|
/>
|
||||||
|
<ArrowRight
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "右移(可长按)",
|
||||||
|
placement: "right-start"
|
||||||
|
}}
|
||||||
|
v-longpress={[() => handCropper("move", [10, 0]), "0:100"]}
|
||||||
|
/>
|
||||||
|
<ArrowH
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "水平翻转",
|
||||||
|
placement: "left-start"
|
||||||
|
}}
|
||||||
|
onClick={() => handCropper("scaleX", -1)}
|
||||||
|
/>
|
||||||
|
<ArrowV
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "垂直翻转",
|
||||||
|
placement: "right-start"
|
||||||
|
}}
|
||||||
|
onClick={() => handCropper("scaleY", -1)}
|
||||||
|
/>
|
||||||
|
<RotateLeft
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "逆时针旋转",
|
||||||
|
placement: "left-start"
|
||||||
|
}}
|
||||||
|
onClick={() => handCropper("rotate", -45)}
|
||||||
|
/>
|
||||||
|
<RotateRight
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "顺时针旋转",
|
||||||
|
placement: "right-start"
|
||||||
|
}}
|
||||||
|
onClick={() => handCropper("rotate", 45)}
|
||||||
|
/>
|
||||||
|
<SearchPlus
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "放大(可长按)",
|
||||||
|
placement: "left-start"
|
||||||
|
}}
|
||||||
|
v-longpress={[() => handCropper("zoom", 0.1), "0:100"]}
|
||||||
|
/>
|
||||||
|
<SearchMinus
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "缩小(可长按)",
|
||||||
|
placement: "right-start"
|
||||||
|
}}
|
||||||
|
v-longpress={[() => handCropper("zoom", -0.1), "0:100"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function onContextmenu(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const { show, setProps, destroy, state } = useTippy(tippyElRef, {
|
||||||
|
content: menuContent,
|
||||||
|
arrow: false,
|
||||||
|
theme: "light",
|
||||||
|
trigger: "manual",
|
||||||
|
interactive: true,
|
||||||
|
appendTo: "parent",
|
||||||
|
// hideOnClick: false,
|
||||||
|
placement: "bottom-end"
|
||||||
|
});
|
||||||
|
|
||||||
|
setProps({
|
||||||
|
getReferenceClientRect: () => ({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
top: event.clientY,
|
||||||
|
bottom: event.clientY,
|
||||||
|
left: event.clientX,
|
||||||
|
right: event.clientX
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
show();
|
||||||
|
|
||||||
|
if (isInClose.value) {
|
||||||
|
if (!state.value.isShown && !state.value.isVisible) return;
|
||||||
|
useEventListener(tippyElRef, "click", destroy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
inSrc,
|
||||||
|
props,
|
||||||
|
imgElRef,
|
||||||
|
tippyElRef,
|
||||||
|
getClass,
|
||||||
|
getWrapperStyle,
|
||||||
|
getImageStyle,
|
||||||
|
isReady,
|
||||||
|
croppered,
|
||||||
|
onContextmenu
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
inSrc,
|
||||||
|
isReady,
|
||||||
|
getClass,
|
||||||
|
getImageStyle,
|
||||||
|
onContextmenu,
|
||||||
|
getWrapperStyle
|
||||||
|
} = this;
|
||||||
|
const { alt, crossorigin } = this.props;
|
||||||
|
|
||||||
|
return inSrc ? (
|
||||||
|
<div
|
||||||
|
ref="tippyElRef"
|
||||||
|
class={getClass}
|
||||||
|
style={getWrapperStyle}
|
||||||
|
onContextmenu={event => onContextmenu(event)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-show={isReady}
|
||||||
|
ref="imgElRef"
|
||||||
|
style={getImageStyle}
|
||||||
|
src={inSrc}
|
||||||
|
alt={alt}
|
||||||
|
crossorigin={crossorigin}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
});
|
1
src/components/ReCropper/src/svg/arrow-down.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M862 465.3h-81c-4.6 0-9 2-12.1 5.5L550 723.1V160c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v563.1L255.1 470.8c-3-3.5-7.4-5.5-12.1-5.5h-81c-6.8 0-10.5 8.1-6 13.2L487.9 861a31.96 31.96 0 0 0 48.3 0L868 478.5c4.5-5.2.8-13.2-6-13.2"/></svg>
|
After Width: | Height: | Size: 346 B |
1
src/components/ReCropper/src/svg/arrow-h.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="m296.992 216.992-272 272L3.008 512l21.984 23.008 272 272 46.016-46.016L126.016 544h772L680.992 760.992l46.016 46.016 272-272L1020.992 512l-21.984-23.008-272-272-46.048 46.048L898.016 480h-772l216.96-216.992z"/></svg>
|
After Width: | Height: | Size: 325 B |
1
src/components/ReCropper/src/svg/arrow-left.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M872 474H286.9l350.2-304c5.6-4.9 2.2-14-5.2-14h-88.5c-3.9 0-7.6 1.4-10.5 3.9L155 487.8a31.96 31.96 0 0 0 0 48.3L535.1 866c1.5 1.3 3.3 2 5.2 2h91.5c7.4 0 10.8-9.2 5.2-14L286.9 550H872c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8"/></svg>
|
After Width: | Height: | Size: 343 B |
1
src/components/ReCropper/src/svg/arrow-right.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M869 487.8 491.2 159.9c-2.9-2.5-6.6-3.9-10.5-3.9h-88.5c-7.4 0-10.8 9.2-5.2 14l350.2 304H152c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h585.1L386.9 854c-5.6 4.9-2.2 14 5.2 14h91.5c1.9 0 3.8-.7 5.2-2L869 536.2a32.07 32.07 0 0 0 0-48.4"/></svg>
|
After Width: | Height: | Size: 350 B |
1
src/components/ReCropper/src/svg/arrow-up.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M868 545.5 536.1 163a31.96 31.96 0 0 0-48.3 0L156 545.5a7.97 7.97 0 0 0 6 13.2h81c4.6 0 9-2 12.1-5.5L474 300.9V864c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V300.9l218.9 252.3c3 3.5 7.4 5.5 12.1 5.5h81c6.8 0 10.5-8 6-13.2"/></svg>
|
After Width: | Height: | Size: 338 B |
1
src/components/ReCropper/src/svg/arrow-v.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="m512 67.008-23.008 21.984-256 256 46.048 46.048L480 190.016v644L279.008 632.96l-46.048 46.08 256 256 23.008 21.984 23.008-21.984 256-256-46.016-46.016L544 834.016v-644l200.992 200.96 46.016-45.984-256-256z"/></svg>
|
After Width: | Height: | Size: 323 B |
1
src/components/ReCropper/src/svg/change.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="M956.8 988.8H585.6c-16 0-25.6-9.6-25.6-28.8V576c0-16 9.6-28.8 25.6-28.8h371.2c16 0 25.6 9.6 25.6 28.8v384c0 16-9.6 28.8-25.6 28.8M608 937.6h326.4V598.4H608zm-121.6 44.8C262.4 982.4 144 848 144 595.2c0-19.2 9.6-28.8 25.6-28.8s25.6 12.8 25.6 28.8c0 220.8 96 326.4 288 326.4 16 0 25.6 12.8 25.6 28.8s-6.4 32-22.4 32"/><path d="M262.4 694.4c-6.4 0-9.6-3.2-16-6.4L160 601.6c-9.6-9.6-9.6-22.4 0-28.8s22.4-9.6 28.8 0l86.4 86.4c9.6 9.6 9.6 22.4 0 28.8-3.2 3.2-6.4 6.4-12.8 6.4"/><path d="M86.4 694.4c-6.4 0-9.6-3.2-16-6.4-9.6-9.6-9.6-22.4 0-28.8l86.4-86.4c9.6-9.6 22.4-9.6 28.8 0 9.6 9.6 9.6 22.4 0 28.8L99.2 688c-3.2 3.2-6.4 6.4-12.8 6.4m790.4-249.6c-16 0-28.8-12.8-28.8-32 0-224-99.2-336-300.8-336-16 0-28.8-12.8-28.8-32s9.6-32 28.8-32c233.6 0 355.2 137.6 355.2 396.8 0 22.4-9.6 35.2-25.6 35.2"/><path d="M876.8 448c-6.4 0-9.6-3.2-16-6.4l-86.4-86.4c-9.6-9.6-9.6-22.4 0-28.8s22.4-9.6 28.8 0l86.4 86.4c9.6 9.6 9.6 22.4 0 28.8 0 3.2-6.4 6.4-12.8 6.4"/><path d="M876.8 448c-6.4 0-9.6-3.2-16-6.4-9.6-9.6-9.6-22.4 0-28.8l86.4-86.4c9.6-9.6 22.4-9.6 28.8 0s9.6 22.4 0 28.8l-86.4 86.4c-3.2 3.2-6.4 6.4-12.8 6.4M288 524.8C156.8 524.8 48 416 48 278.4S156.8 35.2 288 35.2 528 144 528 281.6 419.2 524.8 288 524.8m-3.2-432c-99.2 0-179.2 83.2-179.2 185.6S185.6 464 284.8 464 464 380.8 464 278.4 384 92.8 284.8 92.8"/></svg>
|
After Width: | Height: | Size: 1.4 KiB |
1
src/components/ReCropper/src/svg/download.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M505.7 661a8 8 0 0 0 12.6 0l112-141.7c4.1-5.2.4-12.9-6.3-12.9h-74.1V168c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v338.3H400c-6.7 0-10.4 7.7-6.3 12.9zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8"/></svg>
|
After Width: | Height: | Size: 417 B |
31
src/components/ReCropper/src/svg/index.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import Reload from "./reload.svg?component";
|
||||||
|
import Upload from "./upload.svg?component";
|
||||||
|
import ArrowH from "./arrow-h.svg?component";
|
||||||
|
import ArrowV from "./arrow-v.svg?component";
|
||||||
|
import ArrowUp from "./arrow-up.svg?component";
|
||||||
|
import ChangeIcon from "./change.svg?component";
|
||||||
|
import ArrowDown from "./arrow-down.svg?component";
|
||||||
|
import ArrowLeft from "./arrow-left.svg?component";
|
||||||
|
import DownloadIcon from "./download.svg?component";
|
||||||
|
import ArrowRight from "./arrow-right.svg?component";
|
||||||
|
import RotateLeft from "./rotate-left.svg?component";
|
||||||
|
import SearchPlus from "./search-plus.svg?component";
|
||||||
|
import RotateRight from "./rotate-right.svg?component";
|
||||||
|
import SearchMinus from "./search-minus.svg?component";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Reload,
|
||||||
|
Upload,
|
||||||
|
ArrowH,
|
||||||
|
ArrowV,
|
||||||
|
ArrowUp,
|
||||||
|
ArrowDown,
|
||||||
|
ArrowLeft,
|
||||||
|
ChangeIcon,
|
||||||
|
ArrowRight,
|
||||||
|
RotateLeft,
|
||||||
|
SearchPlus,
|
||||||
|
RotateRight,
|
||||||
|
SearchMinus,
|
||||||
|
DownloadIcon
|
||||||
|
};
|
1
src/components/ReCropper/src/svg/reload.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M168 504.2c1-43.7 10-86.1 26.9-126 17.3-41 42.1-77.7 73.7-109.4S337 212.3 378 195c42.4-17.9 87.4-27 133.9-27s91.5 9.1 133.8 27A341.5 341.5 0 0 1 755 268.8c9.9 9.9 19.2 20.4 27.8 31.4l-60.2 47a8 8 0 0 0 3 14.1l175.7 43c5 1.2 9.9-2.6 9.9-7.7l.8-180.9c0-6.7-7.7-10.5-12.9-6.3l-56.4 44.1C765.8 155.1 646.2 92 511.8 92 282.7 92 96.3 275.6 92 503.8a8 8 0 0 0 8 8.2h60c4.4 0 7.9-3.5 8-7.8m756 7.8h-60c-4.4 0-7.9 3.5-8 7.8-1 43.7-10 86.1-26.9 126-17.3 41-42.1 77.8-73.7 109.4A342.45 342.45 0 0 1 512.1 856a342.24 342.24 0 0 1-243.2-100.8c-9.9-9.9-19.2-20.4-27.8-31.4l60.2-47a8 8 0 0 0-3-14.1l-175.7-43c-5-1.2-9.9 2.6-9.9 7.7l-.7 181c0 6.7 7.7 10.5 12.9 6.3l56.4-44.1C258.2 868.9 377.8 932 512.2 932c229.2 0 415.5-183.7 419.8-411.8a8 8 0 0 0-8-8.2"/></svg>
|
After Width: | Height: | Size: 863 B |
1
src/components/ReCropper/src/svg/rotate-left.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M672 418H144c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32m-44 402H188V494h440z"/><path fill="currentColor" d="M819.3 328.5c-78.8-100.7-196-153.6-314.6-154.2l-.2-64c0-6.5-7.6-10.1-12.6-6.1l-128 101c-4 3.1-3.9 9.1 0 12.3L492 318.6c5.1 4 12.7.4 12.6-6.1v-63.9c12.9.1 25.9.9 38.8 2.5 42.1 5.2 82.1 18.2 119 38.7 38.1 21.2 71.2 49.7 98.4 84.3 27.1 34.7 46.7 73.7 58.1 115.8 11 40.7 14 82.7 8.9 124.8-.7 5.4-1.4 10.8-2.4 16.1h74.9c14.8-103.6-11.3-213-81-302.3"/></svg>
|
After Width: | Height: | Size: 630 B |
1
src/components/ReCropper/src/svg/rotate-right.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M480.5 251.2c13-1.6 25.9-2.4 38.8-2.5v63.9c0 6.5 7.5 10.1 12.6 6.1L660 217.6c4-3.2 4-9.2 0-12.3l-128-101c-5.1-4-12.6-.4-12.6 6.1l-.2 64c-118.6.5-235.8 53.4-314.6 154.2-69.6 89.2-95.7 198.6-81.1 302.4h74.9c-.9-5.3-1.7-10.7-2.4-16.1-5.1-42.1-2.1-84.1 8.9-124.8 11.4-42.2 31-81.1 58.1-115.8 27.2-34.7 60.3-63.2 98.4-84.3 37-20.6 76.9-33.6 119.1-38.8"/><path fill="currentColor" d="M880 418H352c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32m-44 402H396V494h440z"/></svg>
|
After Width: | Height: | Size: 633 B |
1
src/components/ReCropper/src/svg/search-minus.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M637 443H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h312c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8m284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11M696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430"/></svg>
|
After Width: | Height: | Size: 532 B |
1
src/components/ReCropper/src/svg/search-plus.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M637 443H519V309c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v134H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h118v134c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V519h118c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8m284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11M696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430"/></svg>
|
After Width: | Height: | Size: 628 B |
1
src/components/ReCropper/src/svg/upload.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M400 317.7h73.9V656c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V317.7H624c6.7 0 10.4-7.7 6.3-12.9L518.3 163a8 8 0 0 0-12.6 0l-112 141.7c-4.1 5.3-.4 13 6.3 13M878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8"/></svg>
|
After Width: | Height: | Size: 421 B |
@ -11,15 +11,16 @@ const home = 0, // 平台规定只有 home 路由的 rank 才能为 0 ,所以
|
|||||||
nested = 8,
|
nested = 8,
|
||||||
permission = 9,
|
permission = 9,
|
||||||
system = 10,
|
system = 10,
|
||||||
tabs = 11,
|
monitor = 11,
|
||||||
about = 12,
|
tabs = 12,
|
||||||
editor = 13,
|
about = 13,
|
||||||
flowchart = 14,
|
editor = 14,
|
||||||
formdesign = 15,
|
flowchart = 15,
|
||||||
board = 16,
|
formdesign = 16,
|
||||||
ppt = 17,
|
board = 17,
|
||||||
guide = 18,
|
ppt = 18,
|
||||||
menuoverflow = 19;
|
guide = 19,
|
||||||
|
menuoverflow = 20;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
home,
|
home,
|
||||||
@ -33,6 +34,7 @@ export {
|
|||||||
nested,
|
nested,
|
||||||
permission,
|
permission,
|
||||||
system,
|
system,
|
||||||
|
monitor,
|
||||||
tabs,
|
tabs,
|
||||||
about,
|
about,
|
||||||
editor,
|
editor,
|
||||||
|
169
src/views/monitor/logs/login/hook.tsx
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import { message } from "@/utils/message";
|
||||||
|
import { getKeyList } from "@pureadmin/utils";
|
||||||
|
import { getLoginLogsList } from "@/api/system";
|
||||||
|
import { usePublicHooks } from "@/views/system/hooks";
|
||||||
|
import type { PaginationProps } from "@pureadmin/table";
|
||||||
|
import { type Ref, reactive, ref, onMounted, toRaw } from "vue";
|
||||||
|
|
||||||
|
export function useRole(tableRef: Ref) {
|
||||||
|
const form = reactive({
|
||||||
|
username: "",
|
||||||
|
status: "",
|
||||||
|
loginTime: ""
|
||||||
|
});
|
||||||
|
const dataList = ref([]);
|
||||||
|
const loading = ref(true);
|
||||||
|
const selectedNum = ref(0);
|
||||||
|
const { tagStyle } = usePublicHooks();
|
||||||
|
|
||||||
|
const pagination = reactive<PaginationProps>({
|
||||||
|
total: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
currentPage: 1,
|
||||||
|
background: true
|
||||||
|
});
|
||||||
|
const columns: TableColumnList = [
|
||||||
|
{
|
||||||
|
label: "勾选列", // 如果需要表格多选,此处label必须设置
|
||||||
|
type: "selection",
|
||||||
|
fixed: "left",
|
||||||
|
reserveSelection: true // 数据刷新后保留选项
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "序号",
|
||||||
|
prop: "id",
|
||||||
|
minWidth: 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "用户名",
|
||||||
|
prop: "username",
|
||||||
|
minWidth: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "登录 IP",
|
||||||
|
prop: "ip",
|
||||||
|
minWidth: 140
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "登录地点",
|
||||||
|
prop: "address",
|
||||||
|
minWidth: 140
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "操作系统",
|
||||||
|
prop: "system",
|
||||||
|
minWidth: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "浏览器类型",
|
||||||
|
prop: "browser",
|
||||||
|
minWidth: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "登录状态",
|
||||||
|
prop: "status",
|
||||||
|
minWidth: 100,
|
||||||
|
cellRenderer: ({ row, props }) => (
|
||||||
|
<el-tag size={props.size} style={tagStyle.value(row.status)}>
|
||||||
|
{row.status === 1 ? "成功" : "失败"}
|
||||||
|
</el-tag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "登录行为",
|
||||||
|
prop: "behavior",
|
||||||
|
minWidth: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "登录时间",
|
||||||
|
prop: "loginTime",
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: ({ loginTime }) =>
|
||||||
|
dayjs(loginTime).format("YYYY-MM-DD HH:mm:ss")
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleSizeChange(val: number) {
|
||||||
|
console.log(`${val} items per page`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCurrentChange(val: number) {
|
||||||
|
console.log(`current page: ${val}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当CheckBox选择项发生变化时会触发该事件 */
|
||||||
|
function handleSelectionChange(val) {
|
||||||
|
selectedNum.value = val.length;
|
||||||
|
// 重置表格高度
|
||||||
|
tableRef.value.setAdaptive();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消选择 */
|
||||||
|
function onSelectionCancel() {
|
||||||
|
selectedNum.value = 0;
|
||||||
|
// 用于多选表格,清空用户的选择
|
||||||
|
tableRef.value.getTableRef().clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除 */
|
||||||
|
function onbatchDel() {
|
||||||
|
// 返回当前选中的行
|
||||||
|
const curSelected = tableRef.value.getTableRef().getSelectionRows();
|
||||||
|
// 接下来根据实际业务,通过选中行的某项数据,比如下面的id,调用接口进行批量删除
|
||||||
|
message(`已删除序号为 ${getKeyList(curSelected, "id")} 的数据`, {
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
tableRef.value.getTableRef().clearSelection();
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清空日志 */
|
||||||
|
function clearAll() {
|
||||||
|
// 根据实际业务,调用接口删除所有日志数据
|
||||||
|
message("已删除所有日志数据", {
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSearch() {
|
||||||
|
loading.value = true;
|
||||||
|
const { data } = await getLoginLogsList(toRaw(form));
|
||||||
|
dataList.value = data.list;
|
||||||
|
pagination.total = data.total;
|
||||||
|
pagination.pageSize = data.pageSize;
|
||||||
|
pagination.currentPage = data.currentPage;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = formEl => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.resetFields();
|
||||||
|
onSearch();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
onSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
loading,
|
||||||
|
columns,
|
||||||
|
dataList,
|
||||||
|
pagination,
|
||||||
|
selectedNum,
|
||||||
|
onSearch,
|
||||||
|
clearAll,
|
||||||
|
resetForm,
|
||||||
|
onbatchDel,
|
||||||
|
handleSizeChange,
|
||||||
|
onSelectionCancel,
|
||||||
|
handleCurrentChange,
|
||||||
|
handleSelectionChange
|
||||||
|
};
|
||||||
|
}
|
165
src/views/monitor/logs/login/index.vue
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useRole } from "./hook";
|
||||||
|
import { getPickerShortcuts } from "../../utils";
|
||||||
|
import { PureTableBar } from "@/components/RePureTableBar";
|
||||||
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
|
|
||||||
|
import Delete from "@iconify-icons/ep/delete";
|
||||||
|
import Refresh from "@iconify-icons/ep/refresh";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "LoginLog"
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const tableRef = ref();
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
loading,
|
||||||
|
columns,
|
||||||
|
dataList,
|
||||||
|
pagination,
|
||||||
|
selectedNum,
|
||||||
|
onSearch,
|
||||||
|
clearAll,
|
||||||
|
resetForm,
|
||||||
|
onbatchDel,
|
||||||
|
handleSizeChange,
|
||||||
|
onSelectionCancel,
|
||||||
|
handleCurrentChange,
|
||||||
|
handleSelectionChange
|
||||||
|
} = useRole(tableRef);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="main">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:inline="true"
|
||||||
|
:model="form"
|
||||||
|
class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]"
|
||||||
|
>
|
||||||
|
<el-form-item label="用户名" prop="username">
|
||||||
|
<el-input
|
||||||
|
v-model="form.username"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
clearable
|
||||||
|
class="!w-[150px]"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="登录状态" prop="status">
|
||||||
|
<el-select
|
||||||
|
v-model="form.status"
|
||||||
|
placeholder="请选择"
|
||||||
|
clearable
|
||||||
|
class="!w-[150px]"
|
||||||
|
>
|
||||||
|
<el-option label="成功" value="1" />
|
||||||
|
<el-option label="失败" value="0" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="登录时间" prop="loginTime">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="form.loginTime"
|
||||||
|
:shortcuts="getPickerShortcuts()"
|
||||||
|
type="datetimerange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期时间"
|
||||||
|
end-placeholder="结束日期时间"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="useRenderIcon('ri:search-line')"
|
||||||
|
:loading="loading"
|
||||||
|
@click="onSearch"
|
||||||
|
>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<PureTableBar
|
||||||
|
title="登录日志(仅演示,操作后不生效)"
|
||||||
|
:columns="columns"
|
||||||
|
@refresh="onSearch"
|
||||||
|
>
|
||||||
|
<template #buttons>
|
||||||
|
<el-popconfirm title="确定要删除所有日志数据吗?" @confirm="clearAll">
|
||||||
|
<template #reference>
|
||||||
|
<el-button type="danger" :icon="useRenderIcon(Delete)">
|
||||||
|
清空日志
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
<template v-slot="{ size, dynamicColumns }">
|
||||||
|
<div
|
||||||
|
v-if="selectedNum > 0"
|
||||||
|
v-motion-fade
|
||||||
|
class="bg-[var(--el-fill-color-light)] w-full h-[46px] mb-2 pl-4 flex items-center"
|
||||||
|
>
|
||||||
|
<div class="flex-auto">
|
||||||
|
<span
|
||||||
|
style="font-size: var(--el-font-size-base)"
|
||||||
|
class="text-[rgba(42,46,54,0.5)] dark:text-[rgba(220,220,242,0.5)]"
|
||||||
|
>
|
||||||
|
已选 {{ selectedNum }} 项
|
||||||
|
</span>
|
||||||
|
<el-button type="primary" text @click="onSelectionCancel">
|
||||||
|
取消选择
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-popconfirm title="是否确认删除?" @confirm="onbatchDel">
|
||||||
|
<template #reference>
|
||||||
|
<el-button type="danger" text class="mr-1"> 批量删除 </el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</div>
|
||||||
|
<pure-table
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
align-whole="center"
|
||||||
|
table-layout="auto"
|
||||||
|
:loading="loading"
|
||||||
|
:size="size"
|
||||||
|
adaptive
|
||||||
|
:adaptiveConfig="{ offsetBottom: 108 }"
|
||||||
|
:data="dataList"
|
||||||
|
:columns="dynamicColumns"
|
||||||
|
:pagination="pagination"
|
||||||
|
:paginationSmall="size === 'small' ? true : false"
|
||||||
|
:header-cell-style="{
|
||||||
|
background: 'var(--el-fill-color-light)',
|
||||||
|
color: 'var(--el-text-color-primary)'
|
||||||
|
}"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
@page-size-change="handleSizeChange"
|
||||||
|
@page-current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</PureTableBar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:deep(.el-dropdown-menu__item i) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin: 24px 24px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
174
src/views/monitor/logs/operation/hook.tsx
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import { message } from "@/utils/message";
|
||||||
|
import { getKeyList } from "@pureadmin/utils";
|
||||||
|
import { getOperationLogsList } from "@/api/system";
|
||||||
|
import { usePublicHooks } from "@/views/system/hooks";
|
||||||
|
import type { PaginationProps } from "@pureadmin/table";
|
||||||
|
import { type Ref, reactive, ref, onMounted, toRaw } from "vue";
|
||||||
|
|
||||||
|
export function useRole(tableRef: Ref) {
|
||||||
|
const form = reactive({
|
||||||
|
module: "",
|
||||||
|
status: "",
|
||||||
|
operatingTime: ""
|
||||||
|
});
|
||||||
|
const dataList = ref([]);
|
||||||
|
const loading = ref(true);
|
||||||
|
const selectedNum = ref(0);
|
||||||
|
const { tagStyle } = usePublicHooks();
|
||||||
|
|
||||||
|
const pagination = reactive<PaginationProps>({
|
||||||
|
total: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
currentPage: 1,
|
||||||
|
background: true
|
||||||
|
});
|
||||||
|
const columns: TableColumnList = [
|
||||||
|
{
|
||||||
|
label: "勾选列", // 如果需要表格多选,此处label必须设置
|
||||||
|
type: "selection",
|
||||||
|
fixed: "left",
|
||||||
|
reserveSelection: true // 数据刷新后保留选项
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "序号",
|
||||||
|
prop: "id",
|
||||||
|
minWidth: 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "操作人员",
|
||||||
|
prop: "username",
|
||||||
|
minWidth: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "所属模块",
|
||||||
|
prop: "module",
|
||||||
|
minWidth: 140
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "操作概要",
|
||||||
|
prop: "summary",
|
||||||
|
minWidth: 140
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "操作 IP",
|
||||||
|
prop: "ip",
|
||||||
|
minWidth: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "操作地点",
|
||||||
|
prop: "address",
|
||||||
|
minWidth: 140
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "操作系统",
|
||||||
|
prop: "system",
|
||||||
|
minWidth: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "浏览器类型",
|
||||||
|
prop: "browser",
|
||||||
|
minWidth: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "操作状态",
|
||||||
|
prop: "status",
|
||||||
|
minWidth: 100,
|
||||||
|
cellRenderer: ({ row, props }) => (
|
||||||
|
<el-tag size={props.size} style={tagStyle.value(row.status)}>
|
||||||
|
{row.status === 1 ? "成功" : "失败"}
|
||||||
|
</el-tag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "操作时间",
|
||||||
|
prop: "operatingTime",
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: ({ operatingTime }) =>
|
||||||
|
dayjs(operatingTime).format("YYYY-MM-DD HH:mm:ss")
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleSizeChange(val: number) {
|
||||||
|
console.log(`${val} items per page`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCurrentChange(val: number) {
|
||||||
|
console.log(`current page: ${val}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当CheckBox选择项发生变化时会触发该事件 */
|
||||||
|
function handleSelectionChange(val) {
|
||||||
|
selectedNum.value = val.length;
|
||||||
|
// 重置表格高度
|
||||||
|
tableRef.value.setAdaptive();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消选择 */
|
||||||
|
function onSelectionCancel() {
|
||||||
|
selectedNum.value = 0;
|
||||||
|
// 用于多选表格,清空用户的选择
|
||||||
|
tableRef.value.getTableRef().clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除 */
|
||||||
|
function onbatchDel() {
|
||||||
|
// 返回当前选中的行
|
||||||
|
const curSelected = tableRef.value.getTableRef().getSelectionRows();
|
||||||
|
// 接下来根据实际业务,通过选中行的某项数据,比如下面的id,调用接口进行批量删除
|
||||||
|
message(`已删除序号为 ${getKeyList(curSelected, "id")} 的数据`, {
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
tableRef.value.getTableRef().clearSelection();
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清空日志 */
|
||||||
|
function clearAll() {
|
||||||
|
// 根据实际业务,调用接口删除所有日志数据
|
||||||
|
message("已删除所有日志数据", {
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSearch() {
|
||||||
|
loading.value = true;
|
||||||
|
const { data } = await getOperationLogsList(toRaw(form));
|
||||||
|
dataList.value = data.list;
|
||||||
|
pagination.total = data.total;
|
||||||
|
pagination.pageSize = data.pageSize;
|
||||||
|
pagination.currentPage = data.currentPage;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = formEl => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.resetFields();
|
||||||
|
onSearch();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
onSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
loading,
|
||||||
|
columns,
|
||||||
|
dataList,
|
||||||
|
pagination,
|
||||||
|
selectedNum,
|
||||||
|
onSearch,
|
||||||
|
clearAll,
|
||||||
|
resetForm,
|
||||||
|
onbatchDel,
|
||||||
|
handleSizeChange,
|
||||||
|
onSelectionCancel,
|
||||||
|
handleCurrentChange,
|
||||||
|
handleSelectionChange
|
||||||
|
};
|
||||||
|
}
|
165
src/views/monitor/logs/operation/index.vue
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useRole } from "./hook";
|
||||||
|
import { getPickerShortcuts } from "../../utils";
|
||||||
|
import { PureTableBar } from "@/components/RePureTableBar";
|
||||||
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
|
|
||||||
|
import Delete from "@iconify-icons/ep/delete";
|
||||||
|
import Refresh from "@iconify-icons/ep/refresh";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "OperationLog"
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const tableRef = ref();
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
loading,
|
||||||
|
columns,
|
||||||
|
dataList,
|
||||||
|
pagination,
|
||||||
|
selectedNum,
|
||||||
|
onSearch,
|
||||||
|
clearAll,
|
||||||
|
resetForm,
|
||||||
|
onbatchDel,
|
||||||
|
handleSizeChange,
|
||||||
|
onSelectionCancel,
|
||||||
|
handleCurrentChange,
|
||||||
|
handleSelectionChange
|
||||||
|
} = useRole(tableRef);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="main">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:inline="true"
|
||||||
|
:model="form"
|
||||||
|
class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]"
|
||||||
|
>
|
||||||
|
<el-form-item label="所属模块" prop="module">
|
||||||
|
<el-input
|
||||||
|
v-model="form.module"
|
||||||
|
placeholder="请输入所属模块"
|
||||||
|
clearable
|
||||||
|
class="!w-[170px]"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="操作状态" prop="status">
|
||||||
|
<el-select
|
||||||
|
v-model="form.status"
|
||||||
|
placeholder="请选择"
|
||||||
|
clearable
|
||||||
|
class="!w-[150px]"
|
||||||
|
>
|
||||||
|
<el-option label="成功" value="1" />
|
||||||
|
<el-option label="失败" value="0" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="操作时间" prop="operatingTime">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="form.operatingTime"
|
||||||
|
:shortcuts="getPickerShortcuts()"
|
||||||
|
type="datetimerange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期时间"
|
||||||
|
end-placeholder="结束日期时间"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="useRenderIcon('ri:search-line')"
|
||||||
|
:loading="loading"
|
||||||
|
@click="onSearch"
|
||||||
|
>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<PureTableBar
|
||||||
|
title="操作日志(仅演示,操作后不生效)"
|
||||||
|
:columns="columns"
|
||||||
|
@refresh="onSearch"
|
||||||
|
>
|
||||||
|
<template #buttons>
|
||||||
|
<el-popconfirm title="确定要删除所有日志数据吗?" @confirm="clearAll">
|
||||||
|
<template #reference>
|
||||||
|
<el-button type="danger" :icon="useRenderIcon(Delete)">
|
||||||
|
清空日志
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
<template v-slot="{ size, dynamicColumns }">
|
||||||
|
<div
|
||||||
|
v-if="selectedNum > 0"
|
||||||
|
v-motion-fade
|
||||||
|
class="bg-[var(--el-fill-color-light)] w-full h-[46px] mb-2 pl-4 flex items-center"
|
||||||
|
>
|
||||||
|
<div class="flex-auto">
|
||||||
|
<span
|
||||||
|
style="font-size: var(--el-font-size-base)"
|
||||||
|
class="text-[rgba(42,46,54,0.5)] dark:text-[rgba(220,220,242,0.5)]"
|
||||||
|
>
|
||||||
|
已选 {{ selectedNum }} 项
|
||||||
|
</span>
|
||||||
|
<el-button type="primary" text @click="onSelectionCancel">
|
||||||
|
取消选择
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-popconfirm title="是否确认删除?" @confirm="onbatchDel">
|
||||||
|
<template #reference>
|
||||||
|
<el-button type="danger" text class="mr-1"> 批量删除 </el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</div>
|
||||||
|
<pure-table
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
align-whole="center"
|
||||||
|
table-layout="auto"
|
||||||
|
:loading="loading"
|
||||||
|
:size="size"
|
||||||
|
adaptive
|
||||||
|
:adaptiveConfig="{ offsetBottom: 108 }"
|
||||||
|
:data="dataList"
|
||||||
|
:columns="dynamicColumns"
|
||||||
|
:pagination="pagination"
|
||||||
|
:paginationSmall="size === 'small' ? true : false"
|
||||||
|
:header-cell-style="{
|
||||||
|
background: 'var(--el-fill-color-light)',
|
||||||
|
color: 'var(--el-text-color-primary)'
|
||||||
|
}"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
@page-size-change="handleSizeChange"
|
||||||
|
@page-current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</PureTableBar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:deep(.el-dropdown-menu__item i) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin: 24px 24px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
228
src/views/monitor/logs/system/hook.tsx
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import { message } from "@/utils/message";
|
||||||
|
import { getSystemLogsList } from "@/api/system";
|
||||||
|
import type { PaginationProps } from "@pureadmin/table";
|
||||||
|
import { type Ref, reactive, ref, onMounted, toRaw } from "vue";
|
||||||
|
import { getKeyList, useCopyToClipboard } from "@pureadmin/utils";
|
||||||
|
import Info from "@iconify-icons/ri/question-line";
|
||||||
|
|
||||||
|
export function useRole(tableRef: Ref) {
|
||||||
|
const form = reactive({
|
||||||
|
module: "",
|
||||||
|
requestTime: ""
|
||||||
|
});
|
||||||
|
const dataList = ref([]);
|
||||||
|
const loading = ref(true);
|
||||||
|
const selectedNum = ref(0);
|
||||||
|
const { copied, update } = useCopyToClipboard();
|
||||||
|
|
||||||
|
const pagination = reactive<PaginationProps>({
|
||||||
|
total: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
currentPage: 1,
|
||||||
|
background: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// const getLevelType = (type, text = false) => {
|
||||||
|
// switch (type) {
|
||||||
|
// case 0:
|
||||||
|
// return text ? "debug" : "primary";
|
||||||
|
// case 1:
|
||||||
|
// return text ? "info" : "success";
|
||||||
|
// case 2:
|
||||||
|
// return text ? "warn" : "info";
|
||||||
|
// case 3:
|
||||||
|
// return text ? "error" : "warning";
|
||||||
|
// case 4:
|
||||||
|
// return text ? "fatal" : "danger";
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
const columns: TableColumnList = [
|
||||||
|
{
|
||||||
|
label: "勾选列", // 如果需要表格多选,此处label必须设置
|
||||||
|
type: "selection",
|
||||||
|
fixed: "left",
|
||||||
|
reserveSelection: true // 数据刷新后保留选项
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "ID",
|
||||||
|
prop: "id",
|
||||||
|
minWidth: 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "所属模块",
|
||||||
|
prop: "module",
|
||||||
|
minWidth: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headerRenderer: () => (
|
||||||
|
<span class="flex-c">
|
||||||
|
请求接口
|
||||||
|
<iconifyIconOffline
|
||||||
|
icon={Info}
|
||||||
|
class="ml-1 cursor-help"
|
||||||
|
v-tippy={{
|
||||||
|
content: "双击下面请求接口进行拷贝"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
prop: "url",
|
||||||
|
minWidth: 140
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "请求方法",
|
||||||
|
prop: "method",
|
||||||
|
minWidth: 140
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "IP 地址",
|
||||||
|
prop: "ip",
|
||||||
|
minWidth: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "地点",
|
||||||
|
prop: "address",
|
||||||
|
minWidth: 140
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "操作系统",
|
||||||
|
prop: "system",
|
||||||
|
minWidth: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "浏览器类型",
|
||||||
|
prop: "browser",
|
||||||
|
minWidth: 100
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// label: "级别",
|
||||||
|
// prop: "level",
|
||||||
|
// minWidth: 90,
|
||||||
|
// cellRenderer: ({ row, props }) => (
|
||||||
|
// <el-tag size={props.size} type={getLevelType(row.level)} effect="plain">
|
||||||
|
// {getLevelType(row.level, true)}
|
||||||
|
// </el-tag>
|
||||||
|
// )
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
label: "请求耗时",
|
||||||
|
prop: "takesTime",
|
||||||
|
minWidth: 100,
|
||||||
|
cellRenderer: ({ row, props }) => (
|
||||||
|
<el-tag
|
||||||
|
size={props.size}
|
||||||
|
type={row.takesTime < 1000 ? "success" : "warning"}
|
||||||
|
effect="plain"
|
||||||
|
>
|
||||||
|
{row.takesTime} ms
|
||||||
|
</el-tag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "请求时间",
|
||||||
|
prop: "requestTime",
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: ({ requestTime }) =>
|
||||||
|
dayjs(requestTime).format("YYYY-MM-DD HH:mm:ss")
|
||||||
|
}
|
||||||
|
// {
|
||||||
|
// label: "操作",
|
||||||
|
// fixed: "right",
|
||||||
|
// slot: "operation"
|
||||||
|
// }
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleSizeChange(val: number) {
|
||||||
|
console.log(`${val} items per page`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCurrentChange(val: number) {
|
||||||
|
console.log(`current page: ${val}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当CheckBox选择项发生变化时会触发该事件 */
|
||||||
|
function handleSelectionChange(val) {
|
||||||
|
selectedNum.value = val.length;
|
||||||
|
// 重置表格高度
|
||||||
|
tableRef.value.setAdaptive();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消选择 */
|
||||||
|
function onSelectionCancel() {
|
||||||
|
selectedNum.value = 0;
|
||||||
|
// 用于多选表格,清空用户的选择
|
||||||
|
tableRef.value.getTableRef().clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 拷贝请求接口,表格单元格被双击时触发 */
|
||||||
|
function handleCellDblclick({ url }) {
|
||||||
|
update(url);
|
||||||
|
copied.value
|
||||||
|
? message(`${url} 已拷贝`, { type: "success" })
|
||||||
|
: message("拷贝失败", { type: "warning" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除 */
|
||||||
|
function onbatchDel() {
|
||||||
|
// 返回当前选中的行
|
||||||
|
const curSelected = tableRef.value.getTableRef().getSelectionRows();
|
||||||
|
// 接下来根据实际业务,通过选中行的某项数据,比如下面的id,调用接口进行批量删除
|
||||||
|
message(`已删除序号为 ${getKeyList(curSelected, "id")} 的数据`, {
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
tableRef.value.getTableRef().clearSelection();
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清空日志 */
|
||||||
|
function clearAll() {
|
||||||
|
// 根据实际业务,调用接口删除所有日志数据
|
||||||
|
message("已删除所有日志数据", {
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSearch() {
|
||||||
|
loading.value = true;
|
||||||
|
const { data } = await getSystemLogsList(toRaw(form));
|
||||||
|
dataList.value = data.list;
|
||||||
|
pagination.total = data.total;
|
||||||
|
pagination.pageSize = data.pageSize;
|
||||||
|
pagination.currentPage = data.currentPage;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = formEl => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.resetFields();
|
||||||
|
onSearch();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
onSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
loading,
|
||||||
|
columns,
|
||||||
|
dataList,
|
||||||
|
pagination,
|
||||||
|
selectedNum,
|
||||||
|
onSearch,
|
||||||
|
clearAll,
|
||||||
|
resetForm,
|
||||||
|
onbatchDel,
|
||||||
|
handleSizeChange,
|
||||||
|
onSelectionCancel,
|
||||||
|
handleCellDblclick,
|
||||||
|
handleCurrentChange,
|
||||||
|
handleSelectionChange
|
||||||
|
};
|
||||||
|
}
|
156
src/views/monitor/logs/system/index.vue
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useRole } from "./hook";
|
||||||
|
import { getPickerShortcuts } from "../../utils";
|
||||||
|
import { PureTableBar } from "@/components/RePureTableBar";
|
||||||
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
|
|
||||||
|
import Delete from "@iconify-icons/ep/delete";
|
||||||
|
import Refresh from "@iconify-icons/ep/refresh";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "SystemLog"
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const tableRef = ref();
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
loading,
|
||||||
|
columns,
|
||||||
|
dataList,
|
||||||
|
pagination,
|
||||||
|
selectedNum,
|
||||||
|
onSearch,
|
||||||
|
clearAll,
|
||||||
|
resetForm,
|
||||||
|
onbatchDel,
|
||||||
|
handleSizeChange,
|
||||||
|
onSelectionCancel,
|
||||||
|
handleCellDblclick,
|
||||||
|
handleCurrentChange,
|
||||||
|
handleSelectionChange
|
||||||
|
} = useRole(tableRef);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="main">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:inline="true"
|
||||||
|
:model="form"
|
||||||
|
class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]"
|
||||||
|
>
|
||||||
|
<el-form-item label="所属模块" prop="module">
|
||||||
|
<el-input
|
||||||
|
v-model="form.module"
|
||||||
|
placeholder="请输入所属模块"
|
||||||
|
clearable
|
||||||
|
class="!w-[170px]"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="请求时间" prop="requestTime">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="form.requestTime"
|
||||||
|
:shortcuts="getPickerShortcuts()"
|
||||||
|
type="datetimerange"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期时间"
|
||||||
|
end-placeholder="结束日期时间"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="useRenderIcon('ri:search-line')"
|
||||||
|
:loading="loading"
|
||||||
|
@click="onSearch"
|
||||||
|
>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<PureTableBar
|
||||||
|
title="系统日志(仅演示,操作后不生效)"
|
||||||
|
:columns="columns"
|
||||||
|
@refresh="onSearch"
|
||||||
|
>
|
||||||
|
<template #buttons>
|
||||||
|
<el-popconfirm title="确定要删除所有日志数据吗?" @confirm="clearAll">
|
||||||
|
<template #reference>
|
||||||
|
<el-button type="danger" :icon="useRenderIcon(Delete)">
|
||||||
|
清空日志
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
<template v-slot="{ size, dynamicColumns }">
|
||||||
|
<div
|
||||||
|
v-if="selectedNum > 0"
|
||||||
|
v-motion-fade
|
||||||
|
class="bg-[var(--el-fill-color-light)] w-full h-[46px] mb-2 pl-4 flex items-center"
|
||||||
|
>
|
||||||
|
<div class="flex-auto">
|
||||||
|
<span
|
||||||
|
style="font-size: var(--el-font-size-base)"
|
||||||
|
class="text-[rgba(42,46,54,0.5)] dark:text-[rgba(220,220,242,0.5)]"
|
||||||
|
>
|
||||||
|
已选 {{ selectedNum }} 项
|
||||||
|
</span>
|
||||||
|
<el-button type="primary" text @click="onSelectionCancel">
|
||||||
|
取消选择
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-popconfirm title="是否确认删除?" @confirm="onbatchDel">
|
||||||
|
<template #reference>
|
||||||
|
<el-button type="danger" text class="mr-1"> 批量删除 </el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</div>
|
||||||
|
<pure-table
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
align-whole="center"
|
||||||
|
table-layout="auto"
|
||||||
|
:loading="loading"
|
||||||
|
:size="size"
|
||||||
|
adaptive
|
||||||
|
:adaptiveConfig="{ offsetBottom: 108 }"
|
||||||
|
:data="dataList"
|
||||||
|
:columns="dynamicColumns"
|
||||||
|
:pagination="pagination"
|
||||||
|
:paginationSmall="size === 'small' ? true : false"
|
||||||
|
:header-cell-style="{
|
||||||
|
background: 'var(--el-fill-color-light)',
|
||||||
|
color: 'var(--el-text-color-primary)'
|
||||||
|
}"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
@page-size-change="handleSizeChange"
|
||||||
|
@page-current-change="handleCurrentChange"
|
||||||
|
@cell-dblclick="handleCellDblclick"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</PureTableBar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:deep(.el-dropdown-menu__item i) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin: 24px 24px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
117
src/views/monitor/online/hook.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import { message } from "@/utils/message";
|
||||||
|
import { getOnlineLogsList } from "@/api/system";
|
||||||
|
import { reactive, ref, onMounted, toRaw } from "vue";
|
||||||
|
import type { PaginationProps } from "@pureadmin/table";
|
||||||
|
|
||||||
|
export function useRole() {
|
||||||
|
const form = reactive({
|
||||||
|
username: ""
|
||||||
|
});
|
||||||
|
const dataList = ref([]);
|
||||||
|
const loading = ref(true);
|
||||||
|
const pagination = reactive<PaginationProps>({
|
||||||
|
total: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
currentPage: 1,
|
||||||
|
background: true
|
||||||
|
});
|
||||||
|
const columns: TableColumnList = [
|
||||||
|
{
|
||||||
|
label: "序号",
|
||||||
|
prop: "id",
|
||||||
|
minWidth: 60
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "用户名",
|
||||||
|
prop: "username",
|
||||||
|
minWidth: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "登录 IP",
|
||||||
|
prop: "ip",
|
||||||
|
minWidth: 140
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "登录地点",
|
||||||
|
prop: "address",
|
||||||
|
minWidth: 140
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "操作系统",
|
||||||
|
prop: "system",
|
||||||
|
minWidth: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "浏览器类型",
|
||||||
|
prop: "browser",
|
||||||
|
minWidth: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "登录时间",
|
||||||
|
prop: "loginTime",
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: ({ loginTime }) =>
|
||||||
|
dayjs(loginTime).format("YYYY-MM-DD HH:mm:ss")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "操作",
|
||||||
|
fixed: "right",
|
||||||
|
slot: "operation"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOffline(row) {
|
||||||
|
message(`${row.username}已被强制下线`, { type: "success" });
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSearch() {
|
||||||
|
loading.value = true;
|
||||||
|
const { data } = await getOnlineLogsList(toRaw(form));
|
||||||
|
dataList.value = data.list;
|
||||||
|
pagination.total = data.total;
|
||||||
|
pagination.pageSize = data.pageSize;
|
||||||
|
pagination.currentPage = data.currentPage;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = formEl => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.resetFields();
|
||||||
|
onSearch();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
onSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
loading,
|
||||||
|
columns,
|
||||||
|
dataList,
|
||||||
|
pagination,
|
||||||
|
onSearch,
|
||||||
|
resetForm,
|
||||||
|
handleOffline,
|
||||||
|
handleSizeChange,
|
||||||
|
handleCurrentChange,
|
||||||
|
handleSelectionChange
|
||||||
|
};
|
||||||
|
}
|
125
src/views/monitor/online/index.vue
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useRole } from "./hook";
|
||||||
|
import { PureTableBar } from "@/components/RePureTableBar";
|
||||||
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
|
|
||||||
|
import Plane from "@iconify-icons/ri/plane-line";
|
||||||
|
import Refresh from "@iconify-icons/ep/refresh";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "OnlineUser"
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
loading,
|
||||||
|
columns,
|
||||||
|
dataList,
|
||||||
|
pagination,
|
||||||
|
onSearch,
|
||||||
|
resetForm,
|
||||||
|
handleOffline,
|
||||||
|
handleSizeChange,
|
||||||
|
handleCurrentChange,
|
||||||
|
handleSelectionChange
|
||||||
|
} = useRole();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="main">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:inline="true"
|
||||||
|
:model="form"
|
||||||
|
class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]"
|
||||||
|
>
|
||||||
|
<el-form-item label="用户名" prop="username">
|
||||||
|
<el-input
|
||||||
|
v-model="form.username"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
clearable
|
||||||
|
class="!w-[180px]"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="useRenderIcon('ri:search-line')"
|
||||||
|
:loading="loading"
|
||||||
|
@click="onSearch"
|
||||||
|
>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<PureTableBar
|
||||||
|
title="在线用户(仅演示,操作后不生效)"
|
||||||
|
:columns="columns"
|
||||||
|
@refresh="onSearch"
|
||||||
|
>
|
||||||
|
<template v-slot="{ size, dynamicColumns }">
|
||||||
|
<pure-table
|
||||||
|
align-whole="center"
|
||||||
|
showOverflowTooltip
|
||||||
|
table-layout="auto"
|
||||||
|
:loading="loading"
|
||||||
|
:size="size"
|
||||||
|
adaptive
|
||||||
|
:adaptiveConfig="{ offsetBottom: 108 }"
|
||||||
|
:data="dataList"
|
||||||
|
:columns="dynamicColumns"
|
||||||
|
:pagination="pagination"
|
||||||
|
:paginationSmall="size === 'small' ? true : false"
|
||||||
|
:header-cell-style="{
|
||||||
|
background: 'var(--el-fill-color-light)',
|
||||||
|
color: 'var(--el-text-color-primary)'
|
||||||
|
}"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
@page-size-change="handleSizeChange"
|
||||||
|
@page-current-change="handleCurrentChange"
|
||||||
|
>
|
||||||
|
<template #operation="{ row }">
|
||||||
|
<el-popconfirm
|
||||||
|
:title="`是否强制下线${row.username}`"
|
||||||
|
@confirm="handleOffline(row)"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button
|
||||||
|
class="reset-margin"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(Plane)"
|
||||||
|
>
|
||||||
|
强退
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
</pure-table>
|
||||||
|
</template>
|
||||||
|
</PureTableBar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:deep(.el-dropdown-menu__item i) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin: 24px 24px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
129
src/views/monitor/utils.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
/** 日期、时间选择器快捷选项,常搭配 [DatePicker](https://element-plus.org/zh-CN/component/date-picker.html) 和 [DateTimePicker](https://element-plus.org/zh-CN/component/datetime-picker.html) 的`shortcuts`属性使用 */
|
||||||
|
export const getPickerShortcuts = (): Array<{
|
||||||
|
text: string;
|
||||||
|
value: Date | Function;
|
||||||
|
}> => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: "今天",
|
||||||
|
value: () => {
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
const todayEnd = new Date();
|
||||||
|
todayEnd.setHours(23, 59, 59, 999);
|
||||||
|
return [today, todayEnd];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "昨天",
|
||||||
|
value: () => {
|
||||||
|
const yesterday = new Date();
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
|
yesterday.setHours(0, 0, 0, 0);
|
||||||
|
const yesterdayEnd = new Date();
|
||||||
|
yesterdayEnd.setDate(yesterdayEnd.getDate() - 1);
|
||||||
|
yesterdayEnd.setHours(23, 59, 59, 999);
|
||||||
|
return [yesterday, yesterdayEnd];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "前天",
|
||||||
|
value: () => {
|
||||||
|
const beforeYesterday = new Date();
|
||||||
|
beforeYesterday.setDate(beforeYesterday.getDate() - 2);
|
||||||
|
beforeYesterday.setHours(0, 0, 0, 0);
|
||||||
|
const beforeYesterdayEnd = new Date();
|
||||||
|
beforeYesterdayEnd.setDate(beforeYesterdayEnd.getDate() - 2);
|
||||||
|
beforeYesterdayEnd.setHours(23, 59, 59, 999);
|
||||||
|
return [beforeYesterday, beforeYesterdayEnd];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "本周",
|
||||||
|
value: () => {
|
||||||
|
const today = new Date();
|
||||||
|
const startOfWeek = new Date(
|
||||||
|
today.getFullYear(),
|
||||||
|
today.getMonth(),
|
||||||
|
today.getDate() - today.getDay() + (today.getDay() === 0 ? -6 : 1)
|
||||||
|
);
|
||||||
|
startOfWeek.setHours(0, 0, 0, 0);
|
||||||
|
const endOfWeek = new Date(
|
||||||
|
startOfWeek.getTime() +
|
||||||
|
6 * 24 * 60 * 60 * 1000 +
|
||||||
|
23 * 60 * 60 * 1000 +
|
||||||
|
59 * 60 * 1000 +
|
||||||
|
59 * 1000 +
|
||||||
|
999
|
||||||
|
);
|
||||||
|
return [startOfWeek, endOfWeek];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "上周",
|
||||||
|
value: () => {
|
||||||
|
const today = new Date();
|
||||||
|
const startOfLastWeek = new Date(
|
||||||
|
today.getFullYear(),
|
||||||
|
today.getMonth(),
|
||||||
|
today.getDate() - today.getDay() - 7 + (today.getDay() === 0 ? -6 : 1)
|
||||||
|
);
|
||||||
|
startOfLastWeek.setHours(0, 0, 0, 0);
|
||||||
|
const endOfLastWeek = new Date(
|
||||||
|
startOfLastWeek.getTime() +
|
||||||
|
6 * 24 * 60 * 60 * 1000 +
|
||||||
|
23 * 60 * 60 * 1000 +
|
||||||
|
59 * 60 * 1000 +
|
||||||
|
59 * 1000 +
|
||||||
|
999
|
||||||
|
);
|
||||||
|
return [startOfLastWeek, endOfLastWeek];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "本月",
|
||||||
|
value: () => {
|
||||||
|
const today = new Date();
|
||||||
|
const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||||
|
startOfMonth.setHours(0, 0, 0, 0);
|
||||||
|
const endOfMonth = new Date(
|
||||||
|
today.getFullYear(),
|
||||||
|
today.getMonth() + 1,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
endOfMonth.setHours(23, 59, 59, 999);
|
||||||
|
return [startOfMonth, endOfMonth];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "上个月",
|
||||||
|
value: () => {
|
||||||
|
const today = new Date();
|
||||||
|
const startOfLastMonth = new Date(
|
||||||
|
today.getFullYear(),
|
||||||
|
today.getMonth() - 1,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
startOfLastMonth.setHours(0, 0, 0, 0);
|
||||||
|
const endOfLastMonth = new Date(
|
||||||
|
today.getFullYear(),
|
||||||
|
today.getMonth(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
endOfLastMonth.setHours(23, 59, 59, 999);
|
||||||
|
return [startOfLastMonth, endOfLastMonth];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "本年",
|
||||||
|
value: () => {
|
||||||
|
const today = new Date();
|
||||||
|
const startOfYear = new Date(today.getFullYear(), 0, 1);
|
||||||
|
startOfYear.setHours(0, 0, 0, 0);
|
||||||
|
const endOfYear = new Date(today.getFullYear(), 11, 31);
|
||||||
|
endOfYear.setHours(23, 59, 59, 999);
|
||||||
|
return [startOfYear, endOfYear];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
139
src/views/system/dept/form.vue
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import ReCol from "@/components/ReCol";
|
||||||
|
import { formRules } from "./utils/rule";
|
||||||
|
import { FormProps } from "./utils/types";
|
||||||
|
import { usePublicHooks } from "../hooks";
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<FormProps>(), {
|
||||||
|
formInline: () => ({
|
||||||
|
higherDeptOptions: [],
|
||||||
|
parentId: 0,
|
||||||
|
name: "",
|
||||||
|
principal: "",
|
||||||
|
phone: "",
|
||||||
|
email: "",
|
||||||
|
sort: 0,
|
||||||
|
status: 1,
|
||||||
|
remark: ""
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const ruleFormRef = ref();
|
||||||
|
const { switchStyle } = usePublicHooks();
|
||||||
|
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-row :gutter="30">
|
||||||
|
<re-col>
|
||||||
|
<el-form-item label="上级部门">
|
||||||
|
<el-cascader
|
||||||
|
v-model="newFormInline.parentId"
|
||||||
|
class="w-full"
|
||||||
|
:options="newFormInline.higherDeptOptions"
|
||||||
|
:props="{
|
||||||
|
value: 'id',
|
||||||
|
label: 'name',
|
||||||
|
emitPath: false,
|
||||||
|
checkStrictly: true
|
||||||
|
}"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
placeholder="请选择上级部门"
|
||||||
|
>
|
||||||
|
<template #default="{ node, data }">
|
||||||
|
<span>{{ data.name }}</span>
|
||||||
|
<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
|
||||||
|
</template>
|
||||||
|
</el-cascader>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="部门名称" prop="name">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.name"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入部门名称"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="部门负责人">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.principal"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入部门负责人"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="手机号" prop="phone">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.phone"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.email"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入邮箱"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="排序">
|
||||||
|
<el-input-number
|
||||||
|
v-model="newFormInline.sort"
|
||||||
|
class="!w-full"
|
||||||
|
:min="0"
|
||||||
|
:max="9999"
|
||||||
|
controls-position="right"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="部门状态">
|
||||||
|
<el-switch
|
||||||
|
v-model="newFormInline.status"
|
||||||
|
inline-prompt
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
active-text="启用"
|
||||||
|
inactive-text="停用"
|
||||||
|
:style="switchStyle"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.remark"
|
||||||
|
placeholder="请输入备注信息"
|
||||||
|
type="textarea"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
166
src/views/system/dept/index.vue
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useDept } from "./utils/hook";
|
||||||
|
import { PureTableBar } from "@/components/RePureTableBar";
|
||||||
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
|
|
||||||
|
import Delete from "@iconify-icons/ep/delete";
|
||||||
|
import EditPen from "@iconify-icons/ep/edit-pen";
|
||||||
|
import Refresh from "@iconify-icons/ep/refresh";
|
||||||
|
import AddFill from "@iconify-icons/ri/add-circle-line";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "SystemDept"
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const tableRef = ref();
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
loading,
|
||||||
|
columns,
|
||||||
|
dataList,
|
||||||
|
onSearch,
|
||||||
|
resetForm,
|
||||||
|
openDialog,
|
||||||
|
handleDelete,
|
||||||
|
handleSelectionChange
|
||||||
|
} = useDept();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="main">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:inline="true"
|
||||||
|
:model="form"
|
||||||
|
class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]"
|
||||||
|
>
|
||||||
|
<el-form-item label="部门名称:" prop="name">
|
||||||
|
<el-input
|
||||||
|
v-model="form.name"
|
||||||
|
placeholder="请输入部门名称"
|
||||||
|
clearable
|
||||||
|
class="!w-[180px]"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态:" prop="status">
|
||||||
|
<el-select
|
||||||
|
v-model="form.status"
|
||||||
|
placeholder="请选择状态"
|
||||||
|
clearable
|
||||||
|
class="!w-[180px]"
|
||||||
|
>
|
||||||
|
<el-option label="启用" :value="1" />
|
||||||
|
<el-option label="停用" :value="0" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="useRenderIcon('ri:search-line')"
|
||||||
|
:loading="loading"
|
||||||
|
@click="onSearch"
|
||||||
|
>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<PureTableBar
|
||||||
|
title="部门管理(仅演示,操作后不生效)"
|
||||||
|
:columns="columns"
|
||||||
|
:tableRef="tableRef?.getTableRef()"
|
||||||
|
@refresh="onSearch"
|
||||||
|
>
|
||||||
|
<template #buttons>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="useRenderIcon(AddFill)"
|
||||||
|
@click="openDialog()"
|
||||||
|
>
|
||||||
|
新增部门
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<template v-slot="{ size, dynamicColumns }">
|
||||||
|
<pure-table
|
||||||
|
ref="tableRef"
|
||||||
|
adaptive
|
||||||
|
:adaptiveConfig="{ offsetBottom: 45 }"
|
||||||
|
align-whole="center"
|
||||||
|
row-key="id"
|
||||||
|
showOverflowTooltip
|
||||||
|
table-layout="auto"
|
||||||
|
default-expand-all
|
||||||
|
:loading="loading"
|
||||||
|
:size="size"
|
||||||
|
:data="dataList"
|
||||||
|
:columns="dynamicColumns"
|
||||||
|
:header-cell-style="{
|
||||||
|
background: 'var(--el-fill-color-light)',
|
||||||
|
color: 'var(--el-text-color-primary)'
|
||||||
|
}"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<template #operation="{ row }">
|
||||||
|
<el-button
|
||||||
|
class="reset-margin"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(EditPen)"
|
||||||
|
@click="openDialog('修改', row)"
|
||||||
|
>
|
||||||
|
修改
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
class="reset-margin"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(AddFill)"
|
||||||
|
@click="openDialog('新增', { parentId: row.id } as any)"
|
||||||
|
>
|
||||||
|
新增
|
||||||
|
</el-button>
|
||||||
|
<el-popconfirm
|
||||||
|
:title="`是否确认删除部门名称为${row.name}的这条数据`"
|
||||||
|
@confirm="handleDelete(row)"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button
|
||||||
|
class="reset-margin"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(Delete)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
</pure-table>
|
||||||
|
</template>
|
||||||
|
</PureTableBar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.el-table__inner-wrapper::before) {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin: 24px 24px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
177
src/views/system/dept/utils/hook.tsx
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import editForm from "../form.vue";
|
||||||
|
import { handleTree } from "@/utils/tree";
|
||||||
|
import { message } from "@/utils/message";
|
||||||
|
import { getDeptList } from "@/api/system";
|
||||||
|
import { usePublicHooks } from "../../hooks";
|
||||||
|
import { addDialog } from "@/components/ReDialog";
|
||||||
|
import { reactive, ref, onMounted, h } from "vue";
|
||||||
|
import type { FormItemProps } from "../utils/types";
|
||||||
|
import { cloneDeep, isAllEmpty } from "@pureadmin/utils";
|
||||||
|
|
||||||
|
export function useDept() {
|
||||||
|
const form = reactive({
|
||||||
|
name: "",
|
||||||
|
status: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const dataList = ref([]);
|
||||||
|
const loading = ref(true);
|
||||||
|
const { tagStyle } = usePublicHooks();
|
||||||
|
|
||||||
|
const columns: TableColumnList = [
|
||||||
|
{
|
||||||
|
label: "部门名称",
|
||||||
|
prop: "name",
|
||||||
|
width: 180,
|
||||||
|
align: "left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "排序",
|
||||||
|
prop: "sort",
|
||||||
|
minWidth: 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "状态",
|
||||||
|
prop: "status",
|
||||||
|
minWidth: 100,
|
||||||
|
cellRenderer: ({ row, props }) => (
|
||||||
|
<el-tag size={props.size} style={tagStyle.value(row.status)}>
|
||||||
|
{row.status === 1 ? "启用" : "停用"}
|
||||||
|
</el-tag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "创建时间",
|
||||||
|
minWidth: 200,
|
||||||
|
prop: "createTime",
|
||||||
|
formatter: ({ createTime }) =>
|
||||||
|
dayjs(createTime).format("YYYY-MM-DD HH:mm:ss")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "备注",
|
||||||
|
prop: "remark",
|
||||||
|
minWidth: 320
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "操作",
|
||||||
|
fixed: "right",
|
||||||
|
width: 210,
|
||||||
|
slot: "operation"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleSelectionChange(val) {
|
||||||
|
console.log("handleSelectionChange", val);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm(formEl) {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.resetFields();
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSearch() {
|
||||||
|
loading.value = true;
|
||||||
|
const { data } = await getDeptList(); // 这里是返回一维数组结构,前端自行处理成树结构,返回格式要求:唯一id加父节点parentId,parentId取父节点id
|
||||||
|
let newData = data;
|
||||||
|
if (!isAllEmpty(form.name)) {
|
||||||
|
// 前端搜索部门名称
|
||||||
|
newData = newData.filter(item => item.name.includes(form.name));
|
||||||
|
}
|
||||||
|
if (!isAllEmpty(form.status)) {
|
||||||
|
// 前端搜索状态
|
||||||
|
newData = newData.filter(item => item.status === form.status);
|
||||||
|
}
|
||||||
|
dataList.value = handleTree(newData); // 处理成树结构
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatHigherDeptOptions(treeList) {
|
||||||
|
// 根据返回数据的status字段值判断追加是否禁用disabled字段,返回处理后的树结构,用于上级部门级联选择器的展示(实际开发中也是如此,不可能前端需要的每个字段后端都会返回,这时需要前端自行根据后端返回的某些字段做逻辑处理)
|
||||||
|
if (!treeList || !treeList.length) return;
|
||||||
|
const newTreeList = [];
|
||||||
|
for (let i = 0; i < treeList.length; i++) {
|
||||||
|
treeList[i].disabled = treeList[i].status === 0 ? true : false;
|
||||||
|
formatHigherDeptOptions(treeList[i].children);
|
||||||
|
newTreeList.push(treeList[i]);
|
||||||
|
}
|
||||||
|
return newTreeList;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDialog(title = "新增", row?: FormItemProps) {
|
||||||
|
addDialog({
|
||||||
|
title: `${title}部门`,
|
||||||
|
props: {
|
||||||
|
formInline: {
|
||||||
|
higherDeptOptions: formatHigherDeptOptions(cloneDeep(dataList.value)),
|
||||||
|
parentId: row?.parentId ?? 0,
|
||||||
|
name: row?.name ?? "",
|
||||||
|
principal: row?.principal ?? "",
|
||||||
|
phone: row?.phone ?? "",
|
||||||
|
email: row?.email ?? "",
|
||||||
|
sort: row?.sort ?? 0,
|
||||||
|
status: row?.status ?? 1,
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete(row) {
|
||||||
|
message(`您删除了部门名称为${row.name}的这条数据`, { type: "success" });
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
onSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
loading,
|
||||||
|
columns,
|
||||||
|
dataList,
|
||||||
|
/** 搜索 */
|
||||||
|
onSearch,
|
||||||
|
/** 重置 */
|
||||||
|
resetForm,
|
||||||
|
/** 新增、修改部门 */
|
||||||
|
openDialog,
|
||||||
|
/** 删除部门 */
|
||||||
|
handleDelete,
|
||||||
|
handleSelectionChange
|
||||||
|
};
|
||||||
|
}
|
37
src/views/system/dept/utils/rule.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { reactive } from "vue";
|
||||||
|
import type { FormRules } from "element-plus";
|
||||||
|
import { isPhone, isEmail } from "@pureadmin/utils";
|
||||||
|
|
||||||
|
/** 自定义表单规则校验 */
|
||||||
|
export const formRules = reactive(<FormRules>{
|
||||||
|
name: [{ required: true, message: "部门名称为必填项", trigger: "blur" }],
|
||||||
|
phone: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (value === "") {
|
||||||
|
callback();
|
||||||
|
} else if (!isPhone(value)) {
|
||||||
|
callback(new Error("请输入正确的手机号码格式"));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: "blur"
|
||||||
|
// trigger: "click" // 如果想在点击确定按钮时触发这个校验,trigger 设置成 click 即可
|
||||||
|
}
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (value === "") {
|
||||||
|
callback();
|
||||||
|
} else if (!isEmail(value)) {
|
||||||
|
callback(new Error("请输入正确的邮箱格式"));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
16
src/views/system/dept/utils/types.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
interface FormItemProps {
|
||||||
|
higherDeptOptions: Record<string, unknown>[];
|
||||||
|
parentId: number;
|
||||||
|
name: string;
|
||||||
|
principal: string;
|
||||||
|
phone: string | number;
|
||||||
|
email: string;
|
||||||
|
sort: number;
|
||||||
|
status: number;
|
||||||
|
remark: string;
|
||||||
|
}
|
||||||
|
interface FormProps {
|
||||||
|
formInline: FormItemProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { FormItemProps, FormProps };
|
39
src/views/system/hooks.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// 抽离可公用的工具函数等用于系统管理页面逻辑
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { useDark } from "@pureadmin/utils";
|
||||||
|
|
||||||
|
export function usePublicHooks() {
|
||||||
|
const { isDark } = useDark();
|
||||||
|
|
||||||
|
const switchStyle = computed(() => {
|
||||||
|
return {
|
||||||
|
"--el-switch-on-color": "#6abe39",
|
||||||
|
"--el-switch-off-color": "#e84749"
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const tagStyle = computed(() => {
|
||||||
|
return (status: number) => {
|
||||||
|
return status === 1
|
||||||
|
? {
|
||||||
|
"--el-tag-text-color": isDark.value ? "#6abe39" : "#389e0d",
|
||||||
|
"--el-tag-bg-color": isDark.value ? "#172412" : "#f6ffed",
|
||||||
|
"--el-tag-border-color": isDark.value ? "#274a17" : "#b7eb8f"
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
"--el-tag-text-color": isDark.value ? "#e84749" : "#cf1322",
|
||||||
|
"--el-tag-bg-color": isDark.value ? "#2b1316" : "#fff1f0",
|
||||||
|
"--el-tag-border-color": isDark.value ? "#58191c" : "#ffa39e"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
/** 当前网页是否为`dark`模式 */
|
||||||
|
isDark,
|
||||||
|
/** 表现更鲜明的`el-switch`组件 */
|
||||||
|
switchStyle,
|
||||||
|
/** 表现更鲜明的`el-tag`组件 */
|
||||||
|
tagStyle
|
||||||
|
};
|
||||||
|
}
|
26
src/views/system/menu/README.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<!-- 初版,持续完善中 -->
|
||||||
|
|
||||||
|
## 字段含义
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
| :---------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `menuType` | 菜单类型(`0`代表菜单、`1`代表`iframe`、`2`代表外链、`3`代表按钮) |
|
||||||
|
| `parentId` | |
|
||||||
|
| `title` | 菜单名称(兼容国际化、非国际化,如果用国际化的写法就必须在根目录的`locales`文件夹下对应添加) |
|
||||||
|
| `name` | 路由名称(必须唯一并且和当前路由`component`字段对应的页面里用`defineOptions`包起来的`name`保持一致) |
|
||||||
|
| `path` | 路由路径 |
|
||||||
|
| `component` | 组件路径(传`component`组件路径,那么`path`可以随便写,如果不传,`component`组件路径会跟`path`保持一致) |
|
||||||
|
| `rank` | 菜单排序(平台规定只有`home`路由的`rank`才能为`0`,所以后端在返回`rank`的时候需要从非`0`开始 [点击查看更多](https://yiming_chang.gitee.io/pure-admin-doc/pages/routerMenu/#%E8%8F%9C%E5%8D%95%E6%8E%92%E5%BA%8F-rank)) |
|
||||||
|
| `redirect` | 路由重定向 |
|
||||||
|
| `icon` | 菜单图标 |
|
||||||
|
| `extraIcon` | 右侧图标 |
|
||||||
|
| `enterTransition` | 进场动画(页面加载动画) |
|
||||||
|
| `leaveTransition` | 离场动画(页面加载动画) |
|
||||||
|
| `activePath` | 菜单激活(将某个菜单激活,主要用于通过`query`或`params`传参的路由,当它们通过配置`showLink: false`后不在菜单中显示,就不会有任何菜单高亮,而通过设置`activePath`指定激活菜单即可获得高亮,`activePath`为指定激活菜单的`path`) |
|
||||||
|
| `auths` | 权限标识(按钮级别权限设置) |
|
||||||
|
| `frameSrc` | 链接地址(需要内嵌的`iframe`链接地址) |
|
||||||
|
| `frameLoading` | 加载动画(内嵌的`iframe`页面是否开启首次加载动画) |
|
||||||
|
| `keepAlive` | 缓存页面(是否缓存该路由页面,开启后会保存该页面的整体状态,刷新后会清空状态) |
|
||||||
|
| `hiddenTag` | 标签页(当前菜单名称或自定义信息禁止添加到标签页) |
|
||||||
|
| `showLink` | 菜单(是否显示该菜单) |
|
||||||
|
| `showParent` | 父级菜单(是否显示父级菜单 [点击查看更多](https://yiming_chang.gitee.io/pure-admin-doc/pages/routerMenu/#%E7%AC%AC%E4%B8%80%E7%A7%8D-%E8%AF%A5%E6%A8%A1%E5%BC%8F%E9%92%88%E5%AF%B9%E7%88%B6%E7%BA%A7%E8%8F%9C%E5%8D%95%E4%B8%8B%E5%8F%AA%E6%9C%89%E4%B8%80%E4%B8%AA%E5%AD%90%E8%8F%9C%E5%8D%95%E7%9A%84%E6%83%85%E5%86%B5-%E5%9C%A8%E5%AD%90%E8%8F%9C%E5%8D%95%E7%9A%84-meta-%E5%B1%9E%E6%80%A7%E4%B8%AD%E5%8A%A0%E4%B8%8A-showparent-true-%E5%8D%B3%E5%8F%AF)) |
|
326
src/views/system/menu/form.vue
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import ReCol from "@/components/ReCol";
|
||||||
|
import { formRules } from "./utils/rule";
|
||||||
|
import { FormProps } from "./utils/types";
|
||||||
|
import { transformI18n } from "@/plugins/i18n";
|
||||||
|
import { IconSelect } from "@/components/ReIcon";
|
||||||
|
import Segmented from "@/components/ReSegmented";
|
||||||
|
import ReAnimateSelector from "@/components/ReAnimateSelector";
|
||||||
|
import {
|
||||||
|
menuTypeOptions,
|
||||||
|
showLinkOptions,
|
||||||
|
keepAliveOptions,
|
||||||
|
hiddenTagOptions,
|
||||||
|
showParentOptions,
|
||||||
|
frameLoadingOptions
|
||||||
|
} from "./utils/enums";
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<FormProps>(), {
|
||||||
|
formInline: () => ({
|
||||||
|
menuType: 0,
|
||||||
|
higherMenuOptions: [],
|
||||||
|
parentId: 0,
|
||||||
|
title: "",
|
||||||
|
name: "",
|
||||||
|
path: "",
|
||||||
|
component: "",
|
||||||
|
rank: 99,
|
||||||
|
redirect: " ",
|
||||||
|
icon: "",
|
||||||
|
extraIcon: "",
|
||||||
|
enterTransition: "",
|
||||||
|
leaveTransition: "",
|
||||||
|
activePath: "",
|
||||||
|
auths: "",
|
||||||
|
frameSrc: "",
|
||||||
|
frameLoading: true,
|
||||||
|
keepAlive: false,
|
||||||
|
hiddenTag: false,
|
||||||
|
showLink: true,
|
||||||
|
showParent: false
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
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-row :gutter="30">
|
||||||
|
<re-col>
|
||||||
|
<el-form-item label="菜单类型">
|
||||||
|
<Segmented
|
||||||
|
v-model="newFormInline.menuType"
|
||||||
|
:options="menuTypeOptions"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col>
|
||||||
|
<el-form-item label="上级菜单">
|
||||||
|
<el-cascader
|
||||||
|
v-model="newFormInline.parentId"
|
||||||
|
class="w-full"
|
||||||
|
:options="newFormInline.higherMenuOptions"
|
||||||
|
:props="{
|
||||||
|
value: 'id',
|
||||||
|
label: 'title',
|
||||||
|
emitPath: false,
|
||||||
|
checkStrictly: true
|
||||||
|
}"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
placeholder="请选择上级菜单"
|
||||||
|
>
|
||||||
|
<template #default="{ node, data }">
|
||||||
|
<span>{{ transformI18n(data.title) }}</span>
|
||||||
|
<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
|
||||||
|
</template>
|
||||||
|
</el-cascader>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="菜单名称" prop="title">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.title"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入菜单名称"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col v-if="newFormInline.menuType !== 3" :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="路由名称" prop="name">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.name"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入路由名称"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col v-if="newFormInline.menuType !== 3" :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="路由路径" prop="path">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.path"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入路由路径"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col
|
||||||
|
v-show="newFormInline.menuType === 0"
|
||||||
|
:value="12"
|
||||||
|
:xs="24"
|
||||||
|
:sm="24"
|
||||||
|
>
|
||||||
|
<el-form-item label="组件路径">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.component"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入组件路径"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="菜单排序">
|
||||||
|
<el-input-number
|
||||||
|
v-model="newFormInline.rank"
|
||||||
|
class="!w-full"
|
||||||
|
:min="1"
|
||||||
|
:max="9999"
|
||||||
|
controls-position="right"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col
|
||||||
|
v-show="newFormInline.menuType === 0"
|
||||||
|
:value="12"
|
||||||
|
:xs="24"
|
||||||
|
:sm="24"
|
||||||
|
>
|
||||||
|
<el-form-item label="路由重定向">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.redirect"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入默认跳转地址"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col
|
||||||
|
v-show="newFormInline.menuType !== 3"
|
||||||
|
:value="12"
|
||||||
|
:xs="24"
|
||||||
|
:sm="24"
|
||||||
|
>
|
||||||
|
<el-form-item label="菜单图标">
|
||||||
|
<IconSelect v-model="newFormInline.icon" class="w-full" />
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col
|
||||||
|
v-show="newFormInline.menuType !== 3"
|
||||||
|
:value="12"
|
||||||
|
:xs="24"
|
||||||
|
:sm="24"
|
||||||
|
>
|
||||||
|
<el-form-item label="右侧图标">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.extraIcon"
|
||||||
|
clearable
|
||||||
|
placeholder="菜单名称右侧的额外图标"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col v-show="newFormInline.menuType < 2" :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="进场动画">
|
||||||
|
<ReAnimateSelector
|
||||||
|
v-model="newFormInline.enterTransition"
|
||||||
|
placeholder="请选择页面进场加载动画"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col v-show="newFormInline.menuType < 2" :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="离场动画">
|
||||||
|
<ReAnimateSelector
|
||||||
|
v-model="newFormInline.leaveTransition"
|
||||||
|
placeholder="请选择页面离场加载动画"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col
|
||||||
|
v-show="newFormInline.menuType === 0"
|
||||||
|
:value="12"
|
||||||
|
:xs="24"
|
||||||
|
:sm="24"
|
||||||
|
>
|
||||||
|
<el-form-item label="菜单激活">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.activePath"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入需要激活的菜单"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col v-if="newFormInline.menuType === 3" :value="12" :xs="24" :sm="24">
|
||||||
|
<!-- 按钮级别权限设置 -->
|
||||||
|
<el-form-item label="权限标识" prop="auths">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.auths"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入权限标识"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col
|
||||||
|
v-show="newFormInline.menuType === 1"
|
||||||
|
:value="12"
|
||||||
|
:xs="24"
|
||||||
|
:sm="24"
|
||||||
|
>
|
||||||
|
<!-- iframe -->
|
||||||
|
<el-form-item label="链接地址">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.frameSrc"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入 iframe 链接地址"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col v-if="newFormInline.menuType === 1" :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="加载动画">
|
||||||
|
<Segmented
|
||||||
|
:modelValue="newFormInline.frameLoading ? 0 : 1"
|
||||||
|
:options="frameLoadingOptions"
|
||||||
|
@change="
|
||||||
|
({ option: { value } }) => {
|
||||||
|
newFormInline.frameLoading = value;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col v-show="newFormInline.menuType < 2" :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="缓存页面">
|
||||||
|
<Segmented
|
||||||
|
:modelValue="newFormInline.keepAlive ? 0 : 1"
|
||||||
|
:options="keepAliveOptions"
|
||||||
|
@change="
|
||||||
|
({ option: { value } }) => {
|
||||||
|
newFormInline.keepAlive = value;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col v-show="newFormInline.menuType < 2" :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="标签页">
|
||||||
|
<Segmented
|
||||||
|
:modelValue="newFormInline.hiddenTag ? 1 : 0"
|
||||||
|
:options="hiddenTagOptions"
|
||||||
|
@change="
|
||||||
|
({ option: { value } }) => {
|
||||||
|
newFormInline.hiddenTag = value;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col
|
||||||
|
v-show="newFormInline.menuType !== 3"
|
||||||
|
:value="12"
|
||||||
|
:xs="24"
|
||||||
|
:sm="24"
|
||||||
|
>
|
||||||
|
<el-form-item label="菜单">
|
||||||
|
<Segmented
|
||||||
|
:modelValue="newFormInline.showLink ? 0 : 1"
|
||||||
|
:options="showLinkOptions"
|
||||||
|
@change="
|
||||||
|
({ option: { value } }) => {
|
||||||
|
newFormInline.showLink = value;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col
|
||||||
|
v-show="newFormInline.menuType !== 3"
|
||||||
|
:value="8"
|
||||||
|
:xs="24"
|
||||||
|
:sm="24"
|
||||||
|
>
|
||||||
|
<el-form-item label="父级菜单">
|
||||||
|
<Segmented
|
||||||
|
:modelValue="newFormInline.showParent ? 0 : 1"
|
||||||
|
:options="showParentOptions"
|
||||||
|
@change="
|
||||||
|
({ option: { value } }) => {
|
||||||
|
newFormInline.showParent = value;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
157
src/views/system/menu/index.vue
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useMenu } from "./utils/hook";
|
||||||
|
import { transformI18n } from "@/plugins/i18n";
|
||||||
|
import { PureTableBar } from "@/components/RePureTableBar";
|
||||||
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
|
|
||||||
|
import Delete from "@iconify-icons/ep/delete";
|
||||||
|
import EditPen from "@iconify-icons/ep/edit-pen";
|
||||||
|
import Refresh from "@iconify-icons/ep/refresh";
|
||||||
|
import AddFill from "@iconify-icons/ri/add-circle-line";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "SystemMenu"
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const tableRef = ref();
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
loading,
|
||||||
|
columns,
|
||||||
|
dataList,
|
||||||
|
onSearch,
|
||||||
|
resetForm,
|
||||||
|
openDialog,
|
||||||
|
handleDelete,
|
||||||
|
handleSelectionChange
|
||||||
|
} = useMenu();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="main">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:inline="true"
|
||||||
|
:model="form"
|
||||||
|
class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]"
|
||||||
|
>
|
||||||
|
<el-form-item label="菜单名称:" prop="title">
|
||||||
|
<el-input
|
||||||
|
v-model="form.title"
|
||||||
|
placeholder="请输入菜单名称"
|
||||||
|
clearable
|
||||||
|
class="!w-[180px]"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="useRenderIcon('ri:search-line')"
|
||||||
|
:loading="loading"
|
||||||
|
@click="onSearch"
|
||||||
|
>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<PureTableBar
|
||||||
|
title="菜单管理(初版,持续完善中)"
|
||||||
|
:columns="columns"
|
||||||
|
:isExpandAll="false"
|
||||||
|
:tableRef="tableRef?.getTableRef()"
|
||||||
|
@refresh="onSearch"
|
||||||
|
>
|
||||||
|
<template #buttons>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="useRenderIcon(AddFill)"
|
||||||
|
@click="openDialog()"
|
||||||
|
>
|
||||||
|
新增菜单
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<template v-slot="{ size, dynamicColumns }">
|
||||||
|
<pure-table
|
||||||
|
ref="tableRef"
|
||||||
|
adaptive
|
||||||
|
:adaptiveConfig="{ offsetBottom: 45 }"
|
||||||
|
align-whole="center"
|
||||||
|
row-key="id"
|
||||||
|
showOverflowTooltip
|
||||||
|
table-layout="auto"
|
||||||
|
:loading="loading"
|
||||||
|
:size="size"
|
||||||
|
:data="dataList"
|
||||||
|
:columns="dynamicColumns"
|
||||||
|
:header-cell-style="{
|
||||||
|
background: 'var(--el-fill-color-light)',
|
||||||
|
color: 'var(--el-text-color-primary)'
|
||||||
|
}"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<template #operation="{ row }">
|
||||||
|
<el-button
|
||||||
|
class="reset-margin"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(EditPen)"
|
||||||
|
@click="openDialog('修改', row)"
|
||||||
|
>
|
||||||
|
修改
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-show="row.menuType !== 3"
|
||||||
|
class="reset-margin"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(AddFill)"
|
||||||
|
@click="openDialog('新增', { parentId: row.id } as any)"
|
||||||
|
>
|
||||||
|
新增
|
||||||
|
</el-button>
|
||||||
|
<el-popconfirm
|
||||||
|
:title="`是否确认删除菜单名称为${transformI18n(row.title)}的这条数据${row?.children?.length > 0 ? '。注意下级菜单也会一并删除,请谨慎操作' : ''}`"
|
||||||
|
@confirm="handleDelete(row)"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button
|
||||||
|
class="reset-margin"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(Delete)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
</pure-table>
|
||||||
|
</template>
|
||||||
|
</PureTableBar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.el-table__inner-wrapper::before) {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin: 24px 24px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
94
src/views/system/menu/utils/enums.ts
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import type { OptionsType } from "@/components/ReSegmented";
|
||||||
|
|
||||||
|
const menuTypeOptions: Array<OptionsType> = [
|
||||||
|
{
|
||||||
|
label: "菜单",
|
||||||
|
value: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "iframe",
|
||||||
|
value: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "外链",
|
||||||
|
value: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "按钮",
|
||||||
|
value: 3
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const showLinkOptions: Array<OptionsType> = [
|
||||||
|
{
|
||||||
|
label: "显示",
|
||||||
|
tip: "会在菜单中显示",
|
||||||
|
value: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "隐藏",
|
||||||
|
tip: "不会在菜单中显示",
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const keepAliveOptions: Array<OptionsType> = [
|
||||||
|
{
|
||||||
|
label: "缓存",
|
||||||
|
tip: "会保存该页面的整体状态,刷新后会清空状态",
|
||||||
|
value: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "不缓存",
|
||||||
|
tip: "不会保存该页面的整体状态",
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const hiddenTagOptions: Array<OptionsType> = [
|
||||||
|
{
|
||||||
|
label: "允许",
|
||||||
|
tip: "当前菜单名称或自定义信息允许添加到标签页",
|
||||||
|
value: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "禁止",
|
||||||
|
tip: "当前菜单名称或自定义信息禁止添加到标签页",
|
||||||
|
value: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const showParentOptions: Array<OptionsType> = [
|
||||||
|
{
|
||||||
|
label: "显示",
|
||||||
|
tip: "会显示父级菜单",
|
||||||
|
value: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "隐藏",
|
||||||
|
tip: "不会显示父级菜单",
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const frameLoadingOptions: Array<OptionsType> = [
|
||||||
|
{
|
||||||
|
label: "开启",
|
||||||
|
tip: "有首次加载动画",
|
||||||
|
value: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "关闭",
|
||||||
|
tip: "无首次加载动画",
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export {
|
||||||
|
menuTypeOptions,
|
||||||
|
showLinkOptions,
|
||||||
|
keepAliveOptions,
|
||||||
|
hiddenTagOptions,
|
||||||
|
showParentOptions,
|
||||||
|
frameLoadingOptions
|
||||||
|
};
|
223
src/views/system/menu/utils/hook.tsx
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
import editForm from "../form.vue";
|
||||||
|
import { handleTree } from "@/utils/tree";
|
||||||
|
import { message } from "@/utils/message";
|
||||||
|
import { getMenuList } from "@/api/system";
|
||||||
|
import { transformI18n } from "@/plugins/i18n";
|
||||||
|
import { addDialog } from "@/components/ReDialog";
|
||||||
|
import { reactive, ref, onMounted, h } from "vue";
|
||||||
|
import type { FormItemProps } from "../utils/types";
|
||||||
|
import { cloneDeep, isAllEmpty } from "@pureadmin/utils";
|
||||||
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
|
|
||||||
|
export function useMenu() {
|
||||||
|
const form = reactive({
|
||||||
|
title: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const dataList = ref([]);
|
||||||
|
const loading = ref(true);
|
||||||
|
|
||||||
|
const getMenuType = (type, text = false) => {
|
||||||
|
switch (type) {
|
||||||
|
case 0:
|
||||||
|
return text ? "菜单" : "primary";
|
||||||
|
case 1:
|
||||||
|
return text ? "iframe" : "warning";
|
||||||
|
case 2:
|
||||||
|
return text ? "外链" : "danger";
|
||||||
|
case 3:
|
||||||
|
return text ? "按钮" : "info";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: TableColumnList = [
|
||||||
|
{
|
||||||
|
label: "菜单名称",
|
||||||
|
prop: "title",
|
||||||
|
align: "left",
|
||||||
|
cellRenderer: ({ row }) => (
|
||||||
|
<>
|
||||||
|
<span class="inline-block mr-1">
|
||||||
|
{h(useRenderIcon(row.icon), {
|
||||||
|
style: { paddingTop: "1px" }
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<span>{transformI18n(row.title)}</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "菜单类型",
|
||||||
|
prop: "menuType",
|
||||||
|
width: 100,
|
||||||
|
cellRenderer: ({ row, props }) => (
|
||||||
|
<el-tag
|
||||||
|
size={props.size}
|
||||||
|
type={getMenuType(row.menuType)}
|
||||||
|
effect="plain"
|
||||||
|
>
|
||||||
|
{getMenuType(row.menuType, true)}
|
||||||
|
</el-tag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "路由路径",
|
||||||
|
prop: "path"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "组件路径",
|
||||||
|
prop: "component",
|
||||||
|
formatter: ({ path, component }) =>
|
||||||
|
isAllEmpty(component) ? path : component
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "权限标识",
|
||||||
|
prop: "auths"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "排序",
|
||||||
|
prop: "rank",
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "隐藏",
|
||||||
|
prop: "showLink",
|
||||||
|
formatter: ({ showLink }) => (showLink ? "否" : "是"),
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "操作",
|
||||||
|
fixed: "right",
|
||||||
|
width: 210,
|
||||||
|
slot: "operation"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleSelectionChange(val) {
|
||||||
|
console.log("handleSelectionChange", val);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm(formEl) {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.resetFields();
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSearch() {
|
||||||
|
loading.value = true;
|
||||||
|
const { data } = await getMenuList(); // 这里是返回一维数组结构,前端自行处理成树结构,返回格式要求:唯一id加父节点parentId,parentId取父节点id
|
||||||
|
let newData = data;
|
||||||
|
if (!isAllEmpty(form.title)) {
|
||||||
|
// 前端搜索菜单名称
|
||||||
|
newData = newData.filter(item =>
|
||||||
|
transformI18n(item.title).includes(form.title)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
dataList.value = handleTree(newData); // 处理成树结构
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatHigherMenuOptions(treeList) {
|
||||||
|
if (!treeList || !treeList.length) return;
|
||||||
|
const newTreeList = [];
|
||||||
|
for (let i = 0; i < treeList.length; i++) {
|
||||||
|
treeList[i].title = transformI18n(treeList[i].title);
|
||||||
|
formatHigherMenuOptions(treeList[i].children);
|
||||||
|
newTreeList.push(treeList[i]);
|
||||||
|
}
|
||||||
|
return newTreeList;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDialog(title = "新增", row?: FormItemProps) {
|
||||||
|
addDialog({
|
||||||
|
title: `${title}菜单`,
|
||||||
|
props: {
|
||||||
|
formInline: {
|
||||||
|
menuType: row?.menuType ?? 0,
|
||||||
|
higherMenuOptions: formatHigherMenuOptions(cloneDeep(dataList.value)),
|
||||||
|
parentId: row?.parentId ?? 0,
|
||||||
|
title: row?.title ?? "",
|
||||||
|
name: row?.name ?? "",
|
||||||
|
path: row?.path ?? "",
|
||||||
|
component: row?.component ?? "",
|
||||||
|
rank: row?.rank ?? 99,
|
||||||
|
redirect: row?.redirect ?? "",
|
||||||
|
icon: row?.icon ?? "",
|
||||||
|
extraIcon: row?.extraIcon ?? "",
|
||||||
|
enterTransition: row?.enterTransition ?? "",
|
||||||
|
leaveTransition: row?.leaveTransition ?? "",
|
||||||
|
activePath: row?.activePath ?? "",
|
||||||
|
auths: row?.auths ?? "",
|
||||||
|
frameSrc: row?.frameSrc ?? "",
|
||||||
|
frameLoading: row?.frameLoading ?? true,
|
||||||
|
keepAlive: row?.keepAlive ?? false,
|
||||||
|
hiddenTag: row?.hiddenTag ?? false,
|
||||||
|
showLink: row?.showLink ?? true,
|
||||||
|
showParent: row?.showParent ?? false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
width: "45%",
|
||||||
|
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}了菜单名称为${transformI18n(curData.title)}的这条数据`,
|
||||||
|
{
|
||||||
|
type: "success"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
done(); // 关闭弹框
|
||||||
|
onSearch(); // 刷新表格数据
|
||||||
|
}
|
||||||
|
FormRef.validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
console.log("curData", curData);
|
||||||
|
// 表单规则校验通过
|
||||||
|
if (title === "新增") {
|
||||||
|
// 实际开发先调用新增接口,再进行下面操作
|
||||||
|
chores();
|
||||||
|
} else {
|
||||||
|
// 实际开发先调用修改接口,再进行下面操作
|
||||||
|
chores();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete(row) {
|
||||||
|
message(`您删除了菜单名称为${transformI18n(row.title)}的这条数据`, {
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
onSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
loading,
|
||||||
|
columns,
|
||||||
|
dataList,
|
||||||
|
/** 搜索 */
|
||||||
|
onSearch,
|
||||||
|
/** 重置 */
|
||||||
|
resetForm,
|
||||||
|
/** 新增、修改菜单 */
|
||||||
|
openDialog,
|
||||||
|
/** 删除菜单 */
|
||||||
|
handleDelete,
|
||||||
|
handleSelectionChange
|
||||||
|
};
|
||||||
|
}
|
10
src/views/system/menu/utils/rule.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { reactive } from "vue";
|
||||||
|
import type { FormRules } from "element-plus";
|
||||||
|
|
||||||
|
/** 自定义表单规则校验 */
|
||||||
|
export const formRules = reactive(<FormRules>{
|
||||||
|
title: [{ required: true, message: "菜单名称为必填项", trigger: "blur" }],
|
||||||
|
name: [{ required: true, message: "路由名称为必填项", trigger: "blur" }],
|
||||||
|
path: [{ required: true, message: "路由路径为必填项", trigger: "blur" }],
|
||||||
|
auths: [{ required: true, message: "权限标识为必填项", trigger: "blur" }]
|
||||||
|
});
|
29
src/views/system/menu/utils/types.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
interface FormItemProps {
|
||||||
|
/** 菜单类型(0代表菜单、1代表iframe、2代表外链、3代表按钮)*/
|
||||||
|
menuType: number;
|
||||||
|
higherMenuOptions: Record<string, unknown>[];
|
||||||
|
parentId: number;
|
||||||
|
title: string;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
component: string;
|
||||||
|
rank: number;
|
||||||
|
redirect: string;
|
||||||
|
icon: string;
|
||||||
|
extraIcon: string;
|
||||||
|
enterTransition: string;
|
||||||
|
leaveTransition: string;
|
||||||
|
activePath: string;
|
||||||
|
auths: string;
|
||||||
|
frameSrc: string;
|
||||||
|
frameLoading: boolean;
|
||||||
|
keepAlive: boolean;
|
||||||
|
hiddenTag: boolean;
|
||||||
|
showLink: boolean;
|
||||||
|
showParent: boolean;
|
||||||
|
}
|
||||||
|
interface FormProps {
|
||||||
|
formInline: FormItemProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { FormItemProps, FormProps };
|
55
src/views/system/role/form.vue
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<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>
|
219
src/views/system/role/index.vue
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
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 Refresh from "@iconify-icons/ep/refresh";
|
||||||
|
import Menu from "@iconify-icons/ep/menu";
|
||||||
|
import AddFill from "@iconify-icons/ri/add-circle-line";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "SystemRole"
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
loading,
|
||||||
|
columns,
|
||||||
|
dataList,
|
||||||
|
pagination,
|
||||||
|
// buttonClass,
|
||||||
|
onSearch,
|
||||||
|
resetForm,
|
||||||
|
openDialog,
|
||||||
|
handleMenu,
|
||||||
|
handleDelete,
|
||||||
|
// handleDatabase,
|
||||||
|
handleSizeChange,
|
||||||
|
handleCurrentChange,
|
||||||
|
handleSelectionChange
|
||||||
|
} = useRole();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="main">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:inline="true"
|
||||||
|
:model="form"
|
||||||
|
class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]"
|
||||||
|
>
|
||||||
|
<el-form-item label="角色名称:" prop="name">
|
||||||
|
<el-input
|
||||||
|
v-model="form.name"
|
||||||
|
placeholder="请输入角色名称"
|
||||||
|
clearable
|
||||||
|
class="!w-[180px]"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="角色标识:" prop="code">
|
||||||
|
<el-input
|
||||||
|
v-model="form.code"
|
||||||
|
placeholder="请输入角色标识"
|
||||||
|
clearable
|
||||||
|
class="!w-[180px]"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态:" prop="status">
|
||||||
|
<el-select
|
||||||
|
v-model="form.status"
|
||||||
|
placeholder="请选择状态"
|
||||||
|
clearable
|
||||||
|
class="!w-[180px]"
|
||||||
|
>
|
||||||
|
<el-option label="已启用" value="1" />
|
||||||
|
<el-option label="已停用" value="0" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="useRenderIcon('ri:search-line')"
|
||||||
|
:loading="loading"
|
||||||
|
@click="onSearch"
|
||||||
|
>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<PureTableBar
|
||||||
|
title="角色管理(仅演示,操作后不生效)"
|
||||||
|
:columns="columns"
|
||||||
|
@refresh="onSearch"
|
||||||
|
>
|
||||||
|
<template #buttons>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="useRenderIcon(AddFill)"
|
||||||
|
@click="openDialog()"
|
||||||
|
>
|
||||||
|
新增角色
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<template v-slot="{ size, dynamicColumns }">
|
||||||
|
<pure-table
|
||||||
|
align-whole="center"
|
||||||
|
showOverflowTooltip
|
||||||
|
table-layout="auto"
|
||||||
|
:loading="loading"
|
||||||
|
:size="size"
|
||||||
|
adaptive
|
||||||
|
:adaptiveConfig="{ offsetBottom: 108 }"
|
||||||
|
:data="dataList"
|
||||||
|
:columns="dynamicColumns"
|
||||||
|
:pagination="pagination"
|
||||||
|
:paginationSmall="size === 'small' ? true : false"
|
||||||
|
:header-cell-style="{
|
||||||
|
background: 'var(--el-fill-color-light)',
|
||||||
|
color: 'var(--el-text-color-primary)'
|
||||||
|
}"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
@page-size-change="handleSizeChange"
|
||||||
|
@page-current-change="handleCurrentChange"
|
||||||
|
>
|
||||||
|
<template #operation="{ row }">
|
||||||
|
<el-button
|
||||||
|
class="reset-margin"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(EditPen)"
|
||||||
|
@click="openDialog('修改', 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}的这条数据`"
|
||||||
|
@confirm="handleDelete(row)"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button
|
||||||
|
class="reset-margin"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(Delete)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
<!-- <el-dropdown>
|
||||||
|
<el-button
|
||||||
|
class="ml-3 mt-[2px]"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(More)"
|
||||||
|
/>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item>
|
||||||
|
<el-button
|
||||||
|
:class="buttonClass"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(Menu)"
|
||||||
|
@click="handleMenu"
|
||||||
|
>
|
||||||
|
菜单权限
|
||||||
|
</el-button>
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item>
|
||||||
|
<el-button
|
||||||
|
:class="buttonClass"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(Database)"
|
||||||
|
@click="handleDatabase"
|
||||||
|
>
|
||||||
|
数据权限
|
||||||
|
</el-button>
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown> -->
|
||||||
|
</template>
|
||||||
|
</pure-table>
|
||||||
|
</template>
|
||||||
|
</PureTableBar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:deep(.el-dropdown-menu__item i) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin: 24px 24px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
241
src/views/system/role/utils/hook.tsx
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
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 { 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";
|
||||||
|
|
||||||
|
export function useRole() {
|
||||||
|
const form = reactive({
|
||||||
|
name: "",
|
||||||
|
code: "",
|
||||||
|
status: ""
|
||||||
|
});
|
||||||
|
const formRef = ref();
|
||||||
|
const dataList = ref([]);
|
||||||
|
const loading = ref(true);
|
||||||
|
const switchLoadMap = ref({});
|
||||||
|
const { switchStyle } = usePublicHooks();
|
||||||
|
const pagination = reactive<PaginationProps>({
|
||||||
|
total: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
currentPage: 1,
|
||||||
|
background: true
|
||||||
|
});
|
||||||
|
const columns: TableColumnList = [
|
||||||
|
{
|
||||||
|
label: "角色编号",
|
||||||
|
prop: "id",
|
||||||
|
minWidth: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "角色名称",
|
||||||
|
prop: "name",
|
||||||
|
minWidth: 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "角色标识",
|
||||||
|
prop: "code",
|
||||||
|
minWidth: 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "状态",
|
||||||
|
minWidth: 130,
|
||||||
|
cellRenderer: scope => (
|
||||||
|
<el-switch
|
||||||
|
size={scope.props.size === "small" ? "small" : "default"}
|
||||||
|
loading={switchLoadMap.value[scope.index]?.loading}
|
||||||
|
v-model={scope.row.status}
|
||||||
|
active-value={1}
|
||||||
|
inactive-value={0}
|
||||||
|
active-text="已启用"
|
||||||
|
inactive-text="已停用"
|
||||||
|
inline-prompt
|
||||||
|
style={switchStyle.value}
|
||||||
|
onChange={() => onChange(scope as any)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "备注",
|
||||||
|
prop: "remark",
|
||||||
|
minWidth: 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "创建时间",
|
||||||
|
minWidth: 180,
|
||||||
|
prop: "createTime",
|
||||||
|
formatter: ({ createTime }) =>
|
||||||
|
dayjs(createTime).format("YYYY-MM-DD HH:mm:ss")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "操作",
|
||||||
|
fixed: "right",
|
||||||
|
width: 240,
|
||||||
|
slot: "operation"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// const buttonClass = computed(() => {
|
||||||
|
// return [
|
||||||
|
// "!h-[20px]",
|
||||||
|
// "reset-margin",
|
||||||
|
// "!text-gray-500",
|
||||||
|
// "dark:!text-white",
|
||||||
|
// "dark:hover:!text-primary"
|
||||||
|
// ];
|
||||||
|
// });
|
||||||
|
|
||||||
|
function onChange({ row, index }) {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
`确认要<strong>${
|
||||||
|
row.status === 0 ? "停用" : "启用"
|
||||||
|
}</strong><strong style='color:var(--el-color-primary)'>${
|
||||||
|
row.name
|
||||||
|
}</strong>吗?`,
|
||||||
|
"系统提示",
|
||||||
|
{
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "warning",
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
draggable: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
switchLoadMap.value[index] = Object.assign(
|
||||||
|
{},
|
||||||
|
switchLoadMap.value[index],
|
||||||
|
{
|
||||||
|
loading: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
switchLoadMap.value[index] = Object.assign(
|
||||||
|
{},
|
||||||
|
switchLoadMap.value[index],
|
||||||
|
{
|
||||||
|
loading: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
message(`已${row.status === 0 ? "停用" : "启用"}${row.name}`, {
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
row.status === 0 ? (row.status = 1) : (row.status = 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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(() => {
|
||||||
|
loading.value = false;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = formEl => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.resetFields();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 菜单权限 */
|
||||||
|
function handleMenu() {
|
||||||
|
message("等菜单管理页面开发后完善");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 数据权限 可自行开发 */
|
||||||
|
// function handleDatabase() {}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
onSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
loading,
|
||||||
|
columns,
|
||||||
|
dataList,
|
||||||
|
pagination,
|
||||||
|
// buttonClass,
|
||||||
|
onSearch,
|
||||||
|
resetForm,
|
||||||
|
openDialog,
|
||||||
|
handleMenu,
|
||||||
|
handleDelete,
|
||||||
|
// handleDatabase,
|
||||||
|
handleSizeChange,
|
||||||
|
handleCurrentChange,
|
||||||
|
handleSelectionChange
|
||||||
|
};
|
||||||
|
}
|
8
src/views/system/role/utils/rule.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
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" }]
|
||||||
|
});
|
15
src/views/system/role/utils/types.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// 虽然字段很少 但是抽离出来 后续有扩展字段需求就很方便了
|
||||||
|
|
||||||
|
interface FormItemProps {
|
||||||
|
/** 角色名称 */
|
||||||
|
name: string;
|
||||||
|
/** 角色编号 */
|
||||||
|
code: string;
|
||||||
|
/** 备注 */
|
||||||
|
remark: string;
|
||||||
|
}
|
||||||
|
interface FormProps {
|
||||||
|
formInline: FormItemProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { FormItemProps, FormProps };
|
176
src/views/system/user/form/index.vue
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import ReCol from "@/components/ReCol";
|
||||||
|
import { formRules } from "../utils/rule";
|
||||||
|
import { FormProps } from "../utils/types";
|
||||||
|
import { usePublicHooks } from "../../hooks";
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<FormProps>(), {
|
||||||
|
formInline: () => ({
|
||||||
|
title: "新增",
|
||||||
|
higherDeptOptions: [],
|
||||||
|
parentId: 0,
|
||||||
|
nickname: "",
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
phone: "",
|
||||||
|
email: "",
|
||||||
|
sex: "",
|
||||||
|
status: 1,
|
||||||
|
remark: ""
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const sexOptions = [
|
||||||
|
{
|
||||||
|
value: 0,
|
||||||
|
label: "男"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 1,
|
||||||
|
label: "女"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const ruleFormRef = ref();
|
||||||
|
const { switchStyle } = usePublicHooks();
|
||||||
|
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-row :gutter="30">
|
||||||
|
<re-col :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="用户昵称" prop="nickname">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.nickname"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入用户昵称"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="用户名称" prop="username">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.username"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入用户名称"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col
|
||||||
|
v-if="newFormInline.title === '新增'"
|
||||||
|
:value="12"
|
||||||
|
:xs="24"
|
||||||
|
:sm="24"
|
||||||
|
>
|
||||||
|
<el-form-item label="用户密码" prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.password"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入用户密码"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="手机号" prop="phone">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.phone"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.email"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入邮箱"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="用户性别">
|
||||||
|
<el-select
|
||||||
|
v-model="newFormInline.sex"
|
||||||
|
placeholder="请选择用户性别"
|
||||||
|
class="w-full"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="(item, index) in sexOptions"
|
||||||
|
:key="index"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="归属部门">
|
||||||
|
<el-cascader
|
||||||
|
v-model="newFormInline.parentId"
|
||||||
|
class="w-full"
|
||||||
|
:options="newFormInline.higherDeptOptions"
|
||||||
|
:props="{
|
||||||
|
value: 'id',
|
||||||
|
label: 'name',
|
||||||
|
emitPath: false,
|
||||||
|
checkStrictly: true
|
||||||
|
}"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
placeholder="请选择归属部门"
|
||||||
|
>
|
||||||
|
<template #default="{ node, data }">
|
||||||
|
<span>{{ data.name }}</span>
|
||||||
|
<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
|
||||||
|
</template>
|
||||||
|
</el-cascader>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col
|
||||||
|
v-if="newFormInline.title === '新增'"
|
||||||
|
:value="12"
|
||||||
|
:xs="24"
|
||||||
|
:sm="24"
|
||||||
|
>
|
||||||
|
<el-form-item label="用户状态">
|
||||||
|
<el-switch
|
||||||
|
v-model="newFormInline.status"
|
||||||
|
inline-prompt
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
active-text="启用"
|
||||||
|
inactive-text="停用"
|
||||||
|
:style="switchStyle"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.remark"
|
||||||
|
placeholder="请输入备注信息"
|
||||||
|
type="textarea"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
53
src/views/system/user/form/role.vue
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import ReCol from "@/components/ReCol";
|
||||||
|
import { RoleFormProps } from "../utils/types";
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<RoleFormProps>(), {
|
||||||
|
formInline: () => ({
|
||||||
|
username: "",
|
||||||
|
nickname: "",
|
||||||
|
roleOptions: [],
|
||||||
|
ids: []
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const newFormInline = ref(props.formInline);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-form :model="newFormInline">
|
||||||
|
<el-row :gutter="30">
|
||||||
|
<!-- <re-col>
|
||||||
|
<el-form-item label="用户名称" prop="username">
|
||||||
|
<el-input disabled v-model="newFormInline.username" />
|
||||||
|
</el-form-item>
|
||||||
|
</re-col> -->
|
||||||
|
<re-col>
|
||||||
|
<el-form-item label="用户昵称" prop="nickname">
|
||||||
|
<el-input v-model="newFormInline.nickname" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col>
|
||||||
|
<el-form-item label="角色列表" prop="ids">
|
||||||
|
<el-select
|
||||||
|
v-model="newFormInline.ids"
|
||||||
|
placeholder="请选择"
|
||||||
|
class="w-full"
|
||||||
|
clearable
|
||||||
|
multiple
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="(item, index) in newFormInline.roleOptions"
|
||||||
|
:key="index"
|
||||||
|
:value="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
273
src/views/system/user/index.vue
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import tree from "./tree.vue";
|
||||||
|
import { useUser } from "./utils/hook";
|
||||||
|
import { PureTableBar } from "@/components/RePureTableBar";
|
||||||
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
|
|
||||||
|
import Upload from "@iconify-icons/ri/upload-line";
|
||||||
|
import Role from "@iconify-icons/ri/admin-line";
|
||||||
|
import Password from "@iconify-icons/ri/lock-password-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 Refresh from "@iconify-icons/ep/refresh";
|
||||||
|
import AddFill from "@iconify-icons/ri/add-circle-line";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "SystemUser"
|
||||||
|
});
|
||||||
|
|
||||||
|
const treeRef = ref();
|
||||||
|
const formRef = ref();
|
||||||
|
const tableRef = ref();
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
loading,
|
||||||
|
columns,
|
||||||
|
dataList,
|
||||||
|
treeData,
|
||||||
|
treeLoading,
|
||||||
|
selectedNum,
|
||||||
|
pagination,
|
||||||
|
buttonClass,
|
||||||
|
onSearch,
|
||||||
|
resetForm,
|
||||||
|
onbatchDel,
|
||||||
|
openDialog,
|
||||||
|
onTreeSelect,
|
||||||
|
handleUpdate,
|
||||||
|
handleDelete,
|
||||||
|
handleUpload,
|
||||||
|
handleReset,
|
||||||
|
handleRole,
|
||||||
|
handleSizeChange,
|
||||||
|
onSelectionCancel,
|
||||||
|
handleCurrentChange,
|
||||||
|
handleSelectionChange
|
||||||
|
} = useUser(tableRef, treeRef);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<tree
|
||||||
|
ref="treeRef"
|
||||||
|
class="min-w-[200px] mr-2"
|
||||||
|
:treeData="treeData"
|
||||||
|
:treeLoading="treeLoading"
|
||||||
|
@tree-select="onTreeSelect"
|
||||||
|
/>
|
||||||
|
<div class="w-[calc(100%-200px)]">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:inline="true"
|
||||||
|
:model="form"
|
||||||
|
class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]"
|
||||||
|
>
|
||||||
|
<el-form-item label="用户名称:" prop="username">
|
||||||
|
<el-input
|
||||||
|
v-model="form.username"
|
||||||
|
placeholder="请输入用户名称"
|
||||||
|
clearable
|
||||||
|
class="!w-[180px]"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机号码:" prop="phone">
|
||||||
|
<el-input
|
||||||
|
v-model="form.phone"
|
||||||
|
placeholder="请输入手机号码"
|
||||||
|
clearable
|
||||||
|
class="!w-[180px]"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态:" prop="status">
|
||||||
|
<el-select
|
||||||
|
v-model="form.status"
|
||||||
|
placeholder="请选择"
|
||||||
|
clearable
|
||||||
|
class="!w-[180px]"
|
||||||
|
>
|
||||||
|
<el-option label="已开启" value="1" />
|
||||||
|
<el-option label="已关闭" value="0" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="useRenderIcon('ri:search-line')"
|
||||||
|
:loading="loading"
|
||||||
|
@click="onSearch"
|
||||||
|
>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<PureTableBar
|
||||||
|
title="用户管理(仅演示,操作后不生效)"
|
||||||
|
:columns="columns"
|
||||||
|
@refresh="onSearch"
|
||||||
|
>
|
||||||
|
<template #buttons>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="useRenderIcon(AddFill)"
|
||||||
|
@click="openDialog()"
|
||||||
|
>
|
||||||
|
新增用户
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<template v-slot="{ size, dynamicColumns }">
|
||||||
|
<div
|
||||||
|
v-if="selectedNum > 0"
|
||||||
|
v-motion-fade
|
||||||
|
class="bg-[var(--el-fill-color-light)] w-full h-[46px] mb-2 pl-4 flex items-center"
|
||||||
|
>
|
||||||
|
<div class="flex-auto">
|
||||||
|
<span
|
||||||
|
style="font-size: var(--el-font-size-base)"
|
||||||
|
class="text-[rgba(42,46,54,0.5)] dark:text-[rgba(220,220,242,0.5)]"
|
||||||
|
>
|
||||||
|
已选 {{ selectedNum }} 项
|
||||||
|
</span>
|
||||||
|
<el-button type="primary" text @click="onSelectionCancel">
|
||||||
|
取消选择
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-popconfirm title="是否确认删除?" @confirm="onbatchDel">
|
||||||
|
<template #reference>
|
||||||
|
<el-button type="danger" text class="mr-1">
|
||||||
|
批量删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</div>
|
||||||
|
<pure-table
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
adaptive
|
||||||
|
:adaptiveConfig="{ offsetBottom: 108 }"
|
||||||
|
align-whole="center"
|
||||||
|
table-layout="auto"
|
||||||
|
:loading="loading"
|
||||||
|
:size="size"
|
||||||
|
:data="dataList"
|
||||||
|
:columns="dynamicColumns"
|
||||||
|
:pagination="pagination"
|
||||||
|
:paginationSmall="size === 'small' ? true : false"
|
||||||
|
:header-cell-style="{
|
||||||
|
background: 'var(--el-fill-color-light)',
|
||||||
|
color: 'var(--el-text-color-primary)'
|
||||||
|
}"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
@page-size-change="handleSizeChange"
|
||||||
|
@page-current-change="handleCurrentChange"
|
||||||
|
>
|
||||||
|
<template #operation="{ row }">
|
||||||
|
<el-button
|
||||||
|
class="reset-margin"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(EditPen)"
|
||||||
|
@click="openDialog('修改', row)"
|
||||||
|
>
|
||||||
|
修改
|
||||||
|
</el-button>
|
||||||
|
<el-popconfirm
|
||||||
|
:title="`是否确认删除用户编号为${row.id}的这条数据`"
|
||||||
|
@confirm="handleDelete(row)"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button
|
||||||
|
class="reset-margin"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(Delete)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
<el-dropdown>
|
||||||
|
<el-button
|
||||||
|
class="ml-3 mt-[2px]"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(More)"
|
||||||
|
@click="handleUpdate(row)"
|
||||||
|
/>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item>
|
||||||
|
<el-button
|
||||||
|
:class="buttonClass"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(Upload)"
|
||||||
|
@click="handleUpload(row)"
|
||||||
|
>
|
||||||
|
上传头像
|
||||||
|
</el-button>
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item>
|
||||||
|
<el-button
|
||||||
|
:class="buttonClass"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(Password)"
|
||||||
|
@click="handleReset(row)"
|
||||||
|
>
|
||||||
|
重置密码
|
||||||
|
</el-button>
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item>
|
||||||
|
<el-button
|
||||||
|
:class="buttonClass"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(Role)"
|
||||||
|
@click="handleRole(row)"
|
||||||
|
>
|
||||||
|
分配角色
|
||||||
|
</el-button>
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</template>
|
||||||
|
</pure-table>
|
||||||
|
</template>
|
||||||
|
</PureTableBar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:deep(.el-dropdown-menu__item i) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-button:focus-visible) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin: 24px 24px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
1
src/views/system/user/svg/expand.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M22 4V2H2v2h9v14.17l-5.5-5.5-1.42 1.41L12 22l7.92-7.92-1.42-1.41-5.5 5.5V4z"/></svg>
|
After Width: | Height: | Size: 161 B |
1
src/views/system/user/svg/unexpand.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M4 2H2v20h2v-9h14.17l-5.5 5.5 1.41 1.42L22 12l-7.92-7.92-1.41 1.42 5.5 5.5H4z"/></svg>
|
After Width: | Height: | Size: 163 B |
212
src/views/system/user/tree.vue
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
|
import { ref, computed, watch, getCurrentInstance } from "vue";
|
||||||
|
|
||||||
|
import Dept from "@iconify-icons/ri/git-branch-line";
|
||||||
|
// import Reset from "@iconify-icons/ri/restart-line";
|
||||||
|
import More2Fill from "@iconify-icons/ri/more-2-fill";
|
||||||
|
import OfficeBuilding from "@iconify-icons/ep/office-building";
|
||||||
|
import LocationCompany from "@iconify-icons/ep/add-location";
|
||||||
|
import ExpandIcon from "./svg/expand.svg?component";
|
||||||
|
import UnExpandIcon from "./svg/unexpand.svg?component";
|
||||||
|
|
||||||
|
interface Tree {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
highlight?: boolean;
|
||||||
|
children?: Tree[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
treeLoading: Boolean,
|
||||||
|
treeData: Array
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["tree-select"]);
|
||||||
|
|
||||||
|
const treeRef = ref();
|
||||||
|
const isExpand = ref(true);
|
||||||
|
const searchValue = ref("");
|
||||||
|
const highlightMap = ref({});
|
||||||
|
const { proxy } = getCurrentInstance();
|
||||||
|
const defaultProps = {
|
||||||
|
children: "children",
|
||||||
|
label: "name"
|
||||||
|
};
|
||||||
|
const buttonClass = computed(() => {
|
||||||
|
return [
|
||||||
|
"!h-[20px]",
|
||||||
|
"reset-margin",
|
||||||
|
"!text-gray-500",
|
||||||
|
"dark:!text-white",
|
||||||
|
"dark:hover:!text-primary"
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const filterNode = (value: string, data: Tree) => {
|
||||||
|
if (!value) return true;
|
||||||
|
return data.name.includes(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
function nodeClick(value) {
|
||||||
|
const nodeId = value.$treeNodeId;
|
||||||
|
highlightMap.value[nodeId] = highlightMap.value[nodeId]?.highlight
|
||||||
|
? Object.assign({ id: nodeId }, highlightMap.value[nodeId], {
|
||||||
|
highlight: false
|
||||||
|
})
|
||||||
|
: Object.assign({ id: nodeId }, highlightMap.value[nodeId], {
|
||||||
|
highlight: true
|
||||||
|
});
|
||||||
|
Object.values(highlightMap.value).forEach((v: Tree) => {
|
||||||
|
if (v.id !== nodeId) {
|
||||||
|
v.highlight = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
emit(
|
||||||
|
"tree-select",
|
||||||
|
highlightMap.value[nodeId]?.highlight
|
||||||
|
? Object.assign({ ...value, selected: true })
|
||||||
|
: Object.assign({ ...value, selected: false })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRowExpansionAll(status) {
|
||||||
|
isExpand.value = status;
|
||||||
|
const nodes = (proxy.$refs["treeRef"] as any).store._getAllNodes();
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
nodes[i].expanded = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置部门树状态(选中状态、搜索框值、树初始化) */
|
||||||
|
function onTreeReset() {
|
||||||
|
highlightMap.value = {};
|
||||||
|
searchValue.value = "";
|
||||||
|
toggleRowExpansionAll(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(searchValue, val => {
|
||||||
|
treeRef.value!.filter(val);
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({ onTreeReset });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-loading="props.treeLoading"
|
||||||
|
class="h-full bg-bg_color overflow-auto"
|
||||||
|
:style="{ minHeight: `calc(100vh - 145px)` }"
|
||||||
|
>
|
||||||
|
<div class="flex items-center h-[34px]">
|
||||||
|
<el-input
|
||||||
|
v-model="searchValue"
|
||||||
|
class="ml-2"
|
||||||
|
size="small"
|
||||||
|
placeholder="请输入部门名称"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<el-icon class="el-input__icon">
|
||||||
|
<IconifyIconOffline
|
||||||
|
v-show="searchValue.length === 0"
|
||||||
|
icon="ri:search-line"
|
||||||
|
/>
|
||||||
|
</el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-dropdown :hide-on-click="false">
|
||||||
|
<IconifyIconOffline
|
||||||
|
class="w-[28px] cursor-pointer"
|
||||||
|
width="18px"
|
||||||
|
:icon="More2Fill"
|
||||||
|
/>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item>
|
||||||
|
<el-button
|
||||||
|
:class="buttonClass"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:icon="useRenderIcon(isExpand ? ExpandIcon : UnExpandIcon)"
|
||||||
|
@click="toggleRowExpansionAll(isExpand ? false : true)"
|
||||||
|
>
|
||||||
|
{{ isExpand ? "折叠全部" : "展开全部" }}
|
||||||
|
</el-button>
|
||||||
|
</el-dropdown-item>
|
||||||
|
<!-- <el-dropdown-item>
|
||||||
|
<el-button
|
||||||
|
:class="buttonClass"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:icon="useRenderIcon(Reset)"
|
||||||
|
@click="onTreeReset"
|
||||||
|
>
|
||||||
|
重置状态
|
||||||
|
</el-button>
|
||||||
|
</el-dropdown-item> -->
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
<el-divider />
|
||||||
|
<el-tree
|
||||||
|
ref="treeRef"
|
||||||
|
:data="props.treeData"
|
||||||
|
node-key="id"
|
||||||
|
size="small"
|
||||||
|
:props="defaultProps"
|
||||||
|
default-expand-all
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
:filter-node-method="filterNode"
|
||||||
|
@node-click="nodeClick"
|
||||||
|
>
|
||||||
|
<template #default="{ node, data }">
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'pl-1',
|
||||||
|
'pr-1',
|
||||||
|
'rounded',
|
||||||
|
'flex',
|
||||||
|
'items-center',
|
||||||
|
'select-none',
|
||||||
|
'hover:text-primary',
|
||||||
|
searchValue.trim().length > 0 &&
|
||||||
|
node.label.includes(searchValue) &&
|
||||||
|
'text-red-500',
|
||||||
|
highlightMap[node.id]?.highlight ? 'dark:text-primary' : ''
|
||||||
|
]"
|
||||||
|
:style="{
|
||||||
|
color: highlightMap[node.id]?.highlight
|
||||||
|
? 'var(--el-color-primary)'
|
||||||
|
: '',
|
||||||
|
background: highlightMap[node.id]?.highlight
|
||||||
|
? 'var(--el-color-primary-light-7)'
|
||||||
|
: 'transparent'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<IconifyIconOffline
|
||||||
|
:icon="
|
||||||
|
data.type === 1
|
||||||
|
? OfficeBuilding
|
||||||
|
: data.type === 2
|
||||||
|
? LocationCompany
|
||||||
|
: Dept
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
{{ node.label }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.el-divider) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tree) {
|
||||||
|
--el-tree-node-hover-bg-color: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
72
src/views/system/user/upload.vue
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<script setup lang="tsx">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import ReCropper from "@/components/ReCropper";
|
||||||
|
import { formatBytes } from "@pureadmin/utils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
imgSrc: String
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["cropper"]);
|
||||||
|
|
||||||
|
const infos = ref();
|
||||||
|
const popoverRef = ref();
|
||||||
|
const refCropper = ref();
|
||||||
|
const showPopover = ref(false);
|
||||||
|
const cropperImg = ref<string>("");
|
||||||
|
|
||||||
|
function onCropper({ base64, blob, info }) {
|
||||||
|
infos.value = info;
|
||||||
|
cropperImg.value = base64;
|
||||||
|
emit("cropper", { base64, blob, info });
|
||||||
|
}
|
||||||
|
|
||||||
|
function hidePopover() {
|
||||||
|
popoverRef.value.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ hidePopover });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-loading="!showPopover" element-loading-background="transparent">
|
||||||
|
<el-popover
|
||||||
|
ref="popoverRef"
|
||||||
|
:visible="showPopover"
|
||||||
|
placement="right"
|
||||||
|
width="18vw"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<div class="w-[18vw]">
|
||||||
|
<ReCropper
|
||||||
|
ref="refCropper"
|
||||||
|
:src="props.imgSrc"
|
||||||
|
circled
|
||||||
|
@cropper="onCropper"
|
||||||
|
@readied="showPopover = true"
|
||||||
|
/>
|
||||||
|
<p v-show="showPopover" class="mt-1 text-center">
|
||||||
|
温馨提示:右键上方裁剪区可开启功能菜单
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="flex flex-wrap justify-center items-center text-center">
|
||||||
|
<el-image
|
||||||
|
v-if="cropperImg"
|
||||||
|
:src="cropperImg"
|
||||||
|
:preview-src-list="Array.of(cropperImg)"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<div v-if="infos" class="mt-1">
|
||||||
|
<p>
|
||||||
|
图像大小:{{ parseInt(infos.width) }} ×
|
||||||
|
{{ parseInt(infos.height) }}像素
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
文件大小:{{ formatBytes(infos.size) }}({{ infos.size }} 字节)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-popover>
|
||||||
|
</div>
|
||||||
|
</template>
|
525
src/views/system/user/utils/hook.tsx
Normal file
@ -0,0 +1,525 @@
|
|||||||
|
import "./reset.css";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import roleForm from "../form/role.vue";
|
||||||
|
import editForm from "../form/index.vue";
|
||||||
|
import { zxcvbn } from "@zxcvbn-ts/core";
|
||||||
|
import { handleTree } from "@/utils/tree";
|
||||||
|
import { message } from "@/utils/message";
|
||||||
|
import croppingUpload from "../upload.vue";
|
||||||
|
import { usePublicHooks } from "../../hooks";
|
||||||
|
import { addDialog } from "@/components/ReDialog";
|
||||||
|
import type { PaginationProps } from "@pureadmin/table";
|
||||||
|
import type { FormItemProps, RoleFormItemProps } from "../utils/types";
|
||||||
|
import { hideTextAtIndex, getKeyList, isAllEmpty } from "@pureadmin/utils";
|
||||||
|
import {
|
||||||
|
getRoleIds,
|
||||||
|
getDeptList,
|
||||||
|
getUserList,
|
||||||
|
getAllRoleList
|
||||||
|
} from "@/api/system";
|
||||||
|
import {
|
||||||
|
ElForm,
|
||||||
|
ElInput,
|
||||||
|
ElFormItem,
|
||||||
|
ElProgress,
|
||||||
|
ElMessageBox
|
||||||
|
} from "element-plus";
|
||||||
|
import {
|
||||||
|
type Ref,
|
||||||
|
h,
|
||||||
|
ref,
|
||||||
|
toRaw,
|
||||||
|
watch,
|
||||||
|
computed,
|
||||||
|
reactive,
|
||||||
|
onMounted
|
||||||
|
} from "vue";
|
||||||
|
|
||||||
|
export function useUser(tableRef: Ref, treeRef: Ref) {
|
||||||
|
const form = reactive({
|
||||||
|
// 左侧部门树的id
|
||||||
|
deptId: "",
|
||||||
|
username: "",
|
||||||
|
phone: "",
|
||||||
|
status: ""
|
||||||
|
});
|
||||||
|
const formRef = ref();
|
||||||
|
const ruleFormRef = ref();
|
||||||
|
const dataList = ref([]);
|
||||||
|
const loading = ref(true);
|
||||||
|
// 上传头像信息
|
||||||
|
const avatarInfo = ref();
|
||||||
|
const switchLoadMap = ref({});
|
||||||
|
const { switchStyle } = usePublicHooks();
|
||||||
|
const higherDeptOptions = ref();
|
||||||
|
const treeData = ref([]);
|
||||||
|
const treeLoading = ref(true);
|
||||||
|
const selectedNum = ref(0);
|
||||||
|
const pagination = reactive<PaginationProps>({
|
||||||
|
total: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
currentPage: 1,
|
||||||
|
background: true
|
||||||
|
});
|
||||||
|
const columns: TableColumnList = [
|
||||||
|
{
|
||||||
|
label: "勾选列", // 如果需要表格多选,此处label必须设置
|
||||||
|
type: "selection",
|
||||||
|
fixed: "left",
|
||||||
|
reserveSelection: true // 数据刷新后保留选项
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "用户编号",
|
||||||
|
prop: "id",
|
||||||
|
width: 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "用户头像",
|
||||||
|
prop: "avatar",
|
||||||
|
cellRenderer: ({ row }) => (
|
||||||
|
<el-image
|
||||||
|
fit="cover"
|
||||||
|
preview-teleported={true}
|
||||||
|
src={row.avatar}
|
||||||
|
preview-src-list={Array.of(row.avatar)}
|
||||||
|
class="w-[24px] h-[24px] rounded-full align-middle"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
width: 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "用户名称",
|
||||||
|
prop: "username",
|
||||||
|
minWidth: 130
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "用户昵称",
|
||||||
|
prop: "nickname",
|
||||||
|
minWidth: 130
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "性别",
|
||||||
|
prop: "sex",
|
||||||
|
minWidth: 90,
|
||||||
|
cellRenderer: ({ row, props }) => (
|
||||||
|
<el-tag
|
||||||
|
size={props.size}
|
||||||
|
type={row.sex === 1 ? "danger" : null}
|
||||||
|
effect="plain"
|
||||||
|
>
|
||||||
|
{row.sex === 1 ? "女" : "男"}
|
||||||
|
</el-tag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "部门",
|
||||||
|
prop: "dept.name",
|
||||||
|
minWidth: 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "手机号码",
|
||||||
|
prop: "phone",
|
||||||
|
minWidth: 90,
|
||||||
|
formatter: ({ phone }) => hideTextAtIndex(phone, { start: 3, end: 6 })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "状态",
|
||||||
|
prop: "status",
|
||||||
|
minWidth: 90,
|
||||||
|
cellRenderer: scope => (
|
||||||
|
<el-switch
|
||||||
|
size={scope.props.size === "small" ? "small" : "default"}
|
||||||
|
loading={switchLoadMap.value[scope.index]?.loading}
|
||||||
|
v-model={scope.row.status}
|
||||||
|
active-value={1}
|
||||||
|
inactive-value={0}
|
||||||
|
active-text="已启用"
|
||||||
|
inactive-text="已停用"
|
||||||
|
inline-prompt
|
||||||
|
style={switchStyle.value}
|
||||||
|
onChange={() => onChange(scope as any)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "创建时间",
|
||||||
|
minWidth: 90,
|
||||||
|
prop: "createTime",
|
||||||
|
formatter: ({ createTime }) =>
|
||||||
|
dayjs(createTime).format("YYYY-MM-DD HH:mm:ss")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "操作",
|
||||||
|
fixed: "right",
|
||||||
|
width: 180,
|
||||||
|
slot: "operation"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const buttonClass = computed(() => {
|
||||||
|
return [
|
||||||
|
"!h-[20px]",
|
||||||
|
"reset-margin",
|
||||||
|
"!text-gray-500",
|
||||||
|
"dark:!text-white",
|
||||||
|
"dark:hover:!text-primary"
|
||||||
|
];
|
||||||
|
});
|
||||||
|
// 重置的新密码
|
||||||
|
const pwdForm = reactive({
|
||||||
|
newPwd: ""
|
||||||
|
});
|
||||||
|
const pwdProgress = [
|
||||||
|
{ color: "#e74242", text: "非常弱" },
|
||||||
|
{ color: "#EFBD47", text: "弱" },
|
||||||
|
{ color: "#ffa500", text: "一般" },
|
||||||
|
{ color: "#1bbf1b", text: "强" },
|
||||||
|
{ color: "#008000", text: "非常强" }
|
||||||
|
];
|
||||||
|
// 当前密码强度(0-4)
|
||||||
|
const curScore = ref();
|
||||||
|
const roleOptions = ref([]);
|
||||||
|
|
||||||
|
function onChange({ row, index }) {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
`确认要<strong>${
|
||||||
|
row.status === 0 ? "停用" : "启用"
|
||||||
|
}</strong><strong style='color:var(--el-color-primary)'>${
|
||||||
|
row.username
|
||||||
|
}</strong>用户吗?`,
|
||||||
|
"系统提示",
|
||||||
|
{
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "warning",
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
draggable: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
switchLoadMap.value[index] = Object.assign(
|
||||||
|
{},
|
||||||
|
switchLoadMap.value[index],
|
||||||
|
{
|
||||||
|
loading: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setTimeout(() => {
|
||||||
|
switchLoadMap.value[index] = Object.assign(
|
||||||
|
{},
|
||||||
|
switchLoadMap.value[index],
|
||||||
|
{
|
||||||
|
loading: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
message("已成功修改用户状态", {
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
row.status === 0 ? (row.status = 1) : (row.status = 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpdate(row) {
|
||||||
|
console.log(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete(row) {
|
||||||
|
message(`您删除了用户编号为${row.id}的这条数据`, { type: "success" });
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSizeChange(val: number) {
|
||||||
|
console.log(`${val} items per page`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCurrentChange(val: number) {
|
||||||
|
console.log(`current page: ${val}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当CheckBox选择项发生变化时会触发该事件 */
|
||||||
|
function handleSelectionChange(val) {
|
||||||
|
selectedNum.value = val.length;
|
||||||
|
// 重置表格高度
|
||||||
|
tableRef.value.setAdaptive();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消选择 */
|
||||||
|
function onSelectionCancel() {
|
||||||
|
selectedNum.value = 0;
|
||||||
|
// 用于多选表格,清空用户的选择
|
||||||
|
tableRef.value.getTableRef().clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除 */
|
||||||
|
function onbatchDel() {
|
||||||
|
// 返回当前选中的行
|
||||||
|
const curSelected = tableRef.value.getTableRef().getSelectionRows();
|
||||||
|
// 接下来根据实际业务,通过选中行的某项数据,比如下面的id,调用接口进行批量删除
|
||||||
|
message(`已删除用户编号为 ${getKeyList(curSelected, "id")} 的数据`, {
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
tableRef.value.getTableRef().clearSelection();
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSearch() {
|
||||||
|
loading.value = true;
|
||||||
|
const { data } = await getUserList(toRaw(form));
|
||||||
|
dataList.value = data.list;
|
||||||
|
pagination.total = data.total;
|
||||||
|
pagination.pageSize = data.pageSize;
|
||||||
|
pagination.currentPage = data.currentPage;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = formEl => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.resetFields();
|
||||||
|
form.deptId = "";
|
||||||
|
treeRef.value.onTreeReset();
|
||||||
|
onSearch();
|
||||||
|
};
|
||||||
|
|
||||||
|
function onTreeSelect({ id, selected }) {
|
||||||
|
form.deptId = selected ? id : "";
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatHigherDeptOptions(treeList) {
|
||||||
|
// 根据返回数据的status字段值判断追加是否禁用disabled字段,返回处理后的树结构,用于上级部门级联选择器的展示(实际开发中也是如此,不可能前端需要的每个字段后端都会返回,这时需要前端自行根据后端返回的某些字段做逻辑处理)
|
||||||
|
if (!treeList || !treeList.length) return;
|
||||||
|
const newTreeList = [];
|
||||||
|
for (let i = 0; i < treeList.length; i++) {
|
||||||
|
treeList[i].disabled = treeList[i].status === 0 ? true : false;
|
||||||
|
formatHigherDeptOptions(treeList[i].children);
|
||||||
|
newTreeList.push(treeList[i]);
|
||||||
|
}
|
||||||
|
return newTreeList;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDialog(title = "新增", row?: FormItemProps) {
|
||||||
|
addDialog({
|
||||||
|
title: `${title}用户`,
|
||||||
|
props: {
|
||||||
|
formInline: {
|
||||||
|
title,
|
||||||
|
higherDeptOptions: formatHigherDeptOptions(higherDeptOptions.value),
|
||||||
|
parentId: row?.dept.id ?? 0,
|
||||||
|
nickname: row?.nickname ?? "",
|
||||||
|
username: row?.username ?? "",
|
||||||
|
password: row?.password ?? "",
|
||||||
|
phone: row?.phone ?? "",
|
||||||
|
email: row?.email ?? "",
|
||||||
|
sex: row?.sex ?? "",
|
||||||
|
status: row?.status ?? 1,
|
||||||
|
remark: row?.remark ?? ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
width: "46%",
|
||||||
|
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.username}的这条数据`, {
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
done(); // 关闭弹框
|
||||||
|
onSearch(); // 刷新表格数据
|
||||||
|
}
|
||||||
|
FormRef.validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
console.log("curData", curData);
|
||||||
|
// 表单规则校验通过
|
||||||
|
if (title === "新增") {
|
||||||
|
// 实际开发先调用新增接口,再进行下面操作
|
||||||
|
chores();
|
||||||
|
} else {
|
||||||
|
// 实际开发先调用修改接口,再进行下面操作
|
||||||
|
chores();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const cropRef = ref();
|
||||||
|
/** 上传头像 */
|
||||||
|
function handleUpload(row) {
|
||||||
|
addDialog({
|
||||||
|
title: "裁剪、上传头像",
|
||||||
|
width: "40%",
|
||||||
|
draggable: true,
|
||||||
|
closeOnClickModal: false,
|
||||||
|
contentRenderer: () =>
|
||||||
|
h(croppingUpload, {
|
||||||
|
ref: cropRef,
|
||||||
|
imgSrc: row.avatar,
|
||||||
|
onCropper: info => (avatarInfo.value = info)
|
||||||
|
}),
|
||||||
|
beforeSure: done => {
|
||||||
|
console.log("裁剪后的图片信息:", avatarInfo.value);
|
||||||
|
// 根据实际业务使用avatarInfo.value和row里的某些字段去调用上传头像接口即可
|
||||||
|
done(); // 关闭弹框
|
||||||
|
onSearch(); // 刷新表格数据
|
||||||
|
},
|
||||||
|
closeCallBack: () => cropRef.value.hidePopover()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
pwdForm,
|
||||||
|
({ newPwd }) =>
|
||||||
|
(curScore.value = isAllEmpty(newPwd) ? -1 : zxcvbn(newPwd).score)
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 重置密码 */
|
||||||
|
function handleReset(row) {
|
||||||
|
addDialog({
|
||||||
|
title: `重置 ${row.username} 用户的密码`,
|
||||||
|
width: "30%",
|
||||||
|
draggable: true,
|
||||||
|
closeOnClickModal: false,
|
||||||
|
contentRenderer: () => (
|
||||||
|
<>
|
||||||
|
<ElForm ref={ruleFormRef} model={pwdForm}>
|
||||||
|
<ElFormItem
|
||||||
|
prop="newPwd"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "请输入新密码",
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<ElInput
|
||||||
|
clearable
|
||||||
|
show-password
|
||||||
|
type="password"
|
||||||
|
v-model={pwdForm.newPwd}
|
||||||
|
placeholder="请输入新密码"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
<div class="mt-4 flex">
|
||||||
|
{pwdProgress.map(({ color, text }, idx) => (
|
||||||
|
<div
|
||||||
|
class="w-[19vw]"
|
||||||
|
style={{ marginLeft: idx !== 0 ? "4px" : 0 }}
|
||||||
|
>
|
||||||
|
<ElProgress
|
||||||
|
striped
|
||||||
|
striped-flow
|
||||||
|
duration={curScore.value === idx ? 6 : 0}
|
||||||
|
percentage={curScore.value >= idx ? 100 : 0}
|
||||||
|
color={color}
|
||||||
|
stroke-width={10}
|
||||||
|
show-text={false}
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
class="text-center"
|
||||||
|
style={{ color: curScore.value === idx ? color : "" }}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
closeCallBack: () => (pwdForm.newPwd = ""),
|
||||||
|
beforeSure: done => {
|
||||||
|
ruleFormRef.value.validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
// 表单规则校验通过
|
||||||
|
message(`已成功重置 ${row.username} 用户的密码`, {
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
console.log(pwdForm.newPwd);
|
||||||
|
// 根据实际业务使用pwdForm.newPwd和row里的某些字段去调用重置用户密码接口即可
|
||||||
|
done(); // 关闭弹框
|
||||||
|
onSearch(); // 刷新表格数据
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分配角色 */
|
||||||
|
async function handleRole(row) {
|
||||||
|
// 选中的角色列表
|
||||||
|
const ids = (await getRoleIds({ userId: row.id })).data ?? [];
|
||||||
|
addDialog({
|
||||||
|
title: `分配 ${row.username} 用户的角色`,
|
||||||
|
props: {
|
||||||
|
formInline: {
|
||||||
|
username: row?.username ?? "",
|
||||||
|
nickname: row?.nickname ?? "",
|
||||||
|
roleOptions: roleOptions.value ?? [],
|
||||||
|
ids
|
||||||
|
}
|
||||||
|
},
|
||||||
|
width: "400px",
|
||||||
|
draggable: true,
|
||||||
|
fullscreenIcon: true,
|
||||||
|
closeOnClickModal: false,
|
||||||
|
contentRenderer: () => h(roleForm),
|
||||||
|
beforeSure: (done, { options }) => {
|
||||||
|
const curData = options.props.formInline as RoleFormItemProps;
|
||||||
|
console.log("curIds", curData.ids);
|
||||||
|
// 根据实际业务使用curData.ids和row里的某些字段去调用修改角色接口即可
|
||||||
|
done(); // 关闭弹框
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
treeLoading.value = true;
|
||||||
|
onSearch();
|
||||||
|
|
||||||
|
// 归属部门
|
||||||
|
const { data } = await getDeptList();
|
||||||
|
higherDeptOptions.value = handleTree(data);
|
||||||
|
treeData.value = handleTree(data);
|
||||||
|
treeLoading.value = false;
|
||||||
|
|
||||||
|
// 角色列表
|
||||||
|
roleOptions.value = (await getAllRoleList()).data;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
loading,
|
||||||
|
columns,
|
||||||
|
dataList,
|
||||||
|
treeData,
|
||||||
|
treeLoading,
|
||||||
|
selectedNum,
|
||||||
|
pagination,
|
||||||
|
buttonClass,
|
||||||
|
onSearch,
|
||||||
|
resetForm,
|
||||||
|
onbatchDel,
|
||||||
|
openDialog,
|
||||||
|
onTreeSelect,
|
||||||
|
handleUpdate,
|
||||||
|
handleDelete,
|
||||||
|
handleUpload,
|
||||||
|
handleReset,
|
||||||
|
handleRole,
|
||||||
|
handleSizeChange,
|
||||||
|
onSelectionCancel,
|
||||||
|
handleCurrentChange,
|
||||||
|
handleSelectionChange
|
||||||
|
};
|
||||||
|
}
|
5
src/views/system/user/utils/reset.css
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/** 局部重置 ElProgress 的部分样式 */
|
||||||
|
.el-progress-bar__outer,
|
||||||
|
.el-progress-bar__inner {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
39
src/views/system/user/utils/rule.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { reactive } from "vue";
|
||||||
|
import type { FormRules } from "element-plus";
|
||||||
|
import { isPhone, isEmail } from "@pureadmin/utils";
|
||||||
|
|
||||||
|
/** 自定义表单规则校验 */
|
||||||
|
export const formRules = reactive(<FormRules>{
|
||||||
|
nickname: [{ required: true, message: "用户昵称为必填项", trigger: "blur" }],
|
||||||
|
username: [{ required: true, message: "用户名称为必填项", trigger: "blur" }],
|
||||||
|
password: [{ required: true, message: "用户密码为必填项", trigger: "blur" }],
|
||||||
|
phone: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (value === "") {
|
||||||
|
callback();
|
||||||
|
} else if (!isPhone(value)) {
|
||||||
|
callback(new Error("请输入正确的手机号码格式"));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: "blur"
|
||||||
|
// trigger: "click" // 如果想在点击确定按钮时触发这个校验,trigger 设置成 click 即可
|
||||||
|
}
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (value === "") {
|
||||||
|
callback();
|
||||||
|
} else if (!isEmail(value)) {
|
||||||
|
callback(new Error("请输入正确的邮箱格式"));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
36
src/views/system/user/utils/types.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
interface FormItemProps {
|
||||||
|
id?: number;
|
||||||
|
/** 用于判断是`新增`还是`修改` */
|
||||||
|
title: string;
|
||||||
|
higherDeptOptions: Record<string, unknown>[];
|
||||||
|
parentId: number;
|
||||||
|
nickname: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
phone: string | number;
|
||||||
|
email: string;
|
||||||
|
sex: string | number;
|
||||||
|
status: number;
|
||||||
|
dept?: {
|
||||||
|
id?: number;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
remark: string;
|
||||||
|
}
|
||||||
|
interface FormProps {
|
||||||
|
formInline: FormItemProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RoleFormItemProps {
|
||||||
|
username: string;
|
||||||
|
nickname: string;
|
||||||
|
/** 角色列表 */
|
||||||
|
roleOptions: any[];
|
||||||
|
/** 选中的角色列表 */
|
||||||
|
ids: Record<number, unknown>[];
|
||||||
|
}
|
||||||
|
interface RoleFormProps {
|
||||||
|
formInline: RoleFormItemProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { FormItemProps, FormProps, RoleFormItemProps, RoleFormProps };
|