5.0
107
sop-admin/sop-admin-frontend/src/api/system.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { baseUrl, http } from "@/utils/http";
|
||||
|
||||
type Result = {
|
||||
success: boolean;
|
||||
data?: Array<any>;
|
||||
};
|
||||
|
||||
type ResultTable = {
|
||||
success: boolean;
|
||||
data?: {
|
||||
/** 列表数据 */
|
||||
list: Array<any>;
|
||||
/** 总条目数 */
|
||||
total?: number;
|
||||
/** 每页显示条目个数 */
|
||||
pageSize?: number;
|
||||
/** 当前页数 */
|
||||
pageIndex?: 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 = (params?: object) => {
|
||||
return http.request<ResultTable>("get", baseUrl("sys/role/page"), { params });
|
||||
};
|
||||
|
||||
export const addRole = (data?: object) => {
|
||||
return http.request<ResultTable>("post", baseUrl("sys/role/add"), { data });
|
||||
};
|
||||
|
||||
export const updateRoleStatus = (data?: object) => {
|
||||
return http.request<ResultTable>("post", baseUrl("sys/role/updateStatus"), {
|
||||
data
|
||||
});
|
||||
};
|
||||
|
||||
export const updateRole = (data?: object) => {
|
||||
return http.request<ResultTable>("post", baseUrl("sys/role/update"), {
|
||||
data
|
||||
});
|
||||
};
|
||||
|
||||
export const delRole = (data?: object) => {
|
||||
return http.request<ResultTable>("post", baseUrl("sys/role/delete"), {
|
||||
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 });
|
||||
};
|
||||
|
||||
/** 获取系统监控-系统日志-根据 id 查日志详情 */
|
||||
export const getSystemLogsDetail = (data?: object) => {
|
||||
return http.request<Result>("post", "/system-logs-detail", { data });
|
||||
};
|
||||
|
||||
/** 获取角色管理-权限-菜单权限 */
|
||||
export const getRoleMenu = (data?: object) => {
|
||||
return http.request<Result>("post", "/role-menu", { data });
|
||||
};
|
||||
|
||||
/** 获取角色管理-权限-菜单权限-根据角色 id 查对应菜单 */
|
||||
export const getRoleMenuIds = (data?: object) => {
|
||||
return http.request<Result>("post", "/role-menu-ids", { data });
|
||||
};
|
@@ -0,0 +1,7 @@
|
||||
import { withInstall } from "@pureadmin/utils";
|
||||
import reAnimateSelector from "./src/index.vue";
|
||||
|
||||
/** [animate.css](https://animate.style/) 选择器组件 */
|
||||
export const ReAnimateSelector = withInstall(reAnimateSelector);
|
||||
|
||||
export default ReAnimateSelector;
|
@@ -0,0 +1,114 @@
|
||||
export const animates = [
|
||||
/* Attention seekers */
|
||||
"bounce",
|
||||
"flash",
|
||||
"pulse",
|
||||
"rubberBand",
|
||||
"shakeX",
|
||||
"headShake",
|
||||
"swing",
|
||||
"tada",
|
||||
"wobble",
|
||||
"jello",
|
||||
"heartBeat",
|
||||
/* Back entrances */
|
||||
"backInDown",
|
||||
"backInLeft",
|
||||
"backInRight",
|
||||
"backInUp",
|
||||
/* Back exits */
|
||||
"backOutDown",
|
||||
"backOutLeft",
|
||||
"backOutRight",
|
||||
"backOutUp",
|
||||
/* Bouncing entrances */
|
||||
"bounceIn",
|
||||
"bounceInDown",
|
||||
"bounceInLeft",
|
||||
"bounceInRight",
|
||||
"bounceInUp",
|
||||
/* Bouncing exits */
|
||||
"bounceOut",
|
||||
"bounceOutDown",
|
||||
"bounceOutLeft",
|
||||
"bounceOutRight",
|
||||
"bounceOutUp",
|
||||
/* Fading entrances */
|
||||
"fadeIn",
|
||||
"fadeInDown",
|
||||
"fadeInDownBig",
|
||||
"fadeInLeft",
|
||||
"fadeInLeftBig",
|
||||
"fadeInRight",
|
||||
"fadeInRightBig",
|
||||
"fadeInUp",
|
||||
"fadeInUpBig",
|
||||
"fadeInTopLeft",
|
||||
"fadeInTopRight",
|
||||
"fadeInBottomLeft",
|
||||
"fadeInBottomRight",
|
||||
/* Fading exits */
|
||||
"fadeOut",
|
||||
"fadeOutDown",
|
||||
"fadeOutDownBig",
|
||||
"fadeOutLeft",
|
||||
"fadeOutLeftBig",
|
||||
"fadeOutRight",
|
||||
"fadeOutRightBig",
|
||||
"fadeOutUp",
|
||||
"fadeOutUpBig",
|
||||
"fadeOutTopLeft",
|
||||
"fadeOutTopRight",
|
||||
"fadeOutBottomRight",
|
||||
"fadeOutBottomLeft",
|
||||
/* Flippers */
|
||||
"flip",
|
||||
"flipInX",
|
||||
"flipInY",
|
||||
"flipOutX",
|
||||
"flipOutY",
|
||||
/* Lightspeed */
|
||||
"lightSpeedInRight",
|
||||
"lightSpeedInLeft",
|
||||
"lightSpeedOutRight",
|
||||
"lightSpeedOutLeft",
|
||||
/* Rotating entrances */
|
||||
"rotateIn",
|
||||
"rotateInDownLeft",
|
||||
"rotateInDownRight",
|
||||
"rotateInUpLeft",
|
||||
"rotateInUpRight",
|
||||
/* Rotating exits */
|
||||
"rotateOut",
|
||||
"rotateOutDownLeft",
|
||||
"rotateOutDownRight",
|
||||
"rotateOutUpLeft",
|
||||
"rotateOutUpRight",
|
||||
/* Specials */
|
||||
"hinge",
|
||||
"jackInTheBox",
|
||||
"rollIn",
|
||||
"rollOut",
|
||||
/* Zooming entrances */
|
||||
"zoomIn",
|
||||
"zoomInDown",
|
||||
"zoomInLeft",
|
||||
"zoomInRight",
|
||||
"zoomInUp",
|
||||
/* Zooming exits */
|
||||
"zoomOut",
|
||||
"zoomOutDown",
|
||||
"zoomOutLeft",
|
||||
"zoomOutRight",
|
||||
"zoomOutUp",
|
||||
/* Sliding entrances */
|
||||
"slideInDown",
|
||||
"slideInLeft",
|
||||
"slideInRight",
|
||||
"slideInUp",
|
||||
/* Sliding exits */
|
||||
"slideOutDown",
|
||||
"slideOutLeft",
|
||||
"slideOutRight",
|
||||
"slideOutUp"
|
||||
];
|
@@ -0,0 +1,136 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { animates } from "./animate";
|
||||
import { cloneDeep } from "@pureadmin/utils";
|
||||
|
||||
defineOptions({
|
||||
name: "ReAnimateSelector"
|
||||
});
|
||||
|
||||
defineProps({
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "请选择动画"
|
||||
}
|
||||
});
|
||||
|
||||
const inputValue = defineModel({ type: String });
|
||||
|
||||
const searchVal = ref();
|
||||
const animatesList = ref(animates);
|
||||
const copyAnimatesList = cloneDeep(animatesList);
|
||||
|
||||
const animateClass = computed(() => {
|
||||
return [
|
||||
"mt-1",
|
||||
"flex",
|
||||
"border",
|
||||
"w-[130px]",
|
||||
"h-[100px]",
|
||||
"items-center",
|
||||
"cursor-pointer",
|
||||
"transition-all",
|
||||
"justify-center",
|
||||
"border-[#e5e7eb]",
|
||||
"hover:text-primary",
|
||||
"hover:duration-[700ms]"
|
||||
];
|
||||
});
|
||||
|
||||
const animateStyle = computed(
|
||||
() => (i: string) =>
|
||||
inputValue.value === i
|
||||
? {
|
||||
borderColor: "var(--el-color-primary)",
|
||||
color: "var(--el-color-primary)"
|
||||
}
|
||||
: ""
|
||||
);
|
||||
|
||||
function onChangeIcon(animate: string) {
|
||||
inputValue.value = animate;
|
||||
}
|
||||
|
||||
function onClear() {
|
||||
inputValue.value = "";
|
||||
}
|
||||
|
||||
function filterMethod(value: any) {
|
||||
searchVal.value = value;
|
||||
animatesList.value = copyAnimatesList.value.filter((i: string | any[]) =>
|
||||
i.includes(value)
|
||||
);
|
||||
}
|
||||
|
||||
const animateMap = ref({});
|
||||
function onMouseEnter(index: string | number) {
|
||||
animateMap.value[index] = animateMap.value[index]?.loading
|
||||
? Object.assign({}, animateMap.value[index], {
|
||||
loading: false
|
||||
})
|
||||
: Object.assign({}, animateMap.value[index], {
|
||||
loading: true
|
||||
});
|
||||
}
|
||||
function onMouseleave() {
|
||||
animateMap.value = {};
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-select
|
||||
clearable
|
||||
filterable
|
||||
:placeholder="placeholder"
|
||||
popper-class="pure-animate-popper"
|
||||
:model-value="inputValue"
|
||||
:filter-method="filterMethod"
|
||||
@clear="onClear"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="w-[280px]">
|
||||
<el-scrollbar
|
||||
noresize
|
||||
height="212px"
|
||||
:view-style="{ overflow: 'hidden' }"
|
||||
class="border-t border-[#e5e7eb]"
|
||||
>
|
||||
<ul class="flex flex-wrap justify-around mb-1">
|
||||
<li
|
||||
v-for="(animate, index) in animatesList"
|
||||
:key="index"
|
||||
:class="animateClass"
|
||||
:style="animateStyle(animate)"
|
||||
@mouseenter.prevent="onMouseEnter(index)"
|
||||
@mouseleave.prevent="onMouseleave"
|
||||
@click="onChangeIcon(animate)"
|
||||
>
|
||||
<h4
|
||||
:class="[
|
||||
`animate__animated animate__${
|
||||
animateMap[index]?.loading
|
||||
? animate + ' animate__infinite'
|
||||
: ''
|
||||
} `
|
||||
]"
|
||||
>
|
||||
{{ animate }}
|
||||
</h4>
|
||||
</li>
|
||||
</ul>
|
||||
<el-empty
|
||||
v-show="animatesList.length === 0"
|
||||
:description="`${searchVal} 动画不存在`"
|
||||
:image-size="60"
|
||||
/>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.pure-animate-popper {
|
||||
min-width: 0 !important;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,7 @@
|
||||
import reCropper from "./src";
|
||||
import { withInstall } from "@pureadmin/utils";
|
||||
|
||||
/** 图片裁剪组件 */
|
||||
export const ReCropper = withInstall(reCropper);
|
||||
|
||||
export default ReCropper;
|
@@ -0,0 +1,8 @@
|
||||
@import "cropperjs/dist/cropper.css";
|
||||
|
||||
.re-circled {
|
||||
.cropper-view-box,
|
||||
.cropper-face {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
@@ -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 {
|
||||
type PropType,
|
||||
ref,
|
||||
unref,
|
||||
computed,
|
||||
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;
|
||||
}
|
||||
});
|
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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
|
||||
};
|
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -0,0 +1,7 @@
|
||||
import reCropperPreview from "./src/index.vue";
|
||||
import { withInstall } from "@pureadmin/utils";
|
||||
|
||||
/** 图片裁剪预览组件 */
|
||||
export const ReCropperPreview = withInstall(reCropperPreview);
|
||||
|
||||
export default ReCropperPreview;
|
@@ -0,0 +1,76 @@
|
||||
<script setup lang="tsx">
|
||||
import { ref } from "vue";
|
||||
import ReCropper from "@/components/ReCropper";
|
||||
import { formatBytes } from "@pureadmin/utils";
|
||||
|
||||
defineOptions({
|
||||
name: "ReCropperPreview"
|
||||
});
|
||||
|
||||
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="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>
|
3869
sop-admin/sop-admin-frontend/src/components/ReIcon/data.ts
Normal file
@@ -1,12 +1,15 @@
|
||||
import iconifyIconOffline from "./src/iconifyIconOffline";
|
||||
import iconifyIconOnline from "./src/iconifyIconOnline";
|
||||
import iconSelect from "./src/Select.vue";
|
||||
import fontIcon from "./src/iconfont";
|
||||
|
||||
/** 本地图标组件 */
|
||||
const IconifyIconOffline = iconifyIconOffline;
|
||||
/** 在线图标组件 */
|
||||
const IconifyIconOnline = iconifyIconOnline;
|
||||
/** `IconSelect`图标选择器组件 */
|
||||
const IconSelect = iconSelect;
|
||||
/** `iconfont`组件 */
|
||||
const FontIcon = fontIcon;
|
||||
|
||||
export { IconifyIconOffline, IconifyIconOnline, FontIcon };
|
||||
export { IconifyIconOffline, IconifyIconOnline, IconSelect, FontIcon };
|
||||
|
@@ -0,0 +1,268 @@
|
||||
<script setup lang="ts">
|
||||
import { IconJson } from "@/components/ReIcon/data";
|
||||
import { cloneDeep, isAllEmpty } from "@pureadmin/utils";
|
||||
import { ref, computed, CSSProperties, watch } from "vue";
|
||||
import Search from "@iconify-icons/ri/search-eye-line";
|
||||
|
||||
type ParameterCSSProperties = (item?: string) => CSSProperties | undefined;
|
||||
|
||||
defineOptions({
|
||||
name: "IconSelect"
|
||||
});
|
||||
|
||||
const inputValue = defineModel({ type: String });
|
||||
|
||||
const iconList = ref(IconJson);
|
||||
const icon = ref();
|
||||
const currentActiveType = ref("ep:");
|
||||
// 深拷贝图标数据,前端做搜索
|
||||
const copyIconList = cloneDeep(iconList.value);
|
||||
const totalPage = ref(0);
|
||||
// 每页显示35个图标
|
||||
const pageSize = ref(35);
|
||||
const currentPage = ref(1);
|
||||
|
||||
// 搜索条件
|
||||
const filterValue = ref("");
|
||||
|
||||
const tabsList = [
|
||||
{
|
||||
label: "Element Plus",
|
||||
name: "ep:"
|
||||
},
|
||||
{
|
||||
label: "Remix Icon",
|
||||
name: "ri:"
|
||||
},
|
||||
{
|
||||
label: "Font Awesome 5 Solid",
|
||||
name: "fa-solid:"
|
||||
}
|
||||
];
|
||||
|
||||
const pageList = computed(() =>
|
||||
copyIconList[currentActiveType.value]
|
||||
.filter(i => i.includes(filterValue.value))
|
||||
.slice(
|
||||
(currentPage.value - 1) * pageSize.value,
|
||||
currentPage.value * pageSize.value
|
||||
)
|
||||
);
|
||||
|
||||
const iconItemStyle = computed((): ParameterCSSProperties => {
|
||||
return item => {
|
||||
if (inputValue.value === currentActiveType.value + item) {
|
||||
return {
|
||||
borderColor: "var(--el-color-primary)",
|
||||
color: "var(--el-color-primary)"
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
function setVal() {
|
||||
currentActiveType.value = inputValue.value.substring(
|
||||
0,
|
||||
inputValue.value.indexOf(":") + 1
|
||||
);
|
||||
icon.value = inputValue.value.substring(inputValue.value.indexOf(":") + 1);
|
||||
}
|
||||
|
||||
function onBeforeEnter() {
|
||||
if (isAllEmpty(icon.value)) return;
|
||||
setVal();
|
||||
// 寻找当前图标在第几页
|
||||
const curIconIndex = copyIconList[currentActiveType.value].findIndex(
|
||||
i => i === icon.value
|
||||
);
|
||||
currentPage.value = Math.ceil((curIconIndex + 1) / pageSize.value);
|
||||
}
|
||||
|
||||
function onAfterLeave() {
|
||||
filterValue.value = "";
|
||||
}
|
||||
|
||||
function handleClick({ props }) {
|
||||
currentPage.value = 1;
|
||||
currentActiveType.value = props.name;
|
||||
}
|
||||
|
||||
function onChangeIcon(item) {
|
||||
icon.value = item;
|
||||
inputValue.value = currentActiveType.value + item;
|
||||
}
|
||||
|
||||
function onCurrentChange(page) {
|
||||
currentPage.value = page;
|
||||
}
|
||||
|
||||
function onClear() {
|
||||
icon.value = "";
|
||||
inputValue.value = "";
|
||||
}
|
||||
|
||||
watch(
|
||||
() => pageList.value,
|
||||
() =>
|
||||
(totalPage.value = copyIconList[currentActiveType.value].filter(i =>
|
||||
i.includes(filterValue.value)
|
||||
).length),
|
||||
{ immediate: true }
|
||||
);
|
||||
watch(
|
||||
() => inputValue.value,
|
||||
val => val && setVal(),
|
||||
{ immediate: true }
|
||||
);
|
||||
watch(
|
||||
() => filterValue.value,
|
||||
() => (currentPage.value = 1)
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="selector">
|
||||
<el-input v-model="inputValue" disabled>
|
||||
<template #append>
|
||||
<el-popover
|
||||
:width="350"
|
||||
trigger="click"
|
||||
popper-class="pure-popper"
|
||||
:popper-options="{
|
||||
placement: 'auto'
|
||||
}"
|
||||
@before-enter="onBeforeEnter"
|
||||
@after-leave="onAfterLeave"
|
||||
>
|
||||
<template #reference>
|
||||
<div
|
||||
class="w-[40px] h-[32px] cursor-pointer flex justify-center items-center"
|
||||
>
|
||||
<IconifyIconOffline v-if="!icon" :icon="Search" />
|
||||
<IconifyIconOnline v-else :icon="inputValue" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-input
|
||||
v-model="filterValue"
|
||||
class="px-2 pt-2"
|
||||
placeholder="搜索图标"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<el-tabs v-model="currentActiveType" @tab-click="handleClick">
|
||||
<el-tab-pane
|
||||
v-for="(pane, index) in tabsList"
|
||||
:key="index"
|
||||
:label="pane.label"
|
||||
:name="pane.name"
|
||||
>
|
||||
<el-scrollbar height="220px">
|
||||
<ul class="flex flex-wrap px-2 ml-2">
|
||||
<li
|
||||
v-for="(item, key) in pageList"
|
||||
:key="key"
|
||||
:title="item"
|
||||
class="icon-item p-2 cursor-pointer mr-2 mt-1 flex justify-center items-center border border-[#e5e7eb]"
|
||||
:style="iconItemStyle(item)"
|
||||
@click="onChangeIcon(item)"
|
||||
>
|
||||
<IconifyIconOnline
|
||||
:icon="currentActiveType + item"
|
||||
width="20px"
|
||||
height="20px"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<el-empty
|
||||
v-show="pageList.length === 0"
|
||||
:description="`${filterValue} 图标不存在`"
|
||||
:image-size="60"
|
||||
/>
|
||||
</el-scrollbar>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<div
|
||||
class="w-full h-9 flex items-center overflow-auto border-t border-[#e5e7eb]"
|
||||
>
|
||||
<el-pagination
|
||||
class="flex-auto ml-2"
|
||||
:total="totalPage"
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:pager-count="5"
|
||||
layout="pager"
|
||||
background
|
||||
size="small"
|
||||
@current-change="onCurrentChange"
|
||||
/>
|
||||
<el-button
|
||||
class="justify-end mr-2 ml-2"
|
||||
type="danger"
|
||||
size="small"
|
||||
text
|
||||
bg
|
||||
@click="onClear"
|
||||
>
|
||||
清空
|
||||
</el-button>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.icon-item {
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
transition: all 0.4s;
|
||||
transform: scaleX(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-next) {
|
||||
font-size: 15px;
|
||||
line-height: 32px;
|
||||
box-shadow: -5px 0 5px -6px #ccc;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-prev) {
|
||||
font-size: 15px;
|
||||
line-height: 32px;
|
||||
box-shadow: 5px 0 5px -6px #ccc;
|
||||
}
|
||||
|
||||
:deep(.el-input-group__append) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
height: 30px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__header),
|
||||
:deep(.el-tabs__nav-wrap) {
|
||||
position: static;
|
||||
margin: 0;
|
||||
box-shadow: 0 2px 5px rgb(0 0 0 / 6%);
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-wrap::after) {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-wrap) {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__content) {
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
@@ -3,12 +3,68 @@ import { addIcon } from "@iconify/vue/dist/offline";
|
||||
|
||||
// 本地菜单图标,后端在路由的 icon 中返回对应的图标字符串并且前端在此处使用 addIcon 添加即可渲染菜单图标
|
||||
// @iconify-icons/ep
|
||||
import Menu from "@iconify-icons/ep/menu";
|
||||
import Edit from "@iconify-icons/ep/edit";
|
||||
import SetUp from "@iconify-icons/ep/set-up";
|
||||
import Guide from "@iconify-icons/ep/guide";
|
||||
import Monitor from "@iconify-icons/ep/monitor";
|
||||
import Lollipop from "@iconify-icons/ep/lollipop";
|
||||
import Histogram from "@iconify-icons/ep/histogram";
|
||||
import HomeFilled from "@iconify-icons/ep/home-filled";
|
||||
addIcon("ep:menu", Menu);
|
||||
addIcon("ep:edit", Edit);
|
||||
addIcon("ep:set-up", SetUp);
|
||||
addIcon("ep:guide", Guide);
|
||||
addIcon("ep:monitor", Monitor);
|
||||
addIcon("ep:lollipop", Lollipop);
|
||||
addIcon("ep:histogram", Histogram);
|
||||
addIcon("ep:home-filled", HomeFilled);
|
||||
// @iconify-icons/ri
|
||||
import Tag from "@iconify-icons/ri/bookmark-2-line";
|
||||
import Ppt from "@iconify-icons/ri/file-ppt-2-line";
|
||||
import Card from "@iconify-icons/ri/bank-card-line";
|
||||
import Role from "@iconify-icons/ri/admin-fill";
|
||||
import Info from "@iconify-icons/ri/file-info-line";
|
||||
import Dept from "@iconify-icons/ri/git-branch-line";
|
||||
import Table from "@iconify-icons/ri/table-line";
|
||||
import Links from "@iconify-icons/ri/links-fill";
|
||||
import Search from "@iconify-icons/ri/search-line";
|
||||
import FlUser from "@iconify-icons/ri/admin-line";
|
||||
import Setting from "@iconify-icons/ri/settings-3-line";
|
||||
import MindMap from "@iconify-icons/ri/mind-map";
|
||||
import BarChart from "@iconify-icons/ri/bar-chart-horizontal-line";
|
||||
import LoginLog from "@iconify-icons/ri/window-line";
|
||||
import Artboard from "@iconify-icons/ri/artboard-line";
|
||||
import SystemLog from "@iconify-icons/ri/file-search-line";
|
||||
import ListCheck from "@iconify-icons/ri/list-check";
|
||||
import UbuntuFill from "@iconify-icons/ri/ubuntu-fill";
|
||||
import OnlineUser from "@iconify-icons/ri/user-voice-line";
|
||||
import EditBoxLine from "@iconify-icons/ri/edit-box-line";
|
||||
import OperationLog from "@iconify-icons/ri/history-fill";
|
||||
import InformationLine from "@iconify-icons/ri/information-line";
|
||||
import TerminalWindowLine from "@iconify-icons/ri/terminal-window-line";
|
||||
import CheckboxCircleLine from "@iconify-icons/ri/checkbox-circle-line";
|
||||
addIcon("ri:bookmark-2-line", Tag);
|
||||
addIcon("ri:file-ppt-2-line", Ppt);
|
||||
addIcon("ri:bank-card-line", Card);
|
||||
addIcon("ri:admin-fill", Role);
|
||||
addIcon("ri:file-info-line", Info);
|
||||
addIcon("ri:git-branch-line", Dept);
|
||||
addIcon("ri:links-fill", Links);
|
||||
addIcon("ri:table-line", Table);
|
||||
addIcon("ri:search-line", Search);
|
||||
addIcon("ri:admin-line", FlUser);
|
||||
addIcon("ri:settings-3-line", Setting);
|
||||
addIcon("ri:mind-map", MindMap);
|
||||
addIcon("ri:bar-chart-horizontal-line", BarChart);
|
||||
addIcon("ri:window-line", LoginLog);
|
||||
addIcon("ri:file-search-line", SystemLog);
|
||||
addIcon("ri:artboard-line", Artboard);
|
||||
addIcon("ri:list-check", ListCheck);
|
||||
addIcon("ri:ubuntu-fill", UbuntuFill);
|
||||
addIcon("ri:user-voice-line", OnlineUser);
|
||||
addIcon("ri:edit-box-line", EditBoxLine);
|
||||
addIcon("ri:history-fill", OperationLog);
|
||||
addIcon("ri:information-line", InformationLine);
|
||||
addIcon("ri:terminal-window-line", TerminalWindowLine);
|
||||
addIcon("ri:checkbox-circle-line", CheckboxCircleLine);
|
||||
|
55
sop-admin/sop-admin-frontend/src/router/enums.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
// 完整版菜单比较多,将 rank 抽离出来,在此方便维护
|
||||
|
||||
const home = 0, // 平台规定只有 home 路由的 rank 才能为 0 ,所以后端在返回 rank 的时候需要从非 0 开始
|
||||
vueflow = 1,
|
||||
ganttastic = 2,
|
||||
components = 3,
|
||||
able = 4,
|
||||
table = 5,
|
||||
form = 6,
|
||||
list = 7,
|
||||
result = 8,
|
||||
error = 9,
|
||||
frame = 10,
|
||||
nested = 11,
|
||||
permission = 12,
|
||||
system = 13,
|
||||
monitor = 14,
|
||||
tabs = 15,
|
||||
about = 16,
|
||||
editor = 17,
|
||||
flowchart = 18,
|
||||
formdesign = 19,
|
||||
board = 20,
|
||||
ppt = 21,
|
||||
mind = 22,
|
||||
guide = 23,
|
||||
menuoverflow = 24;
|
||||
|
||||
export {
|
||||
home,
|
||||
vueflow,
|
||||
ganttastic,
|
||||
components,
|
||||
able,
|
||||
table,
|
||||
form,
|
||||
list,
|
||||
result,
|
||||
error,
|
||||
frame,
|
||||
nested,
|
||||
permission,
|
||||
system,
|
||||
monitor,
|
||||
tabs,
|
||||
about,
|
||||
editor,
|
||||
flowchart,
|
||||
formdesign,
|
||||
board,
|
||||
ppt,
|
||||
mind,
|
||||
guide,
|
||||
menuoverflow
|
||||
};
|
139
sop-admin/sop-admin-frontend/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>
|
172
sop-admin/sop-admin-frontend/src/views/system/dept/index.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<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();
|
||||
|
||||
function onFullscreen() {
|
||||
// 重置表格高度
|
||||
tableRef.value.setAdaptive();
|
||||
}
|
||||
</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] overflow-auto"
|
||||
>
|
||||
<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"
|
||||
@fullscreen="onFullscreen"
|
||||
>
|
||||
<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>
|
@@ -0,0 +1,178 @@
|
||||
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, deviceDetection } 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,
|
||||
fullscreen: deviceDetection(),
|
||||
fullscreenIcon: true,
|
||||
closeOnClickModal: false,
|
||||
contentRenderer: () => h(editForm, { ref: formRef, formInline: null }),
|
||||
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
|
||||
};
|
||||
}
|
@@ -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"
|
||||
}
|
||||
]
|
||||
});
|
@@ -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
sop-admin/sop-admin-frontend/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
sop-admin/sop-admin-frontend/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://pure-admin.github.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` | 标签页(当前菜单名称或自定义信息禁止添加到标签页) |
|
||||
| `fixedTag` | 固定标签页(当前菜单名称是否固定显示在标签页且不可关闭) |
|
||||
| `showLink` | 菜单(是否显示该菜单) |
|
||||
| `showParent` | 父级菜单(是否显示父级菜单 [点击查看更多](https://pure-admin.github.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)) |
|
||||
|
342
sop-admin/sop-admin-frontend/src/views/system/menu/form.vue
Normal file
@@ -0,0 +1,342 @@
|
||||
<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,
|
||||
fixedTagOptions,
|
||||
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,
|
||||
fixedTag: 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 !== 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="12"
|
||||
: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>
|
||||
|
||||
<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 < 2" :value="12" :xs="24" :sm="24">
|
||||
<el-form-item label="固定标签页">
|
||||
<Segmented
|
||||
:modelValue="newFormInline.fixedTag ? 0 : 1"
|
||||
:options="fixedTagOptions"
|
||||
@change="
|
||||
({ option: { value } }) => {
|
||||
newFormInline.fixedTag = value;
|
||||
}
|
||||
"
|
||||
/>
|
||||
</el-form-item>
|
||||
</re-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
163
sop-admin/sop-admin-frontend/src/views/system/menu/index.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<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();
|
||||
|
||||
function onFullscreen() {
|
||||
// 重置表格高度
|
||||
tableRef.value.setAdaptive();
|
||||
}
|
||||
</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] overflow-auto"
|
||||
>
|
||||
<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"
|
||||
@fullscreen="onFullscreen"
|
||||
>
|
||||
<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>
|
@@ -0,0 +1,108 @@
|
||||
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 fixedTagOptions: 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,
|
||||
fixedTagOptions,
|
||||
keepAliveOptions,
|
||||
hiddenTagOptions,
|
||||
showParentOptions,
|
||||
frameLoadingOptions
|
||||
};
|
@@ -0,0 +1,225 @@
|
||||
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 { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||
import { cloneDeep, isAllEmpty, deviceDetection } from "@pureadmin/utils";
|
||||
|
||||
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,
|
||||
fixedTag: row?.fixedTag ?? false,
|
||||
showLink: row?.showLink ?? true,
|
||||
showParent: row?.showParent ?? false
|
||||
}
|
||||
},
|
||||
width: "45%",
|
||||
draggable: true,
|
||||
fullscreen: deviceDetection(),
|
||||
fullscreenIcon: true,
|
||||
closeOnClickModal: false,
|
||||
contentRenderer: () => h(editForm, { ref: formRef, formInline: null }),
|
||||
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
|
||||
};
|
||||
}
|
@@ -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" }]
|
||||
});
|
@@ -0,0 +1,30 @@
|
||||
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;
|
||||
fixedTag: boolean;
|
||||
showLink: boolean;
|
||||
showParent: boolean;
|
||||
}
|
||||
interface FormProps {
|
||||
formInline: FormItemProps;
|
||||
}
|
||||
|
||||
export type { FormItemProps, FormProps };
|
56
sop-admin/sop-admin-frontend/src/views/system/role/form.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { formRules } from "./utils/rule";
|
||||
import { FormProps } from "./utils/types";
|
||||
|
||||
const props = withDefaults(defineProps<FormProps>(), {
|
||||
formInline: () => ({
|
||||
id: 0,
|
||||
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>
|
344
sop-admin/sop-admin-frontend/src/views/system/role/index.vue
Normal file
@@ -0,0 +1,344 @@
|
||||
<script setup lang="ts">
|
||||
import { useRole } from "./utils/hook";
|
||||
import { ref, computed, nextTick, onMounted } from "vue";
|
||||
import { PureTableBar } from "@/components/RePureTableBar";
|
||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||
import {
|
||||
delay,
|
||||
subBefore,
|
||||
deviceDetection,
|
||||
useResizeObserver
|
||||
} from "@pureadmin/utils";
|
||||
|
||||
// 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";
|
||||
import Close from "@iconify-icons/ep/close";
|
||||
import Check from "@iconify-icons/ep/check";
|
||||
|
||||
defineOptions({
|
||||
name: "SystemRole"
|
||||
});
|
||||
|
||||
const iconClass = computed(() => {
|
||||
return [
|
||||
"w-[22px]",
|
||||
"h-[22px]",
|
||||
"flex",
|
||||
"justify-center",
|
||||
"items-center",
|
||||
"outline-none",
|
||||
"rounded-[4px]",
|
||||
"cursor-pointer",
|
||||
"transition-colors",
|
||||
"hover:bg-[#0000000f]",
|
||||
"dark:hover:bg-[#ffffff1f]",
|
||||
"dark:hover:text-[#ffffffd9]"
|
||||
];
|
||||
});
|
||||
|
||||
const treeRef = ref();
|
||||
const formRef = ref();
|
||||
const tableRef = ref();
|
||||
const contentRef = ref();
|
||||
const treeHeight = ref();
|
||||
|
||||
const {
|
||||
form,
|
||||
isShow,
|
||||
curRow,
|
||||
loading,
|
||||
columns,
|
||||
rowStyle,
|
||||
dataList,
|
||||
treeData,
|
||||
treeProps,
|
||||
isLinkage,
|
||||
pagination,
|
||||
isExpandAll,
|
||||
isSelectAll,
|
||||
treeSearchValue,
|
||||
// buttonClass,
|
||||
onSearch,
|
||||
resetForm,
|
||||
openDialog,
|
||||
handleMenu,
|
||||
handleSave,
|
||||
handleDelete,
|
||||
filterMethod,
|
||||
transformI18n,
|
||||
onQueryChanged,
|
||||
// handleDatabase,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
handleSelectionChange
|
||||
} = useRole(treeRef);
|
||||
|
||||
onMounted(() => {
|
||||
useResizeObserver(contentRef, async () => {
|
||||
await nextTick();
|
||||
delay(60).then(() => {
|
||||
treeHeight.value = parseFloat(
|
||||
subBefore(tableRef.value.getTableDoms().tableWrapper.style.height, "px")
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
</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] overflow-auto"
|
||||
>
|
||||
<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>
|
||||
|
||||
<div
|
||||
ref="contentRef"
|
||||
:class="['flex', deviceDetection() ? 'flex-wrap' : '']"
|
||||
>
|
||||
<PureTableBar
|
||||
:class="[isShow && !deviceDetection() ? '!w-[60vw]' : 'w-full']"
|
||||
style="transition: width 220ms cubic-bezier(0.4, 0, 0.2, 1)"
|
||||
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
|
||||
ref="tableRef"
|
||||
align-whole="center"
|
||||
showOverflowTooltip
|
||||
table-layout="auto"
|
||||
:loading="loading"
|
||||
:size="size"
|
||||
adaptive
|
||||
:row-style="rowStyle"
|
||||
:adaptiveConfig="{ offsetBottom: 108 }"
|
||||
:data="dataList"
|
||||
:columns="dynamicColumns"
|
||||
:pagination="{ ...pagination, size }"
|
||||
: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.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-button
|
||||
class="reset-margin"
|
||||
link
|
||||
type="primary"
|
||||
:size="size"
|
||||
:icon="useRenderIcon(Menu)"
|
||||
@click="handleMenu(row)"
|
||||
>
|
||||
权限
|
||||
</el-button>
|
||||
<!-- <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
|
||||
v-if="isShow"
|
||||
class="!min-w-[calc(100vw-60vw-268px)] w-full mt-2 px-2 pb-2 bg-bg_color ml-2 overflow-auto"
|
||||
>
|
||||
<div class="flex justify-between w-full px-3 pt-5 pb-4">
|
||||
<div class="flex">
|
||||
<span :class="iconClass">
|
||||
<IconifyIconOffline
|
||||
v-tippy="{
|
||||
content: '关闭'
|
||||
}"
|
||||
class="dark:text-white"
|
||||
width="18px"
|
||||
height="18px"
|
||||
:icon="Close"
|
||||
@click="handleMenu"
|
||||
/>
|
||||
</span>
|
||||
<span :class="[iconClass, 'ml-2']">
|
||||
<IconifyIconOffline
|
||||
v-tippy="{
|
||||
content: '保存菜单权限'
|
||||
}"
|
||||
class="dark:text-white"
|
||||
width="18px"
|
||||
height="18px"
|
||||
:icon="Check"
|
||||
@click="handleSave"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<p class="font-bold truncate">
|
||||
菜单权限
|
||||
{{ `${curRow?.name ? `(${curRow.name})` : ""}` }}
|
||||
</p>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="treeSearchValue"
|
||||
placeholder="请输入菜单进行搜索"
|
||||
class="mb-1"
|
||||
clearable
|
||||
@input="onQueryChanged"
|
||||
/>
|
||||
<div class="flex flex-wrap">
|
||||
<el-checkbox v-model="isExpandAll" label="展开/折叠" />
|
||||
<el-checkbox v-model="isSelectAll" label="全选/全不选" />
|
||||
<el-checkbox v-model="isLinkage" label="父子联动" />
|
||||
</div>
|
||||
<el-tree-v2
|
||||
ref="treeRef"
|
||||
show-checkbox
|
||||
:data="treeData"
|
||||
:props="treeProps"
|
||||
:height="treeHeight"
|
||||
:check-strictly="!isLinkage"
|
||||
:filter-method="filterMethod"
|
||||
>
|
||||
<template #default="{ node }">
|
||||
<span>{{ transformI18n(node.label) }}</span>
|
||||
</template>
|
||||
</el-tree-v2>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
@@ -0,0 +1,316 @@
|
||||
import dayjs from "dayjs";
|
||||
import editForm from "../form.vue";
|
||||
import { handleTree } from "@/utils/tree";
|
||||
import { message } from "@/utils/message";
|
||||
import { ElMessageBox } from "element-plus";
|
||||
import { usePublicHooks } from "../../hooks";
|
||||
import { transformI18n } from "@/plugins/i18n";
|
||||
import { addDialog } from "@/components/ReDialog";
|
||||
import type { FormItemProps } from "../utils/types";
|
||||
import type { PaginationProps } from "@pureadmin/table";
|
||||
import { getKeyList, deviceDetection } from "@pureadmin/utils";
|
||||
import {
|
||||
addRole,
|
||||
updateRoleStatus,
|
||||
delRole,
|
||||
getRoleList,
|
||||
getRoleMenu,
|
||||
getRoleMenuIds,
|
||||
updateRole
|
||||
} from "@/api/system";
|
||||
import { type Ref, reactive, ref, onMounted, h, toRaw, watch } from "vue";
|
||||
import { StatusEnum } from "@/model/enums";
|
||||
|
||||
export function useRole(treeRef: Ref) {
|
||||
const form = reactive({
|
||||
name: "",
|
||||
code: "",
|
||||
status: ""
|
||||
});
|
||||
const curRow = ref();
|
||||
const formRef = ref();
|
||||
const dataList = ref([]);
|
||||
const treeIds = ref([]);
|
||||
const treeData = ref([]);
|
||||
const isShow = ref(false);
|
||||
const loading = ref(true);
|
||||
const isLinkage = ref(false);
|
||||
const treeSearchValue = ref();
|
||||
const switchLoadMap = ref({});
|
||||
const isExpandAll = ref(false);
|
||||
const isSelectAll = ref(false);
|
||||
const { switchStyle } = usePublicHooks();
|
||||
const treeProps = {
|
||||
value: "id",
|
||||
label: "title",
|
||||
children: "children"
|
||||
};
|
||||
const pagination = reactive<PaginationProps>({
|
||||
total: 0,
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
background: true
|
||||
});
|
||||
const columns: TableColumnList = [
|
||||
{
|
||||
label: "角色编号",
|
||||
prop: "id"
|
||||
},
|
||||
{
|
||||
label: "角色名称",
|
||||
prop: "name"
|
||||
},
|
||||
{
|
||||
label: "角色标识",
|
||||
prop: "code"
|
||||
},
|
||||
{
|
||||
label: "状态",
|
||||
cellRenderer: scope => (
|
||||
<el-switch
|
||||
size={scope.props.size === "small" ? "small" : "default"}
|
||||
loading={switchLoadMap.value[scope.index]?.loading}
|
||||
v-model={scope.row.status}
|
||||
active-value={StatusEnum.ENABLE}
|
||||
inactive-value={StatusEnum.DISABLE}
|
||||
active-text="已启用"
|
||||
inactive-text="已停用"
|
||||
inline-prompt
|
||||
style={switchStyle.value}
|
||||
onChange={() => onChange(scope as any)}
|
||||
/>
|
||||
),
|
||||
minWidth: 90
|
||||
},
|
||||
{
|
||||
label: "备注",
|
||||
prop: "remark",
|
||||
minWidth: 160
|
||||
},
|
||||
{
|
||||
label: "创建时间",
|
||||
prop: "createTime",
|
||||
minWidth: 160,
|
||||
formatter: ({ createTime }) =>
|
||||
dayjs(createTime).format("YYYY-MM-DD HH:mm:ss")
|
||||
},
|
||||
{
|
||||
label: "操作",
|
||||
fixed: "right",
|
||||
width: 210,
|
||||
slot: "operation"
|
||||
}
|
||||
];
|
||||
// const buttonClass = computed(() => {
|
||||
// return [
|
||||
// "!h-[20px]",
|
||||
// "reset-margin",
|
||||
// "!text-gray-500",
|
||||
// "dark:!text-white",
|
||||
// "dark:hover:!text-primary"
|
||||
// ];
|
||||
// });
|
||||
|
||||
function onChange({ row }) {
|
||||
ElMessageBox.confirm(
|
||||
`确认要<strong>${
|
||||
row.status === StatusEnum.DISABLE ? "停用" : "启用"
|
||||
}</strong><strong style='color:var(--el-color-primary)'>${
|
||||
row.name
|
||||
}</strong>吗?`,
|
||||
"系统提示",
|
||||
{
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
dangerouslyUseHTMLString: true,
|
||||
draggable: true
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
updateRoleStatus(row).then(_ => {
|
||||
message("操作成功", {
|
||||
type: "success"
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
row.status === StatusEnum.DISABLE
|
||||
? (row.status = StatusEnum.ENABLE)
|
||||
: (row.status = StatusEnum.DISABLE);
|
||||
});
|
||||
}
|
||||
|
||||
function handleDelete(row) {
|
||||
delRole(row).then(_ => {
|
||||
message("删除成功", { 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.pageIndex;
|
||||
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
const resetForm = formEl => {
|
||||
if (!formEl) return;
|
||||
formEl.resetFields();
|
||||
onSearch();
|
||||
};
|
||||
|
||||
function openDialog(title = "新增", row?: FormItemProps) {
|
||||
addDialog({
|
||||
title: `${title}角色`,
|
||||
props: {
|
||||
formInline: {
|
||||
id: row?.id ?? 0,
|
||||
name: row?.name ?? "",
|
||||
code: row?.code ?? "",
|
||||
remark: row?.remark ?? ""
|
||||
}
|
||||
},
|
||||
width: "40%",
|
||||
draggable: true,
|
||||
fullscreen: deviceDetection(),
|
||||
fullscreenIcon: true,
|
||||
closeOnClickModal: false,
|
||||
contentRenderer: () => h(editForm, { ref: formRef, formInline: null }),
|
||||
beforeSure: (done, { options }) => {
|
||||
const FormRef = formRef.value.getRef();
|
||||
const curData = options.props.formInline as FormItemProps;
|
||||
FormRef.validate(valid => {
|
||||
if (valid) {
|
||||
// 表单规则校验通过
|
||||
if (title === "新增") {
|
||||
// 实际开发先调用新增接口,再进行下面操作
|
||||
addRole(curData).then(_ => {
|
||||
done(); // 关闭弹框
|
||||
onSearch(); // 刷新表格数据
|
||||
});
|
||||
} else {
|
||||
// 实际开发先调用修改接口,再进行下面操作
|
||||
updateRole(curData).then(_ => {
|
||||
done(); // 关闭弹框
|
||||
onSearch(); // 刷新表格数据
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 菜单权限 */
|
||||
async function handleMenu(row?: any) {
|
||||
const { id } = row;
|
||||
if (id) {
|
||||
curRow.value = row;
|
||||
isShow.value = true;
|
||||
const { data } = await getRoleMenuIds({ id });
|
||||
treeRef.value.setCheckedKeys(data);
|
||||
} else {
|
||||
curRow.value = null;
|
||||
isShow.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 高亮当前权限选中行 */
|
||||
function rowStyle({ row: { id } }) {
|
||||
return {
|
||||
cursor: "pointer",
|
||||
background: id === curRow.value?.id ? "var(--el-fill-color-light)" : ""
|
||||
};
|
||||
}
|
||||
|
||||
/** 菜单权限-保存 */
|
||||
function handleSave() {
|
||||
const { id, name } = curRow.value;
|
||||
// 根据用户 id 调用实际项目中菜单权限修改接口
|
||||
console.log(id, treeRef.value.getCheckedKeys());
|
||||
message(`角色名称为${name}的菜单权限修改成功`, {
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
|
||||
/** 数据权限 可自行开发 */
|
||||
// function handleDatabase() {}
|
||||
|
||||
const onQueryChanged = (query: string) => {
|
||||
treeRef.value!.filter(query);
|
||||
};
|
||||
|
||||
const filterMethod = (query: string, node) => {
|
||||
return transformI18n(node.title)!.includes(query);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
onSearch();
|
||||
const { data } = await getRoleMenu();
|
||||
treeIds.value = getKeyList(data, "id");
|
||||
treeData.value = handleTree(data);
|
||||
});
|
||||
|
||||
watch(isExpandAll, val => {
|
||||
val
|
||||
? treeRef.value.setExpandedKeys(treeIds.value)
|
||||
: treeRef.value.setExpandedKeys([]);
|
||||
});
|
||||
|
||||
watch(isSelectAll, val => {
|
||||
val
|
||||
? treeRef.value.setCheckedKeys(treeIds.value)
|
||||
: treeRef.value.setCheckedKeys([]);
|
||||
});
|
||||
|
||||
return {
|
||||
form,
|
||||
isShow,
|
||||
curRow,
|
||||
loading,
|
||||
columns,
|
||||
rowStyle,
|
||||
dataList,
|
||||
treeData,
|
||||
treeProps,
|
||||
isLinkage,
|
||||
pagination,
|
||||
isExpandAll,
|
||||
isSelectAll,
|
||||
treeSearchValue,
|
||||
// buttonClass,
|
||||
onSearch,
|
||||
resetForm,
|
||||
openDialog,
|
||||
handleMenu,
|
||||
handleSave,
|
||||
handleDelete,
|
||||
filterMethod,
|
||||
transformI18n,
|
||||
onQueryChanged,
|
||||
// handleDatabase,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
handleSelectionChange
|
||||
};
|
||||
}
|
@@ -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" }]
|
||||
});
|
@@ -0,0 +1,16 @@
|
||||
// 虽然字段很少 但是抽离出来 后续有扩展字段需求就很方便了
|
||||
|
||||
interface FormItemProps {
|
||||
id: 0;
|
||||
/** 角色名称 */
|
||||
name: string;
|
||||
/** 角色编号 */
|
||||
code: string;
|
||||
/** 备注 */
|
||||
remark: string;
|
||||
}
|
||||
interface FormProps {
|
||||
formInline: FormItemProps;
|
||||
}
|
||||
|
||||
export type { FormItemProps, FormProps };
|
@@ -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>
|
@@ -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>
|
275
sop-admin/sop-admin-frontend/src/views/system/user/index.vue
Normal file
@@ -0,0 +1,275 @@
|
||||
<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,
|
||||
deviceDetection,
|
||||
onSearch,
|
||||
resetForm,
|
||||
onbatchDel,
|
||||
openDialog,
|
||||
onTreeSelect,
|
||||
handleUpdate,
|
||||
handleDelete,
|
||||
handleUpload,
|
||||
handleReset,
|
||||
handleRole,
|
||||
handleSizeChange,
|
||||
onSelectionCancel,
|
||||
handleCurrentChange,
|
||||
handleSelectionChange
|
||||
} = useUser(tableRef, treeRef);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['flex', 'justify-between', deviceDetection() && 'flex-wrap']">
|
||||
<tree
|
||||
ref="treeRef"
|
||||
:class="['mr-2', deviceDetection() ? 'w-full' : 'min-w-[200px]']"
|
||||
:treeData="treeData"
|
||||
:treeLoading="treeLoading"
|
||||
@tree-select="onTreeSelect"
|
||||
/>
|
||||
<div
|
||||
:class="[deviceDetection() ? ['w-full', 'mt-2'] : '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] overflow-auto"
|
||||
>
|
||||
<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, size }"
|
||||
: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>
|
@@ -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 |
@@ -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 |
215
sop-admin/sop-admin-frontend/src/views/system/user/tree.vue
Normal file
@@ -0,0 +1,215 @@
|
||||
<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[];
|
||||
}
|
||||
|
||||
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]",
|
||||
"!text-sm",
|
||||
"reset-margin",
|
||||
"!text-[var(--el-text-color-regular)]",
|
||||
"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="treeLoading"
|
||||
class="h-full bg-bg_color overflow-hidden relative"
|
||||
:style="{ minHeight: `calc(100vh - 141px)` }"
|
||||
>
|
||||
<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-scrollbar height="calc(90vh - 88px)">
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="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 }">
|
||||
<div
|
||||
:class="[
|
||||
'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
|
||||
"
|
||||
/>
|
||||
<span class="!w-[120px] !truncate" :title="node.label">
|
||||
{{ node.label }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-divider) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.el-tree) {
|
||||
--el-tree-node-hover-bg-color: transparent;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,535 @@
|
||||
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 userAvatar from "@/assets/user.jpg";
|
||||
import { usePublicHooks } from "../../hooks";
|
||||
import { addDialog } from "@/components/ReDialog";
|
||||
import type { PaginationProps } from "@pureadmin/table";
|
||||
import ReCropperPreview from "@/components/ReCropperPreview";
|
||||
import type { FormItemProps, RoleFormItemProps } from "../utils/types";
|
||||
import {
|
||||
getKeyList,
|
||||
isAllEmpty,
|
||||
hideTextAtIndex,
|
||||
deviceDetection
|
||||
} 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 || userAvatar}
|
||||
preview-src-list={Array.of(row.avatar || userAvatar)}
|
||||
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.pageIndex;
|
||||
|
||||
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,
|
||||
fullscreen: deviceDetection(),
|
||||
fullscreenIcon: true,
|
||||
closeOnClickModal: false,
|
||||
contentRenderer: () => h(editForm, { ref: formRef, formInline: null }),
|
||||
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%",
|
||||
closeOnClickModal: false,
|
||||
fullscreen: deviceDetection(),
|
||||
contentRenderer: () =>
|
||||
h(ReCropperPreview, {
|
||||
ref: cropRef,
|
||||
imgSrc: row.avatar || userAvatar,
|
||||
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,
|
||||
fullscreen: deviceDetection(),
|
||||
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,
|
||||
fullscreen: deviceDetection(),
|
||||
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,
|
||||
deviceDetection,
|
||||
onSearch,
|
||||
resetForm,
|
||||
onbatchDel,
|
||||
openDialog,
|
||||
onTreeSelect,
|
||||
handleUpdate,
|
||||
handleDelete,
|
||||
handleUpload,
|
||||
handleReset,
|
||||
handleRole,
|
||||
handleSizeChange,
|
||||
onSelectionCancel,
|
||||
handleCurrentChange,
|
||||
handleSelectionChange
|
||||
};
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
/** 局部重置 ElProgress 的部分样式 */
|
||||
.el-progress-bar__outer,
|
||||
.el-progress-bar__inner {
|
||||
border-radius: 0;
|
||||
}
|
@@ -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"
|
||||
}
|
||||
]
|
||||
});
|
@@ -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 };
|