fix: 🧩 路由相关修改和菜单图标缺陷修复,代码生成器修复

This commit is contained in:
Bunny 2024-10-03 13:44:52 +08:00
parent 575770ffeb
commit bf0d3f9c66
65 changed files with 730 additions and 7716 deletions

30
src/api/v1/menuIcon.ts Normal file
View File

@ -0,0 +1,30 @@
import { http } from '@/api/service/request';
import type { BaseResult, ResultTable } from '@/api/service/types';
/**
* ---
*/
export const fetchGetMenuIconList = (data: any) => {
return http.request<BaseResult<ResultTable>>('get', `menuIcon/getMenuIconList/${data.currentPage}/${data.pageSize}`, { params: data });
};
/**
* ---
*/
export const fetchAddMenuIcon = (data: any) => {
return http.request<BaseResult<object>>('post', 'menuIcon/addMenuIcon', { data });
};
/**
* ---
*/
export const fetchUpdateMenuIcon = (data: any) => {
return http.request<BaseResult<object>>('put', 'menuIcon/updateMenuIcon', { data });
};
/**
* ---
*/
export const fetchDeleteMenuIcon = (data: any) => {
return http.request<BaseResult<object>>('delete', 'menuIcon/deleteMenuIcon', { data });
};

View File

@ -12,8 +12,8 @@ export const getMenuIconList = (data: any) => {
};
/** 菜单管理-列表 */
export const getMenuList = (data?: any) => {
return http.request<BaseResult<ResultTable>>('get', `router/getMenus`, { data });
export const getMenusList = (data?: any) => {
return http.request<BaseResult<ResultTable>>('get', `router/getMenusList`, { params: data });
};
/** 菜单管理-添加菜单 */

View File

@ -1,6 +1,6 @@
import type { iconType } from "./types";
import { h, defineComponent, type Component } from "vue";
import { IconifyIconOnline, IconifyIconOffline, FontIcon } from "../index";
import type { iconType } from './types';
import { type Component, defineComponent, h } from 'vue';
import { FontIcon, IconifyIconOffline, IconifyIconOnline } from '../index';
/**
* `iconfont` `svg` `iconify`
@ -10,52 +10,48 @@ import { IconifyIconOnline, IconifyIconOffline, FontIcon } from "../index";
* @returns Component
*/
export function useRenderIcon(icon: any, attrs?: iconType): Component {
// iconfont
const ifReg = /^IF-/;
// typeof icon === "function" 属于SVG
if (ifReg.test(icon)) {
// iconfont
const name = icon.split(ifReg)[1];
const iconName = name.slice(
0,
name.indexOf(" ") == -1 ? name.length : name.indexOf(" ")
);
const iconType = name.slice(name.indexOf(" ") + 1, name.length);
return defineComponent({
name: "FontIcon",
render() {
return h(FontIcon, {
icon: iconName,
iconType,
...attrs
});
}
});
} else if (typeof icon === "function" || typeof icon?.render === "function") {
// svg
return attrs ? h(icon, { ...attrs }) : icon;
} else if (typeof icon === "object") {
return defineComponent({
name: "OfflineIcon",
render() {
return h(IconifyIconOffline, {
icon: icon,
...attrs
});
}
});
} else {
// 通过是否存在 : 符号来判断是在线还是本地图标,存在即是在线图标,反之
return defineComponent({
name: "Icon",
render() {
const IconifyIcon =
icon && icon.includes(":") ? IconifyIconOnline : IconifyIconOffline;
return h(IconifyIcon, {
icon: icon,
...attrs
});
}
});
}
// iconfont
const ifReg = /^IF-/;
// typeof icon === "function" 属于SVG
if (ifReg.test(icon)) {
// iconfont
const name = icon.split(ifReg)[1];
const iconName = name.slice(0, name.indexOf(' ') == -1 ? name.length : name.indexOf(' '));
const iconType = name.slice(name.indexOf(' ') + 1, name.length);
return defineComponent({
name: 'FontIcon',
render() {
return h(FontIcon, {
icon: iconName,
iconType,
...attrs,
});
},
});
} else if (typeof icon === 'function' || typeof icon?.render === 'function') {
// svg
return attrs ? h(icon, { ...attrs }) : icon;
} else if (typeof icon === 'object') {
return defineComponent({
name: 'OfflineIcon',
render() {
return h(IconifyIconOffline, {
icon: icon,
...attrs,
});
},
});
} else {
// 通过是否存在 : 符号来判断是在线还是本地图标,存在即是在线图标,反之
return defineComponent({
name: 'Icon',
render() {
const IconifyIcon = icon && icon.includes(':') ? IconifyIconOnline : IconifyIconOffline;
return h(IconifyIcon, {
icon: icon,
...attrs,
});
},
});
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +0,0 @@
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, IconSelect, FontIcon };

View File

@ -1,222 +0,0 @@
<script lang="ts" setup>
import { IconJson } from '@/components/ReIcon/data';
import { cloneDeep, isAllEmpty } from '@pureadmin/utils';
import { computed, CSSProperties, ref, 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
:popper-options="{
placement: 'auto',
}"
:width="350"
popper-class="pure-popper"
trigger="click"
@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" clearable placeholder="搜索图标" />
<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"
:style="iconItemStyle(item)"
:title="item"
class="icon-item p-2 cursor-pointer mr-2 mt-1 flex justify-center items-center border border-[#e5e7eb]"
@click="onChangeIcon(item)"
>
<IconifyIconOnline :icon="currentActiveType + item" height="20px" width="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
:current-page="currentPage"
:page-size="pageSize"
:pager-count="5"
:total="totalPage"
background
class="flex-auto ml-2"
layout="pager"
size="small"
@current-change="onCurrentChange"
/>
<el-button bg class="justify-end mr-2 ml-2" size="small" text type="danger" @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>

View File

@ -1,61 +0,0 @@
import type { iconType } from "./types";
import { h, defineComponent, type Component } from "vue";
import { IconifyIconOnline, IconifyIconOffline, FontIcon } from "../index";
/**
* `iconfont` `svg` `iconify`
* @see {@link https://pure-admin.github.io/pure-admin-doc/pages/icon/}
* @param icon
* @param attrs iconType
* @returns Component
*/
export function useRenderIcon(icon: any, attrs?: iconType): Component {
// iconfont
const ifReg = /^IF-/;
// typeof icon === "function" 属于SVG
if (ifReg.test(icon)) {
// iconfont
const name = icon.split(ifReg)[1];
const iconName = name.slice(
0,
name.indexOf(" ") == -1 ? name.length : name.indexOf(" ")
);
const iconType = name.slice(name.indexOf(" ") + 1, name.length);
return defineComponent({
name: "FontIcon",
render() {
return h(FontIcon, {
icon: iconName,
iconType,
...attrs
});
}
});
} else if (typeof icon === "function" || typeof icon?.render === "function") {
// svg
return attrs ? h(icon, { ...attrs }) : icon;
} else if (typeof icon === "object") {
return defineComponent({
name: "OfflineIcon",
render() {
return h(IconifyIconOffline, {
icon: icon,
...attrs
});
}
});
} else {
// 通过是否存在 : 符号来判断是在线还是本地图标,存在即是在线图标,反之
return defineComponent({
name: "Icon",
render() {
const IconifyIcon =
icon && icon.includes(":") ? IconifyIconOnline : IconifyIconOffline;
return h(IconifyIcon, {
icon: icon,
...attrs
});
}
});
}
}

View File

@ -1,48 +0,0 @@
import { h, defineComponent } from "vue";
// 封装iconfont组件默认`font-class`引用模式,支持`unicode`引用、`font-class`引用、`symbol`引用 https://www.iconfont.cn/help/detail?spm=a313x.7781069.1998910419.20&helptype=code
export default defineComponent({
name: "FontIcon",
props: {
icon: {
type: String,
default: ""
}
},
render() {
const attrs = this.$attrs;
if (Object.keys(attrs).includes("uni") || attrs?.iconType === "uni") {
return h(
"i",
{
class: "iconfont",
...attrs
},
this.icon
);
} else if (
Object.keys(attrs).includes("svg") ||
attrs?.iconType === "svg"
) {
return h(
"svg",
{
class: "icon-svg",
"aria-hidden": true
},
{
default: () => [
h("use", {
"xlink:href": `#${this.icon}`
})
]
}
);
} else {
return h("i", {
class: `iconfont ${this.icon}`,
...attrs
});
}
}
});

View File

@ -1,30 +0,0 @@
import { h, defineComponent } from "vue";
import { Icon as IconifyIcon, addIcon } from "@iconify/vue/dist/offline";
// Iconify Icon在Vue里本地使用用于内网环境
export default defineComponent({
name: "IconifyIconOffline",
components: { IconifyIcon },
props: {
icon: {
default: null
}
},
render() {
if (typeof this.icon === "object") addIcon(this.icon, this.icon);
const attrs = this.$attrs;
return h(
IconifyIcon,
{
icon: this.icon,
style: attrs?.style
? Object.assign(attrs.style, { outline: "none" })
: { outline: "none" },
...attrs
},
{
default: () => []
}
);
}
});

View File

@ -1,30 +0,0 @@
import { h, defineComponent } from "vue";
import { Icon as IconifyIcon } from "@iconify/vue";
// Iconify Icon在Vue里在线使用用于外网环境
export default defineComponent({
name: "IconifyIconOnline",
components: { IconifyIcon },
props: {
icon: {
type: String,
default: ""
}
},
render() {
const attrs = this.$attrs;
return h(
IconifyIcon,
{
icon: `${this.icon}`,
style: attrs?.style
? Object.assign(attrs.style, { outline: "none" })
: { outline: "none" },
...attrs
},
{
default: () => []
}
);
}
});

View File

@ -1,70 +0,0 @@
// 这里存放本地图标,在 src/layout/index.vue 文件中加载,避免在首启动加载
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);

View File

@ -1,20 +0,0 @@
export interface iconType {
// iconify (https://docs.iconify.design/icon-components/vue/#properties)
inline?: boolean;
width?: string | number;
height?: string | number;
horizontalFlip?: boolean;
verticalFlip?: boolean;
flip?: string;
rotate?: number | string;
color?: string;
horizontalAlign?: boolean;
verticalAlign?: boolean;
align?: string;
onLoad?: Function;
includes?: Function;
// svg 需要什么SVG属性自行添加
fill?: string;
// all icon
style?: object;
}

View File

@ -1,8 +0,0 @@
import reSegmented from "./src/index";
import { withInstall } from "@pureadmin/utils";
/** 分段控制器组件 */
export const ReSegmented = withInstall(reSegmented);
export default ReSegmented;
export type { OptionsType } from "./src/type";

View File

@ -1,157 +0,0 @@
.pure-segmented {
--pure-control-padding-horizontal: 12px;
--pure-control-padding-horizontal-sm: 8px;
--pure-segmented-track-padding: 2px;
--pure-segmented-line-width: 1px;
--pure-segmented-border-radius-small: 4px;
--pure-segmented-border-radius-base: 6px;
--pure-segmented-border-radius-large: 8px;
box-sizing: border-box;
display: inline-block;
padding: var(--pure-segmented-track-padding);
font-size: var(--el-font-size-base);
color: rgba(0, 0, 0, 0.65);
background-color: rgb(0 0 0 / 4%);
border-radius: var(--pure-segmented-border-radius-base);
}
.pure-segmented-block {
display: flex;
}
.pure-segmented-block .pure-segmented-item {
flex: 1;
min-width: 0;
}
.pure-segmented-block .pure-segmented-item > .pure-segmented-item-label > span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* small */
.pure-segmented.pure-segmented--small {
border-radius: var(--pure-segmented-border-radius-small);
}
.pure-segmented.pure-segmented--small .pure-segmented-item {
border-radius: var(--el-border-radius-small);
}
.pure-segmented.pure-segmented--small .pure-segmented-item > div {
min-height: calc(
var(--el-component-size-small) - var(--pure-segmented-track-padding) * 2
);
line-height: calc(
var(--el-component-size-small) - var(--pure-segmented-track-padding) * 2
);
padding: 0
calc(
var(--pure-control-padding-horizontal-sm) -
var(--pure-segmented-line-width)
);
}
/* large */
.pure-segmented.pure-segmented--large {
border-radius: var(--pure-segmented-border-radius-large);
}
.pure-segmented.pure-segmented--large .pure-segmented-item {
border-radius: calc(
var(--el-border-radius-base) + var(--el-border-radius-small)
);
}
.pure-segmented.pure-segmented--large .pure-segmented-item > div {
min-height: calc(
var(--el-component-size-large) - var(--pure-segmented-track-padding) * 2
);
line-height: calc(
var(--el-component-size-large) - var(--pure-segmented-track-padding) * 2
);
padding: 0
calc(
var(--pure-control-padding-horizontal) - var(--pure-segmented-line-width)
);
font-size: var(--el-font-size-medium);
}
/* default */
.pure-segmented-item {
position: relative;
text-align: center;
cursor: pointer;
border-radius: var(--el-border-radius-base);
transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.pure-segmented .pure-segmented-item > div {
min-height: calc(
var(--el-component-size) - var(--pure-segmented-track-padding) * 2
);
line-height: calc(
var(--el-component-size) - var(--pure-segmented-track-padding) * 2
);
padding: 0
calc(
var(--pure-control-padding-horizontal) - var(--pure-segmented-line-width)
);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
transition: 0.1s;
}
.pure-segmented-group {
position: relative;
display: flex;
align-items: stretch;
justify-items: flex-start;
width: 100%;
}
.pure-segmented-item-selected {
position: absolute;
top: 0;
left: 0;
box-sizing: border-box;
display: none;
width: 0;
height: 100%;
padding: 4px 0;
background-color: #fff;
border-radius: 4px;
box-shadow:
0 2px 8px -2px rgb(0 0 0 / 5%),
0 1px 4px -1px rgb(0 0 0 / 7%),
0 0 1px rgb(0 0 0 / 7%);
transition:
transform 0.5s cubic-bezier(0.645, 0.045, 0.355, 1),
width 0.5s cubic-bezier(0.645, 0.045, 0.355, 1);
will-change: transform, width;
}
.pure-segmented-item > input {
position: absolute;
inset-block-start: 0;
inset-inline-start: 0;
width: 0;
height: 0;
opacity: 0;
pointer-events: none;
}
.pure-segmented-item-label {
display: flex;
align-items: center;
justify-content: center;
}
.pure-segmented-item-icon svg {
width: 16px;
height: 16px;
}
.pure-segmented-item-disabled {
color: rgba(0, 0, 0, 0.25);
cursor: not-allowed;
}

View File

@ -1,216 +0,0 @@
import "./index.css";
import type { OptionsType } from "./type";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import {
useDark,
isNumber,
isFunction,
useResizeObserver
} from "@pureadmin/utils";
import {
type PropType,
h,
ref,
toRef,
watch,
nextTick,
defineComponent,
getCurrentInstance
} from "vue";
const props = {
options: {
type: Array<OptionsType>,
default: () => []
},
/** 默认选中,按照第一个索引为 `0` 的模式,可选(`modelValue`只有传`number`类型时才为响应式) */
modelValue: {
type: undefined,
require: false,
default: "0"
},
/** 将宽度调整为父元素宽度 */
block: {
type: Boolean,
default: false
},
/** 控件尺寸 */
size: {
type: String as PropType<"small" | "default" | "large">
},
/** 是否全局禁用,默认 `false` */
disabled: {
type: Boolean,
default: false
},
/** 当内容发生变化时,设置 `resize` 可使其自适应容器位置 */
resize: {
type: Boolean,
default: false
}
};
export default defineComponent({
name: "ReSegmented",
props,
emits: ["change", "update:modelValue"],
setup(props, { emit }) {
const width = ref(0);
const translateX = ref(0);
const { isDark } = useDark();
const initStatus = ref(false);
const curMouseActive = ref(-1);
const segmentedItembg = ref("");
const instance = getCurrentInstance()!;
const curIndex = isNumber(props.modelValue)
? toRef(props, "modelValue")
: ref(0);
function handleChange({ option, index }, event: Event) {
if (props.disabled || option.disabled) return;
event.preventDefault();
isNumber(props.modelValue)
? emit("update:modelValue", index)
: (curIndex.value = index);
segmentedItembg.value = "";
emit("change", { index, option });
}
function handleMouseenter({ option, index }, event: Event) {
if (props.disabled) return;
event.preventDefault();
curMouseActive.value = index;
if (option.disabled || curIndex.value === index) {
segmentedItembg.value = "";
} else {
segmentedItembg.value = isDark.value
? "#1f1f1f"
: "rgba(0, 0, 0, 0.06)";
}
}
function handleMouseleave(_, event: Event) {
if (props.disabled) return;
event.preventDefault();
curMouseActive.value = -1;
}
function handleInit(index = curIndex.value) {
nextTick(() => {
const curLabelRef = instance?.proxy?.$refs[`labelRef${index}`] as ElRef;
if (!curLabelRef) return;
width.value = curLabelRef.clientWidth;
translateX.value = curLabelRef.offsetLeft;
initStatus.value = true;
});
}
function handleResizeInit() {
useResizeObserver(".pure-segmented", () => {
nextTick(() => {
handleInit(curIndex.value);
});
});
}
(props.block || props.resize) && handleResizeInit();
watch(
() => curIndex.value,
index => {
nextTick(() => {
handleInit(index);
});
},
{
immediate: true
}
);
watch(() => props.size, handleResizeInit, {
immediate: true
});
const rendLabel = () => {
return props.options.map((option, index) => {
return (
<label
ref={`labelRef${index}`}
class={[
"pure-segmented-item",
(props.disabled || option?.disabled) &&
"pure-segmented-item-disabled"
]}
style={{
background:
curMouseActive.value === index ? segmentedItembg.value : "",
color: props.disabled
? null
: !option.disabled &&
(curIndex.value === index || curMouseActive.value === index)
? isDark.value
? "rgba(255, 255, 255, 0.85)"
: "rgba(0,0,0,.88)"
: ""
}}
onMouseenter={event => handleMouseenter({ option, index }, event)}
onMouseleave={event => handleMouseleave({ option, index }, event)}
onClick={event => handleChange({ option, index }, event)}
>
<input type="radio" name="segmented" />
<div
class="pure-segmented-item-label"
v-tippy={{
content: option?.tip,
zIndex: 41000
}}
>
{option.icon && !isFunction(option.label) ? (
<span
class="pure-segmented-item-icon"
style={{ marginRight: option.label ? "6px" : 0 }}
>
{h(
useRenderIcon(option.icon, {
...option?.iconAttrs
})
)}
</span>
) : null}
{option.label ? (
isFunction(option.label) ? (
h(option.label)
) : (
<span>{option.label}</span>
)
) : null}
</div>
</label>
);
});
};
return () => (
<div
class={{
"pure-segmented": true,
"pure-segmented-block": props.block,
"pure-segmented--large": props.size === "large",
"pure-segmented--small": props.size === "small"
}}
>
<div class="pure-segmented-group">
<div
class="pure-segmented-item-selected"
style={{
width: `${width.value}px`,
transform: `translateX(${translateX.value}px)`,
display: initStatus.value ? "block" : "none"
}}
></div>
{rendLabel()}
</div>
</div>
);
}
});

View File

@ -1,20 +0,0 @@
import type { VNode, Component } from "vue";
import type { iconType } from "@/components/ReIcon/src/types.ts";
export interface OptionsType {
/** 文字 */
label?: string | (() => VNode | Component);
/**
* @description `useRenderIcon`
* @see {@link https://pure-admin.github.io/pure-admin-doc/pages/icon/#%E9%80%9A%E7%94%A8%E5%9B%BE%E6%A0%87-userendericon-hooks }
*/
icon?: string | Component;
/** 图标属性、样式配置 */
iconAttrs?: iconType;
/** 值 */
value?: any;
/** 是否禁用 */
disabled?: boolean;
/** `tooltip` 提示 */
tip?: string;
}

View File

@ -1,8 +1,8 @@
import reSegmented from "./src/index";
import { withInstall } from "@pureadmin/utils";
import reSegmented from './src/index';
import { withInstall } from '@pureadmin/utils';
/** 分段控制器组件 */
export const ReSegmented = withInstall(reSegmented);
export default ReSegmented;
export type { OptionsType } from "./src/type";
export type { OptionsType } from './src/type';

View File

@ -126,13 +126,7 @@ export default defineComponent({
onClick={event => handleChange({ option, index }, event)}
>
<input type='radio' name='segmented' />
<div
class='pure-segmented-item-label'
v-tippy={{
content: option?.tip,
zIndex: 41000,
}}
>
<div class='pure-segmented-item-label' v-tippy={{ content: option?.tip, zIndex: 41000 }}>
{option.icon && !isFunction(option.label) ? (
<span class='pure-segmented-item-icon' style={{ marginRight: option.label ? '6px' : 0 }}>
{h(

View File

@ -1,20 +1,20 @@
import type { Component, VNode } from "vue";
import type { iconType } from "@/components/CommonIcon/src/types.ts";
import type { Component, VNode } from 'vue';
import type { iconType } from '@/components/CommonIcon/src/types.ts';
export interface OptionsType {
/** 文字 */
label?: string | (() => VNode | Component);
/**
* @description `useRenderIcon`
* @see {@link https://pure-admin.github.io/pure-admin-doc/pages/icon/#%E9%80%9A%E7%94%A8%E5%9B%BE%E6%A0%87-userendericon-hooks }
*/
icon?: string | Component;
/** 图标属性、样式配置 */
iconAttrs?: iconType;
/** 值 */
value?: any;
/** 是否禁用 */
disabled?: boolean;
/** `tooltip` 提示 */
tip?: string;
/** 文字 */
label?: string | (() => VNode | Component);
/**
* @description `useRenderIcon`
* @see {@link https://pure-admin.github.io/pure-admin-doc/pages/icon/#%E9%80%9A%E7%94%A8%E5%9B%BE%E6%A0%87-userendericon-hooks }
*/
icon?: string | Component;
/** 图标属性、样式配置 */
iconAttrs?: iconType;
/** 值 */
value?: any;
/** 是否禁用 */
disabled?: boolean;
/** `tooltip` 提示 */
tip?: string;
}

View File

@ -0,0 +1,162 @@
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue';
import { fetchGetMenuIconList } from '@/api/v1/menuIcon';
import { FormProps } from '@/components/SelectIcon/types';
import { $t } from '@/plugins/i18n';
const props = withDefaults(defineProps<FormProps>(), {
formInline: () => ({
icon: '',
}),
});
const innerForm = reactive({
datalist: [],
currentPage: 1,
pageSize: 30,
total: 100,
loading: false,
});
const form = ref(props.formInline);
/**
* * 搜索和初始化
*/
const onSearch = async () => {
innerForm.loading = true;
//
const baseResult = await fetchGetMenuIconList(innerForm);
const data = baseResult.data;
//
innerForm.datalist = data.list;
innerForm.currentPage = data.pageNo;
innerForm.pageSize = data.pageSize;
innerForm.total = data.total;
innerForm.value = false;
};
/**
* * 修改图标
* @param value
*/
const onChangeIcon = (value: any) => {
form.value.icon = value.iconName;
};
/**
* * 清除图标
*/
const onClear = () => {
form.value.icon = '';
};
/**
* * 修改当前页
* @param value
*/
const onCurrentChange = (value: string) => {
innerForm.currentPage = value;
};
onMounted(() => {
onSearch();
});
</script>
<template>
<div class="selector">
<el-popover :popper-options="{ placement: 'auto' }" :width="350" popper-class="pure-popper" trigger="click">
<template #reference>
<div class="w-[60px] h-[32px] cursor-pointer flex justify-center items-center">
<el-text v-if="!form.icon" type="primary">{{ $t('select_icon') }}</el-text>
<IconifyIconOnline v-else :icon="form.icon" style="font-size: 32px" />
</div>
</template>
<ul class="flex flex-wrap px-2 ml-2">
<li
v-for="(item, key) in innerForm.datalist"
:key="key"
:title="item.iconName"
class="icon-item p-2 cursor-pointer mr-2 mt-1 flex justify-center items-center border border-[#e5e7eb]"
@click="onChangeIcon(item)"
>
<IconifyIconOnline :icon="item.iconName" height="20px" width="20px" />
</li>
</ul>
<el-empty v-show="innerForm.datalist.length === 0" :image-size="60" description="图标不存在" />
<div class="w-full h-9 flex items-center overflow-auto border-t border-[#e5e7eb]">
<el-pagination
:current-page="innerForm.currentPage"
:page-size="innerForm.pageSize"
:pager-count="5"
:total="innerForm.total"
background
class="flex-auto ml-2"
hide-on-single-page
layout="pager"
size="small"
@current-change="onCurrentChange"
/>
<el-button bg class="justify-end mr-2 ml-2" size="small" text type="danger" @click="onClear"> 清空</el-button>
</div>
</el-popover>
</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>

View File

@ -0,0 +1,8 @@
export interface FormItemProps {
/** 菜单类型0代表菜单、1代表iframe、2代表外链、3代表按钮*/
icon: string;
}
export interface FormProps {
formInline: FormItemProps;
}

View File

@ -1,75 +0,0 @@
<template>
<div class="main mt-2 p-2 bg-bg_color">
<pure-table
ref="tableRef"
:adaptiveConfig="{ offsetBottom: 108 }"
:columns="column"
:data="dataList"
:header-cell-style="cellHeaderStyle"
:loading="loading"
:pagination="pagination"
:paginationSmall="size === 'small'"
:size="size"
adaptive
align-whole="center"
border
row-key="id"
table-layout="auto"
@selection-change="handleSelectionChange"
@page-size-change="handleSizeChange"
@page-current-change="handleCurrentChange"
/>
</div>
</template>
<script lang="ts" setup>
import { cellHeaderStyle } from '@/components/TableBar/utils/tableStyle';
import PureTable from '@pureadmin/table';
import type { PropType } from 'vue';
// *
defineProps({
//
dataList: {
type: Array<any>,
default: [],
},
//
column: {
type: Array<any>,
default: [],
},
//
loading: {
type: Boolean,
default: false,
},
// small | default | large
size: {
type: String as PropType<any>,
default: 'default',
},
//
pagination: {
type: Object,
default: Object,
},
//
handleSelectionChange: {
type: Function as PropType<Function>,
default: () => {},
},
//
handleSizeChange: {
type: Function as PropType<Function>,
default: () => {},
},
//
handleCurrentChange: {
type: Function as PropType<Function>,
default: () => {},
},
});
</script>
<style lang="scss" scoped></style>

View File

@ -1,100 +0,0 @@
<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">
<slot name="tableForm" />
</el-form>
<div class="mt-2 p-2 bg-bg_color">
<pure-table
ref="tableRef"
:adaptiveConfig="{ offsetBottom: 108 }"
:columns="column"
:data="dataList"
:header-cell-style="cellHeaderStyle"
:loading="loading"
:pagination="pagination"
:paginationSmall="size === 'small'"
:size="size"
adaptive
align-whole="center"
border
row-key="id"
table-layout="auto"
@selection-change="handleSelectionChange"
@page-size-change="handleSizeChange"
@page-current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import { cellHeaderStyle } from '@/components/TableBar/utils/tableStyle';
import PureTable from '@pureadmin/table';
// *
defineProps({
//
dataList: {
type: Array<any>,
default: [],
},
//
column: {
type: Array<any>,
default: [],
},
//
loading: {
type: Boolean,
default: false,
},
// small | default | large
size: {
type: String as PropType<any>,
default: 'default',
},
//
pagination: {
type: Object,
default: Object,
},
//
handleSelectionChange: {
type: Function as PropType<Function>,
default: () => {},
},
//
handleSizeChange: {
type: Function as PropType<Function>,
default: () => {},
},
//
handleCurrentChange: {
type: Function as PropType<Function>,
default: () => {},
},
//
form: {
type: Object as PropType<any>,
default: Object,
},
});
</script>
<style lang="scss" scoped>
: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>

View File

@ -1,20 +1,20 @@
<script lang="ts" setup>
import { h, onMounted, ref, useSlots } from "vue";
import { type TippyOptions, useTippy } from "vue-tippy";
import { h, onMounted, ref, useSlots } from 'vue';
import { type TippyOptions, useTippy } from 'vue-tippy';
defineOptions({
name: "ReText"
name: 'ReText',
});
const props = defineProps({
//
lineClamp: {
type: [String, Number]
},
tippyProps: {
type: Object as PropType<TippyOptions>,
default: () => ({})
}
//
lineClamp: {
type: [String, Number],
},
tippyProps: {
type: Object as PropType<TippyOptions>,
default: () => ({}),
},
});
const $slots = useSlots();
@ -23,44 +23,44 @@ const textRef = ref();
const tippyFunc = ref();
const isTextEllipsis = (el: HTMLElement) => {
if (!props.lineClamp) {
//
return el.scrollWidth > el.clientWidth;
} else {
//
return el.scrollHeight > el.clientHeight;
}
if (!props.lineClamp) {
//
return el.scrollWidth > el.clientWidth;
} else {
//
return el.scrollHeight > el.clientHeight;
}
};
const getTippyProps = () => ({
content: h($slots.content || $slots.default),
...props.tippyProps
content: h($slots.content || $slots.default),
...props.tippyProps,
});
function handleHover(event: MouseEvent) {
if (isTextEllipsis(event.target as HTMLElement)) {
tippyFunc.value.setProps(getTippyProps());
tippyFunc.value.enable();
} else {
tippyFunc.value.disable();
}
if (isTextEllipsis(event.target as HTMLElement)) {
tippyFunc.value.setProps(getTippyProps());
tippyFunc.value.enable();
} else {
tippyFunc.value.disable();
}
}
onMounted(() => {
tippyFunc.value = useTippy(textRef.value?.$el, getTippyProps());
tippyFunc.value = useTippy(textRef.value?.$el, getTippyProps());
});
</script>
<template>
<el-text
ref="textRef"
v-bind="{
truncated: !lineClamp,
lineClamp,
...$attrs
}"
@mouseover.self="handleHover"
>
<slot />
</el-text>
<el-text
ref="textRef"
v-bind="{
truncated: !lineClamp,
lineClamp,
...$attrs,
}"
@mouseover.self="handleHover"
>
<slot />
</el-text>
</template>

View File

@ -1,4 +1,4 @@
import type { OptionsType } from '@/components/ReSegmented';
import type { OptionsType } from '@/components/Segmented';
export const menuTypeOptions: Array<OptionsType> = [
{ label: '菜单', value: 0 },
@ -8,11 +8,7 @@ export const menuTypeOptions: Array<OptionsType> = [
export const showLinkOptions: Array<OptionsType> = [
{ label: '显示', tip: '会在菜单中显示', value: true },
{
label: '隐藏',
tip: '不会在菜单中显示',
value: false,
},
{ label: '隐藏', tip: '不会在菜单中显示', value: false },
];
export const fixedTagOptions: Array<OptionsType> = [

View File

@ -2,6 +2,7 @@ import { defineStore } from 'pinia';
import { fetchAddI18n, fetchDeleteI18n, fetchGetI18n, fetchGetI18nList, fetchUpdateI18n } from '@/api/v1/i18n';
import { pageSizes } from '@/enums/baseConstant';
import { storeMessage } from '@/utils/message';
import { storePagination } from '@/store/useStorePagination';
export const userI18nStore = defineStore('i18nStore', {
persist: true,
@ -55,16 +56,9 @@ export const userI18nStore = defineStore('i18nStore', {
delete data.background;
const result = await fetchGetI18nList(data);
// 如果成功赋值内容
if (result.code === 200) {
this.datalist = result.data.list;
this.pagination.currentPage = result.data.pageNo;
this.pagination.pageSize = result.data.pageSize;
this.pagination.total = result.data.total;
return true;
}
return false;
// 公共页面函数hook
const pagination = storePagination.bind(this);
return pagination(result);
},
/**

View File

@ -4,7 +4,6 @@ import { pageSizes } from '@/enums/baseConstant';
import { storeMessage } from '@/utils/message';
export const userI18nTypeStore = defineStore('i18nTypeStore', {
persist: true,
state() {
return {
// 多语言列表

View File

@ -1,23 +1,72 @@
import { defineStore } from 'pinia';
import { getMenuIconList } from '@/api/v1/system';
import { fetchAddMenuIcon, fetchDeleteMenuIcon, fetchGetMenuIconList, fetchUpdateMenuIcon } from '@/api/v1/menuIcon';
import { pageSizes } from '@/enums/baseConstant';
import { storeMessage } from '@/utils/message';
import { storePagination } from '@/store/useStorePagination';
export const userMenuIconStore = defineStore('menuIconStore', {
/**
* Store
*/
export const useMenuIconStore = defineStore('menuIconStore', {
state() {
return {
menuIconList: [],
menuIconPage: { page: 1, limit: 30, total: 0 },
// 系统菜单图标列表
datalist: [],
// 查询表单
form: {
// icon 名称
iconName: undefined,
},
// 分页查询结果
pagination: {
currentPage: 1,
pageSize: 150,
total: 100,
pageSizes,
},
// 加载
loading: false,
};
},
getters: {},
actions: {
/**
* *
* *
*/
async getMenuIconList() {
const result = await getMenuIconList(this.menuIconPage);
if (result.code === 200) {
this.menuIconList = result.data.list;
}
const data = { ...this.pagination, ...this.form };
delete data.pageSizes;
delete data.total;
delete data.background;
const response = await fetchGetMenuIconList(data);
// 公共页面函数hook
const pagination = storePagination.bind(this);
return pagination(response);
},
/**
* *
*/
async addMenuIcon(data: any) {
const result = await fetchAddMenuIcon(data);
return storeMessage(result);
},
/**
* *
*/
async updateMenuIcon(data: any) {
const result = await fetchUpdateMenuIcon(data);
return storeMessage(result);
},
/**
* *
*/
async deleteMenuIcon(data: any) {
const result = await fetchDeleteMenuIcon(data);
return storeMessage(result);
},
},
});

View File

@ -1,5 +1,5 @@
import { defineStore } from 'pinia';
import { addMenu, deletedMenuByIds, getMenuList, updateMenu } from '@/api/v1/system';
import { addMenu, deletedMenuByIds, getMenusList, updateMenu } from '@/api/v1/system';
import { storeMessage } from '@/utils/message';
import { handleTree } from '@/utils/tree';
@ -7,6 +7,7 @@ export const userRouterStore = defineStore('routerStore', {
state() {
return {
datalist: [],
form: { title: undefined, visible: undefined },
loading: false,
};
},
@ -16,7 +17,9 @@ export const userRouterStore = defineStore('routerStore', {
* *
*/
async getMenuList() {
const result = await getMenuList();
const data = { ...this.pagination, ...this.form };
const result = await getMenusList(data);
if (result.code === 200) {
this.datalist = handleTree(result.data as any);
return true;

View File

@ -0,0 +1,15 @@
/**
* *
* @param response
*/
export function storePagination(response: any) {
// 返回成功为其赋值
if (response.code === 200) {
this.datalist = response.data.list;
this.pagination.currentPage = response.data.pageNo;
this.pagination.pageSize = response.data.pageSize;
this.pagination.total = response.data.total;
return true;
}
return false;
}

View File

@ -1,139 +0,0 @@
<script lang="ts" setup>
import { ref } from "vue";
import ReCol from "@/components/MyCol";
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"
:options="newFormInline.higherDeptOptions"
:props="{
value: 'id',
label: 'name',
emitPath: false,
checkStrictly: true
}"
class="w-full"
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 :sm="24" :value="12" :xs="24">
<el-form-item label="部门名称" prop="name">
<el-input
v-model="newFormInline.name"
clearable
placeholder="请输入部门名称"
/>
</el-form-item>
</re-col>
<re-col :sm="24" :value="12" :xs="24">
<el-form-item label="部门负责人">
<el-input
v-model="newFormInline.principal"
clearable
placeholder="请输入部门负责人"
/>
</el-form-item>
</re-col>
<re-col :sm="24" :value="12" :xs="24">
<el-form-item label="手机号" prop="phone">
<el-input
v-model="newFormInline.phone"
clearable
placeholder="请输入手机号"
/>
</el-form-item>
</re-col>
<re-col :sm="24" :value="12" :xs="24">
<el-form-item label="邮箱" prop="email">
<el-input
v-model="newFormInline.email"
clearable
placeholder="请输入邮箱"
/>
</el-form-item>
</re-col>
<re-col :sm="24" :value="12" :xs="24">
<el-form-item label="排序">
<el-input-number
v-model="newFormInline.sort"
:max="9999"
:min="0"
class="!w-full"
controls-position="right"
/>
</el-form-item>
</re-col>
<re-col :sm="24" :value="12" :xs="24">
<el-form-item label="部门状态">
<el-switch
v-model="newFormInline.status"
:active-value="1"
:inactive-value="0"
:style="switchStyle"
active-text="启用"
inactive-text="停用"
inline-prompt
/>
</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>

View File

@ -1,172 +0,0 @@
<script lang="ts" setup>
import { ref } from "vue";
import { useDept } from "./utils/hook";
import { PureTableBar } from "@/components/TableBar";
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"
class="!w-[180px]"
clearable
placeholder="请输入部门名称"
/>
</el-form-item>
<el-form-item label="状态:" prop="status">
<el-select
v-model="form.status"
class="!w-[180px]"
clearable
placeholder="请选择状态"
>
<el-option :value="1" label="启用" />
<el-option :value="0" label="停用" />
</el-select>
</el-form-item>
<el-form-item>
<el-button
:icon="useRenderIcon('ri:search-line')"
:loading="loading"
type="primary"
@click="onSearch"
>
搜索
</el-button>
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
重置
</el-button>
</el-form-item>
</el-form>
<PureTableBar
:columns="columns"
:tableRef="tableRef?.getTableRef()"
title="部门管理(仅演示,操作后不生效)"
@fullscreen="onFullscreen"
@refresh="onSearch"
>
<template #buttons>
<el-button
:icon="useRenderIcon(AddFill)"
type="primary"
@click="openDialog()"
>
新增部门
</el-button>
</template>
<template v-slot="{ size, dynamicColumns }">
<pure-table
ref="tableRef"
:adaptiveConfig="{ offsetBottom: 45 }"
:columns="dynamicColumns"
:data="dataList"
:header-cell-style="{
background: 'var(--el-fill-color-light)',
color: 'var(--el-text-color-primary)'
}"
:loading="loading"
:size="size"
adaptive
align-whole="center"
default-expand-all
row-key="id"
showOverflowTooltip
table-layout="auto"
@selection-change="handleSelectionChange"
>
<template #operation="{ row }">
<el-button
:icon="useRenderIcon(EditPen)"
:size="size"
class="reset-margin"
link
type="primary"
@click="openDialog('修改', row)"
>
修改
</el-button>
<el-button
:icon="useRenderIcon(AddFill)"
:size="size"
class="reset-margin"
link
type="primary"
@click="openDialog('新增', { parentId: row.id } as any)"
>
新增
</el-button>
<el-popconfirm
:title="`是否确认删除部门名称为${row.name}的这条数据`"
@confirm="handleDelete(row)"
>
<template #reference>
<el-button
:icon="useRenderIcon(Delete)"
:size="size"
class="reset-margin"
link
type="primary"
>
删除
</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>

View File

@ -1,180 +0,0 @@
import dayjs from "dayjs";
import editForm from "../form.vue";
import { handleTree } from "@/utils/tree";
import { message } from "@/utils/message";
import { getDeptList } from "@/api/v1/system";
import { usePublicHooks } from "../../hooks";
import { addDialog } from "@/components/BaseDialog";
import { h, onMounted, reactive, ref } from "vue";
import type { FormItemProps } from "../utils/types";
import { cloneDeep, deviceDetection, isAllEmpty } from "@pureadmin/utils";
export function useDept() {
const form = reactive({
name: "",
status: null
});
const formRef = ref();
const dataList = ref([]);
const loading = ref(true);
const { tagStyle } = usePublicHooks();
const columns: TableColumnList = [
{
label: "部门名称",
prop: "name",
width: 180,
align: "left"
},
{
label: "排序",
prop: "sort",
minWidth: 70
},
{
label: "状态",
prop: "status",
minWidth: 100,
cellRenderer: ({ row, props }) => (
<el-tag size={props.size} style={tagStyle.value(row.status)}>
{row.status === 1 ? "启用" : "停用"}
</el-tag>
)
},
{
label: "创建时间",
minWidth: 200,
prop: "createTime",
formatter: ({ createTime }) =>
dayjs(createTime).format("YYYY-MM-DD HH:mm:ss")
},
{
label: "备注",
prop: "remark",
minWidth: 320
},
{
label: "操作",
fixed: "right",
width: 210,
slot: "operation"
}
];
function handleSelectionChange(val) {
console.log("handleSelectionChange", val);
}
function resetForm(formEl) {
if (!formEl) return;
formEl.resetFields();
onSearch();
}
async function onSearch() {
loading.value = true;
const { data } = await getDeptList(); // 这里是返回一维数组结构前端自行处理成树结构返回格式要求唯一id加父节点parentIdparentId取父节点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
};
}

View File

@ -1,37 +0,0 @@
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"
}
]
});

View File

@ -1,16 +0,0 @@
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 };

View File

@ -2,7 +2,7 @@
import { onMounted, ref } from 'vue';
import { userI18nStore } from '@/store/i18n/i18n';
import { deleteIds, onAdd, onDelete, onDeleteBatch, onSearch, onUpdate } from '@/views/i18n/i18n-setting/utils/hook';
import { useRenderIcon } from '@/components/ReIcon/src/hooks';
import { useRenderIcon } from '@/components/CommonIcon/src/hooks';
import AddFill from '@iconify-icons/ri/add-circle-line';
import EditPen from '@iconify-icons/ep/edit-pen';
import Delete from '@iconify-icons/ep/delete';

View File

@ -4,7 +4,7 @@ import { FormInstance } from 'element-plus';
import { rules } from '@/views/i18n/i18n-type-setting/utils/columns';
import { FormProps } from '@/views/i18n/i18n-type-setting/utils/types';
import { frameSureOptions } from '@/enums';
import Segmented from '@/components/ReSegmented';
import Segmented from '@/components/Segmented';
const props = withDefaults(defineProps<FormProps>(), {
formInline: () => ({

View File

@ -2,7 +2,7 @@
import { onMounted, ref } from 'vue';
import { columns } from '@/views/i18n/i18n-type-setting/utils/columns';
import PureTableBar from '@/components/TableBar/src/bar';
import { useRenderIcon } from '@/components/ReIcon/src/hooks';
import { useRenderIcon } from '@/components/CommonIcon/src/hooks';
import AddFill from '@iconify-icons/ri/add-circle-line';
import PureTable from '@pureadmin/table';
import { userI18nTypeStore } from '@/store/i18n/i18nType';
@ -10,7 +10,6 @@ import { onAdd, onDelete, onSearch, onUpdate } from '@/views/i18n/i18n-type-sett
import Delete from '@iconify-icons/ep/delete';
import EditPen from '@iconify-icons/ep/edit-pen';
import TableIsDefaultTag from '@/components/TableBar/src/TableIsDefaultTag.vue';
import { resetForm } from '@/views/system/menu/utils/hook';
import Refresh from '@iconify-icons/ep/refresh';
import { selectUserinfo } from '@/components/Table/Userinfo/columns';
import { $t } from '@/plugins/i18n';

View File

@ -1,99 +0,0 @@
<script setup lang="ts">
import { hasAuth, getAuths } from "@/router/utils";
defineOptions({
name: "PermissionButtonRouter"
});
</script>
<template>
<div>
<p class="mb-2">当前拥有的code列表{{ getAuths() }}</p>
<el-card shadow="never" class="mb-2">
<template #header>
<div class="card-header">组件方式判断权限</div>
</template>
<el-space wrap>
<Auth value="permission:btn:add">
<el-button plain type="warning">
拥有code'permission:btn:add' 权限可见
</el-button>
</Auth>
<Auth :value="['permission:btn:edit']">
<el-button plain type="primary">
拥有code['permission:btn:edit'] 权限可见
</el-button>
</Auth>
<Auth
:value="[
'permission:btn:add',
'permission:btn:edit',
'permission:btn:delete'
]"
>
<el-button plain type="danger">
拥有code['permission:btn:add', 'permission:btn:edit',
'permission:btn:delete'] 权限可见
</el-button>
</Auth>
</el-space>
</el-card>
<el-card shadow="never" class="mb-2">
<template #header>
<div class="card-header">函数方式判断权限</div>
</template>
<el-space wrap>
<el-button v-if="hasAuth('permission:btn:add')" plain type="warning">
拥有code'permission:btn:add' 权限可见
</el-button>
<el-button v-if="hasAuth(['permission:btn:edit'])" plain type="primary">
拥有code['permission:btn:edit'] 权限可见
</el-button>
<el-button
v-if="
hasAuth([
'permission:btn:add',
'permission:btn:edit',
'permission:btn:delete'
])
"
plain
type="danger"
>
拥有code['permission:btn:add', 'permission:btn:edit',
'permission:btn:delete'] 权限可见
</el-button>
</el-space>
</el-card>
<el-card shadow="never">
<template #header>
<div class="card-header">
指令方式判断权限该方式不能动态修改权限
</div>
</template>
<el-space wrap>
<el-button v-auth="'permission:btn:add'" plain type="warning">
拥有code'permission:btn:add' 权限可见
</el-button>
<el-button v-auth="['permission:btn:edit']" plain type="primary">
拥有code['permission:btn:edit'] 权限可见
</el-button>
<el-button
v-auth="[
'permission:btn:add',
'permission:btn:edit',
'permission:btn:delete'
]"
plain
type="danger"
>
拥有code['permission:btn:add', 'permission:btn:edit',
'permission:btn:delete'] 权限可见
</el-button>
</el-space>
</el-card>
</div>
</template>

View File

@ -1,109 +0,0 @@
<script setup lang="ts">
import { hasPerms } from "@/utils/auth";
import { useUserStoreHook } from "@/store/modules/user";
const { permissions } = useUserStoreHook();
defineOptions({
name: "PermissionButtonLogin"
});
</script>
<template>
<div>
<p class="mb-2">当前拥有的code列表{{ permissions }}</p>
<p v-show="permissions?.[0] === '*:*:*'" class="mb-2">
*:*:* 代表拥有全部按钮级别权限
</p>
<el-card shadow="never" class="mb-2">
<template #header>
<div class="card-header">组件方式判断权限</div>
</template>
<el-space wrap>
<Perms value="permission:btn:add">
<el-button plain type="warning">
拥有code'permission:btn:add' 权限可见
</el-button>
</Perms>
<Perms :value="['permission:btn:edit']">
<el-button plain type="primary">
拥有code['permission:btn:edit'] 权限可见
</el-button>
</Perms>
<Perms
:value="[
'permission:btn:add',
'permission:btn:edit',
'permission:btn:delete'
]"
>
<el-button plain type="danger">
拥有code['permission:btn:add', 'permission:btn:edit',
'permission:btn:delete'] 权限可见
</el-button>
</Perms>
</el-space>
</el-card>
<el-card shadow="never" class="mb-2">
<template #header>
<div class="card-header">函数方式判断权限</div>
</template>
<el-space wrap>
<el-button v-if="hasPerms('permission:btn:add')" plain type="warning">
拥有code'permission:btn:add' 权限可见
</el-button>
<el-button
v-if="hasPerms(['permission:btn:edit'])"
plain
type="primary"
>
拥有code['permission:btn:edit'] 权限可见
</el-button>
<el-button
v-if="
hasPerms([
'permission:btn:add',
'permission:btn:edit',
'permission:btn:delete'
])
"
plain
type="danger"
>
拥有code['permission:btn:add', 'permission:btn:edit',
'permission:btn:delete'] 权限可见
</el-button>
</el-space>
</el-card>
<el-card shadow="never">
<template #header>
<div class="card-header">
指令方式判断权限该方式不能动态修改权限
</div>
</template>
<el-space wrap>
<el-button v-perms="'permission:btn:add'" plain type="warning">
拥有code'permission:btn:add' 权限可见
</el-button>
<el-button v-perms="['permission:btn:edit']" plain type="primary">
拥有code['permission:btn:edit'] 权限可见
</el-button>
<el-button
v-perms="[
'permission:btn:add',
'permission:btn:edit',
'permission:btn:delete'
]"
plain
type="danger"
>
拥有code['permission:btn:add', 'permission:btn:edit',
'permission:btn:delete'] 权限可见
</el-button>
</el-space>
</el-card>
</div>
</template>

View File

@ -1,59 +0,0 @@
<script lang="ts" setup>
import { initRouter } from '@/router/utils';
import { storageLocal } from '@pureadmin/utils';
import { computed, type CSSProperties, ref } from 'vue';
import { useUserStoreHook } from '@/store/modules/user';
import { usePermissionStoreHook } from '@/store/permission';
defineOptions({
name: 'PermissionPage',
});
const elStyle = computed((): CSSProperties => {
return {
width: '85vw',
justifyContent: 'start',
};
});
const username = ref(useUserStoreHook()?.username);
const options = [
{
value: 'admin',
label: '管理员角色',
},
{
value: 'common',
label: '普通角色',
},
];
function onChange() {
useUserStoreHook()
.loginByUsername({ username: username.value, password: 'admin123' })
.then(res => {
if (res.success) {
storageLocal().removeItem('async-routes');
usePermissionStoreHook().clearAllCachePage();
initRouter();
}
});
}
</script>
<template>
<div>
<p class="mb-2">模拟后台根据不同角色返回对应路由观察左侧菜单变化管理员角色可查看系统管理菜单普通角色不可查看系统管理菜单</p>
<el-card :style="elStyle" shadow="never">
<template #header>
<div class="card-header">
<span>当前角色{{ username }}</span>
</div>
</template>
<el-select v-model="username" class="!w-[160px]" @change="onChange">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-card>
</div>
</template>

View File

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

View File

@ -1,344 +0,0 @@
<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>

View File

@ -1,318 +0,0 @@
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 { addDialog } from "@/components/BaseDialog";
import type { FormItemProps } from "../utils/types";
import type { PaginationProps } from "@pureadmin/table";
import { deviceDetection, getKeyList } from "@pureadmin/utils";
import { getRoleList, getRoleMenu, getRoleMenuIds } from "@/api/v1/system";
import { h, onMounted, reactive, type Ref, ref, toRaw, watch } from "vue";
import { $t } from "@/plugins/i18n";
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={1}
inactive-value={0}
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, index }) {
ElMessageBox.confirm(
`确认要<strong>${
row.status === 0 ? "停用" : "启用"
}</strong><strong style='color:var(--el-color-primary)'>${
row.name
}</strong>?`,
"系统提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
dangerouslyUseHTMLString: true,
draggable: true
}
)
.then(() => {
switchLoadMap.value[index] = Object.assign(
{},
switchLoadMap.value[index],
{
loading: true
}
);
setTimeout(() => {
switchLoadMap.value[index] = Object.assign(
{},
switchLoadMap.value[index],
{
loading: false
}
);
message(`${row.status === 0 ? "停用" : "启用"}${row.name}`, {
type: "success"
});
}, 300);
})
.catch(() => {
row.status === 0 ? (row.status = 1) : (row.status = 0);
});
}
function handleDelete(row) {
message(`您删除了角色名称为${row.name}的这条数据`, { type: "success" });
onSearch();
}
function handleSizeChange(val: number) {
console.log(`${val} items per page`);
}
function handleCurrentChange(val: number) {
console.log(`current page: ${val}`);
}
function handleSelectionChange(val) {
console.log("handleSelectionChange", val);
}
async function onSearch() {
loading.value = true;
const { data } = await getRoleList(toRaw(form));
dataList.value = data.list;
pagination.total = data.total;
pagination.pageSize = data.pageSize;
pagination.currentPage = data.currentPage;
setTimeout(() => {
loading.value = false;
}, 500);
}
const resetForm = formEl => {
if (!formEl) return;
formEl.resetFields();
onSearch();
};
function openDialog(title = "新增", row?: FormItemProps) {
addDialog({
title: `${title}角色`,
props: {
formInline: {
name: row?.name ?? "",
code: row?.code ?? "",
remark: row?.remark ?? ""
}
},
width: "40%",
draggable: true,
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();
}
}
});
}
});
}
/** 菜单权限 */
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 $t(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,
onSearch,
resetForm,
openDialog,
handleMenu,
handleSave,
handleDelete,
filterMethod,
$t,
onQueryChanged,
handleSizeChange,
handleCurrentChange,
handleSelectionChange
};
}

View File

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

View File

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

View File

@ -1,25 +1,18 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { ref } from 'vue';
import ReCol from '@/components/MyCol';
import { formRules } from '@/views/system/menu/utils/rule';
import { formRules } from '@/views/system/menu/utils/columns';
import IconSelect from '@/components/SelectIcon/Select.vue';
import Segmented from '@/components/Segmented';
import { menuTypeOptions } from '@/enums';
import { FormProps } from '@/views/system/menu/utils/types';
import { IconSelect } from '@/components/ReIcon';
import Segmented from '@/components/ReSegmented';
import { menuTypeOptions, showLinkOptions } from '@/enums';
import { userMenuIconStore } from '@/store/modules/menuIcon';
const props = withDefaults(defineProps<FormProps>(), {
formInline: () => ({}),
});
const menuIconStore = userMenuIconStore();
const ruleFormRef = ref();
const newFormInline = ref(props.formInline);
onMounted(() => {
menuIconStore.getMenuIconList();
});
defineExpose({ menuFormRef: ruleFormRef });
</script>
@ -81,7 +74,7 @@ defineExpose({ menuFormRef: ruleFormRef });
<re-col :sm="24" :value="12" :xs="24">
<el-form-item label="菜单图标" prop="icon">
<IconSelect v-model="newFormInline.icon" class="w-full" />
<IconSelect :form-inline="newFormInline" class="w-full" />
</el-form-item>
</re-col>
@ -93,7 +86,9 @@ defineExpose({ menuFormRef: ruleFormRef });
<re-col :sm="24" :value="12" :xs="24">
<el-form-item label="是否显示">
<Segmented :modelValue="newFormInline.visible ? 0 : 1" :options="showLinkOptions" @change="({ option: { value } }) => (newFormInline.visible = value)" />
<el-switch v-model="newFormInline.visible" active-text="开启" inactive-text="隐藏" inline-prompt style="
--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949" />
</el-form-item>
</re-col>
</el-row>

View File

@ -2,25 +2,48 @@
import { onMounted, ref } from 'vue';
import { $t } from '@/plugins/i18n';
import PureTableBar from '@/components/TableBar/src/bar';
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';
import { handleDelete, onAdd, onSearch, onUpdate, resetForm } from '@/views/system/menu/utils/hook';
import form from '@/views/role/form.vue';
import form from '@/views/system/menu/form.vue';
import PureTable from '@pureadmin/table';
import { columns } from '@/views/system/menu/utils/rule';
import { columns } from '@/views/system/menu/utils/columns';
import { userRouterStore } from '@/store/modules/router';
defineOptions({
name: 'SystemMenu',
});
import { useRenderIcon } from '@/components/CommonIcon/src/hooks';
const formRef = ref();
const tableRef = ref();
const routerStore = userRouterStore();
/**
* * 修改菜单是否显示
* @param val
*/
const onchangeVisible = async (val: boolean) => {
const data = {
id: val.id,
visible: val.visible,
menuType: val.menuType,
title: val.title,
name: val.name,
path: val.path,
};
await routerStore.updateMenu(data);
await onSearch();
};
/**
* 表单重置
* @param formEl
*/
const resetForm = async (formEl: any) => {
if (!formEl) return;
formEl.resetFields();
await onSearch();
};
onMounted(() => {
onSearch();
});
@ -30,7 +53,7 @@ onMounted(() => {
<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" class="!w-[180px]" clearable placeholder="输入菜单名称" />
<el-input v-model="routerStore.form.title" class="!w-[180px]" clearable placeholder="输入菜单名称" />
</el-form-item>
<el-form-item>
<el-button :icon="useRenderIcon('ri:search-line')" :loading="routerStore.loading" type="primary" @click="onSearch"> 搜索 </el-button>
@ -53,10 +76,27 @@ onMounted(() => {
:size="size"
adaptive
align-whole="center"
border
highlight-current-row
row-key="id"
showOverflowTooltip
table-layout="auto"
>
<template #visible="{ row }">
<el-switch
v-model="row.visible"
active-text="显示"
class="ml-2"
inactive-text="隐藏"
inline-prompt
style="
--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
width="60"
@update:modelValue="onchangeVisible(row)"
/>
</template>
<template #operation="{ row }">
<el-button :icon="useRenderIcon(EditPen)" :size="size" class="reset-margin" link type="primary" @click="onUpdate(row)"> 修改 </el-button>
<el-button v-show="row.menuType !== 3" :icon="useRenderIcon(AddFill)" :size="size" class="reset-margin" link type="primary" @click="onAdd(row.id)"> 新增 </el-button>

View File

@ -1,9 +1,9 @@
import { h, reactive } from 'vue';
import type { FormRules } from 'element-plus';
import { useRenderIcon } from '@/components/ReIcon/src/hooks';
import { $t } from '@/plugins/i18n';
import { isAllEmpty } from '@pureadmin/utils';
import { getMenuType } from '@/views/system/menu/utils/hook';
import { useRenderIcon } from '@/components/CommonIcon/src/hooks';
export const columns: TableColumnList = [
{
@ -37,14 +37,8 @@ export const columns: TableColumnList = [
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: '隐藏', prop: 'visible', slot: 'visible', width: 100 },
{ label: '操作', fixed: 'right', width: 210, slot: 'operation' },
];

View File

@ -29,16 +29,6 @@ export const getMenuType = (type, text = false) => {
}
};
/**
*
* @param formEl
*/
export const resetForm = async (formEl: any) => {
if (!formEl) return;
formEl.resetFields();
await onSearch();
};
/**
* *
*/
@ -93,14 +83,12 @@ export function onAdd(parentId: any = 0) {
if (!valid) return;
delete curData.higherMenuOptions;
console.log(curData);
// const result = await routerStore.addMenu(curData);
// // 刷新表格数据
// if (result) {
// done();
// await onSearch();
// }
const result = await routerStore.addMenu(curData);
// 刷新表格数据
if (result) {
done();
await onSearch();
}
});
},
});
@ -125,7 +113,7 @@ export const onUpdate = (row?: FormItemProps) => {
rank: row?.rank,
icon: row?.icon,
frameSrc: row?.frameSrc,
visible: row?.visible,
visible: row.visible,
},
},
width: '45%',
@ -142,13 +130,12 @@ export const onUpdate = (row?: FormItemProps) => {
delete curData.higherMenuOptions;
curData.id = row.id;
console.log(row);
// const result = await routerStore.updateMenu(curData);
// // 刷新表格数据
// if (result) {
// done();
// await onSearch();
// }
const result = await routerStore.updateMenu(curData);
// 刷新表格数据
if (result) {
done();
await onSearch();
}
});
},
});

View File

@ -0,0 +1,94 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { columns } from '@/views/system/menuIcon/utils/columns';
import PureTableBar from '@/components/TableBar/src/bar';
import { useRenderIcon } from '@/components/CommonIcon/src/hooks';
import AddFill from '@iconify-icons/ri/add-circle-line';
import PureTable from '@pureadmin/table';
import { onAdd, onDelete, onSearch, onUpdate } from '@/views/system/menuIcon/utils/hook';
import Delete from '@iconify-icons/ep/delete';
import EditPen from '@iconify-icons/ep/edit-pen';
import Refresh from '@iconify-icons/ep/refresh';
import { selectUserinfo } from '@/components/Table/Userinfo/columns';
import { $t } from '@/plugins/i18n';
import { useMenuIconStore } from '@/store/modules/menuIcon';
const tableRef = ref();
const formRef = ref();
const menuIconStore = useMenuIconStore();
const resetForm = async formEl => {
if (!formEl) return;
formEl.resetFields();
await onSearch();
};
onMounted(() => {
onSearch();
});
</script>
<template>
<div class="main">
<el-form ref="formRef" :inline="true" :model="menuIconStore.form" class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px] overflow-auto">
<el-form-item :label="$t('menuIcon_iconName')" prop="iconName">
<el-input v-model="menuIconStore.form.iconName" :placeholder="`${$t('input')} ${$t('iconName')}`" class="!w-[180px]" clearable />
</el-form-item>
<el-form-item>
<el-button :icon="useRenderIcon('ri:search-line')" :loading="menuIconStore.loading" type="primary" @click="onSearch"> {{ $t('search') }} </el-button>
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)"> {{ $t('buttons.reset') }}</el-button>
</el-form-item>
</el-form>
<PureTableBar :columns="columns" title="系统菜单图标" @fullscreen="tableRef.setAdaptive()" @refresh="onSearch">
<template #buttons>
<el-button :icon="useRenderIcon(AddFill)" type="primary" @click="onAdd"> 添加系统菜单图标</el-button>
</template>
<template v-slot="{ size, dynamicColumns }">
<pure-table
ref="tableRef"
:adaptiveConfig="{ offsetBottom: 45 }"
:columns="dynamicColumns"
:data="menuIconStore.datalist"
:header-cell-style="{ background: 'var(--el-fill-color-light)', color: 'var(--el-text-color-primary)' }"
:loading="menuIconStore.loading"
:size="size"
adaptive
align-whole="center"
border
highlight-current-row
row-key="id"
showOverflowTooltip
table-layout="auto"
>
<template #iconName="{ row }">
<div class="flex justify-center">
<component :is="useRenderIcon(row.iconName)" class="flex justify-center" style="font-size: 30px" />
</div>
</template>
<template #createUser="{ row }">
<el-button link type="primary" @click="selectUserinfo(row.createUser)">{{ $t('table.createUser') }} </el-button>
</template>
<template #updateUser="{ row }">
<el-button link type="primary" @click="selectUserinfo(row.updateUser)">{{ $t('table.updateUser') }} </el-button>
</template>
<template #operation="{ row }">
<el-button :icon="useRenderIcon(EditPen)" :size="size" class="reset-margin" link type="primary" @click="onUpdate(row)"> {{ $t('modify') }} </el-button>
<el-button :icon="useRenderIcon(AddFill)" :size="size" class="reset-margin" link type="primary" @click="onAdd"> {{ $t('add_new') }} </el-button>
<el-popconfirm :title="`是否确认删除 ${row.iconName}数据`" @confirm="onDelete(row)">
<template #reference>
<el-button :icon="useRenderIcon(Delete)" :size="size" class="reset-margin" link type="primary">
{{ $t('delete') }}
</el-button>
</template>
</el-popconfirm>
</template>
</pure-table>
</template>
</PureTableBar>
</div>
</template>

View File

@ -0,0 +1,34 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { FormInstance } from 'element-plus';
import { rules } from '@/views/system/menuIcon/utils/columns';
import { FormProps } from '@/views/system/menuIcon/utils/types';
import { $t } from '@/plugins/i18n';
import { useRenderIcon } from '@/components/CommonIcon/src/hooks';
const props = withDefaults(defineProps<FormProps>(), {
formInline: () => ({
// icon
iconName: undefined,
}),
});
const formRef = ref<FormInstance>();
const form = ref(props.formInline);
defineExpose({ formRef });
</script>
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-width="auto">
<el-form-item :label="$t('menuIcon_iconName')" prop="iconName">
<el-input v-model="form.iconName" autocomplete="off" type="text" />
</el-form-item>
<el-form-item :label="$t('menuIcon_preview')">
<div class="flex justify-center">
<component :is="useRenderIcon(form.iconName)" class="flex justify-center" style="font-size: 30px" />
</div>
</el-form-item>
</el-form>
</template>

View File

@ -0,0 +1,22 @@
import { reactive } from 'vue';
import { $t } from '@/plugins/i18n';
// 表格列
export const columns: TableColumnList = [
{ type: 'index', index: (index: number) => index + 1 },
{ type: 'selection', align: 'left' },
{ label: $t('id'), prop: 'id' },
// icon 名称
{ label: $t('menuIcon_iconName'), prop: 'iconName', slot: 'iconName' },
{ label: $t('table.updateTime'), prop: 'updateTime', sortable: true },
{ label: $t('table.createTime'), prop: 'createTime', sortable: true },
{ label: $t('table.createUser'), prop: 'createUser', slot: 'createUser' },
{ label: $t('table.updateUser'), prop: 'updateUser', slot: 'updateUser' },
{ label: $t('table.operation'), fixed: 'right', width: 210, slot: 'operation' },
];
// 添加规则
export const rules = reactive({
// icon 名称
iconName: [{ required: true, message: `${$t('input')}${$t('menuIcon_iconName')}`, trigger: 'blur' }],
});

View File

@ -0,0 +1,103 @@
import { deviceDetection } from '@pureadmin/utils';
import { addDialog } from '@/components/BaseDialog/index';
import MenuIconDialog from '@/views/system/menuIcon/menu-icon-dialog.vue';
import { useMenuIconStore } from '@/store/modules/menuIcon';
import { h, ref } from 'vue';
import { messageBox } from '@/utils/message';
import type { FormItemProps } from '@/views/system/menuIcon/utils/types';
import { $t } from '@/plugins/i18n';
export const formRef = ref();
const menuIconStore = useMenuIconStore();
/**
* *
*/
export async function onSearch() {
menuIconStore.loading = true;
await menuIconStore.getMenuIconList();
menuIconStore.loading = false;
}
/**
* * ${}
*/
export function onAdd() {
addDialog({
title: `${$t('add_new')} ${$t('menuIcon')}`,
width: '30%',
props: {
formInline: {
iconName: undefined,
},
},
draggable: true,
fullscreen: deviceDetection(),
fullscreenIcon: true,
closeOnClickModal: false,
contentRenderer: () => h(MenuIconDialog, { ref: formRef }),
beforeSure: (done, { options }) => {
const form = options.props.formInline as FormItemProps;
formRef.value.formRef.validate(async (valid: any) => {
if (!valid) return;
const result = await menuIconStore.addMenuIcon(form);
if (!result) return;
done();
await onSearch();
});
},
});
}
/**
* *
* @param row
*/
export function onUpdate(row: any) {
addDialog({
title: `${$t('modify')} ${$t('menuIcon')}`,
width: '30%',
props: {
formInline: {
iconName: row.iconName,
},
},
draggable: true,
fullscreen: deviceDetection(),
fullscreenIcon: true,
closeOnClickModal: false,
contentRenderer: () => h(MenuIconDialog, { ref: formRef }),
beforeSure: (done, { options }) => {
const form = options.props.formInline as FormItemProps;
formRef.value.formRef.validate(async (valid: any) => {
if (!valid) return;
const result = await menuIconStore.updateMenuIcon({ ...form, id: row.id });
if (!result) return;
done();
await onSearch();
});
},
});
}
/**
* *
*/
export const onDelete = async (row: any) => {
const id = row.id;
// 是否确认删除
const result = await messageBox({
title: $t('confirm_delete'),
showMessage: false,
confirmMessage: undefined,
cancelMessage: $t('cancel_delete'),
});
if (!result) return;
// 删除数据
await menuIconStore.deleteMenuIcon([id]);
await onSearch();
};

View File

@ -0,0 +1,10 @@
// 添加或者修改表单元素
export interface FormItemProps {
// icon 名称
iconName: string;
}
// 添加或修改表单Props
export interface FormProps {
formInline: FormItemProps;
}

View File

@ -1,119 +0,0 @@
<script lang="ts" setup>
import { ref } from 'vue';
import ReCol from '@/components/MyCol';
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 :sm="24" :value="12" :xs="24">
<el-form-item label="用户昵称" prop="nickname">
<el-input v-model="newFormInline.nickname" clearable placeholder="请输入用户昵称" />
</el-form-item>
</re-col>
<re-col :sm="24" :value="12" :xs="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 === '新增'" :sm="24" :value="12" :xs="24">
<el-form-item label="用户密码" prop="password">
<el-input v-model="newFormInline.password" clearable placeholder="请输入用户密码" />
</el-form-item>
</re-col>
<re-col :sm="24" :value="12" :xs="24">
<el-form-item label="手机号" prop="phone">
<el-input v-model="newFormInline.phone" clearable placeholder="请输入手机号" />
</el-form-item>
</re-col>
<re-col :sm="24" :value="12" :xs="24">
<el-form-item label="邮箱" prop="email">
<el-input v-model="newFormInline.email" clearable placeholder="请输入邮箱" />
</el-form-item>
</re-col>
<re-col :sm="24" :value="12" :xs="24">
<el-form-item label="用户性别">
<el-select v-model="newFormInline.sex" class="w-full" clearable placeholder="请选择用户性别">
<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 :sm="24" :value="12" :xs="24">
<el-form-item label="归属部门">
<el-cascader
v-model="newFormInline.parentId"
:options="newFormInline.higherDeptOptions"
:props="{
value: 'id',
label: 'name',
emitPath: false,
checkStrictly: true,
}"
class="w-full"
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 === '新增'" :sm="24" :value="12" :xs="24">
<el-form-item label="用户状态">
<el-switch v-model="newFormInline.status" :active-value="1" :inactive-value="0" :style="switchStyle" active-text="启用" inactive-text="停用" inline-prompt />
</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>

View File

@ -1,42 +0,0 @@
<script lang="ts" setup>
import { ref } from 'vue';
import ReCol from '@/components/MyCol';
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" class="w-full" clearable multiple placeholder="请选择">
<el-option v-for="(item, index) in newFormInline.roleOptions" :key="index" :label="item.name" :value="item.id">
{{ item.name }}
</el-option>
</el-select>
</el-form-item>
</re-col>
</el-row>
</el-form>
</template>

View File

@ -1,161 +0,0 @@
<script lang="ts" setup>
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" class="!w-[180px]" clearable placeholder="请输入用户名称" />
</el-form-item>
<el-form-item label="手机号码:" prop="phone">
<el-input v-model="form.phone" class="!w-[180px]" clearable placeholder="请输入手机号码" />
</el-form-item>
<el-form-item label="状态:" prop="status">
<el-select v-model="form.status" class="!w-[180px]" clearable placeholder="请选择">
<el-option label="已开启" value="1" />
<el-option label="已关闭" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button :icon="useRenderIcon('ri:search-line')" :loading="loading" type="primary" @click="onSearch"> 搜索 </el-button>
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)"> 重置 </el-button>
</el-form-item>
</el-form>
<PureTableBar :columns="columns" title="用户管理(仅演示,操作后不生效)" @refresh="onSearch">
<template #buttons>
<el-button :icon="useRenderIcon(AddFill)" type="primary" @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 class="text-[rgba(42,46,54,0.5)] dark:text-[rgba(220,220,242,0.5)]" style="font-size: var(--el-font-size-base)"> 已选 {{ selectedNum }} </span>
<el-button text type="primary" @click="onSelectionCancel"> 取消选择 </el-button>
</div>
<el-popconfirm title="是否确认删除?" @confirm="onbatchDel">
<template #reference>
<el-button class="mr-1" text type="danger"> 批量删除 </el-button>
</template>
</el-popconfirm>
</div>
<pure-table
ref="tableRef"
:adaptiveConfig="{ offsetBottom: 108 }"
:columns="dynamicColumns"
:data="dataList"
:header-cell-style="{
background: 'var(--el-fill-color-light)',
color: 'var(--el-text-color-primary)',
}"
:loading="loading"
:pagination="{ ...pagination, size }"
:size="size"
adaptive
align-whole="center"
row-key="id"
table-layout="auto"
@selection-change="handleSelectionChange"
@page-size-change="handleSizeChange"
@page-current-change="handleCurrentChange"
>
<template #operation="{ row }">
<el-button :icon="useRenderIcon(EditPen)" :size="size" class="reset-margin" link type="primary" @click="openDialog('修改', row)"> 修改 </el-button>
<el-popconfirm :title="`是否确认删除用户编号为${row.id}的这条数据`" @confirm="handleDelete(row)">
<template #reference>
<el-button :icon="useRenderIcon(Delete)" :size="size" class="reset-margin" link type="primary"> 删除 </el-button>
</template>
</el-popconfirm>
<el-dropdown>
<el-button :icon="useRenderIcon(More)" :size="size" class="ml-3 mt-[2px]" link type="primary" @click="handleUpdate(row)" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<el-button :class="buttonClass" :icon="useRenderIcon(Upload)" :size="size" link type="primary" @click="handleUpload(row)"> 上传头像 </el-button>
</el-dropdown-item>
<el-dropdown-item>
<el-button :class="buttonClass" :icon="useRenderIcon(Password)" :size="size" link type="primary" @click="handleReset(row)"> 重置密码 </el-button>
</el-dropdown-item>
<el-dropdown-item>
<el-button :class="buttonClass" :icon="useRenderIcon(Role)" :size="size" link type="primary" @click="handleRole(row)"> 分配角色 </el-button>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</pure-table>
</template>
</PureTableBar>
</div>
</div>
</template>
<style lang="scss" scoped>
: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>

View File

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 161 B

View File

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 163 B

View File

@ -1,156 +0,0 @@
<script lang="ts" setup>
import { useRenderIcon } from '@/components/ReIcon/src/hooks';
import { computed, getCurrentInstance, ref, watch } 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" :style="{ minHeight: `calc(100vh - 141px)` }" class="h-full bg-bg_color overflow-hidden relative">
<div class="flex items-center h-[34px]">
<el-input v-model="searchValue" class="ml-2" clearable placeholder="请输入部门名称" size="small">
<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 :icon="More2Fill" class="w-[28px] cursor-pointer" width="18px" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<el-button :class="buttonClass" :icon="useRenderIcon(isExpand ? ExpandIcon : UnExpandIcon)" link type="primary" @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" :expand-on-click-node="false" :filter-node-method="filterNode" :props="defaultProps" default-expand-all node-key="id" size="small" @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 :title="node.label" class="!w-[120px] !truncate">
{{ 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>

View File

@ -1,456 +0,0 @@
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/BaseDialog';
import type { PaginationProps } from '@pureadmin/table';
import ReCropperPreview from '@/components/ReCropperPreview';
import type { FormItemProps, RoleFormItemProps } from '../utils/types';
import { deviceDetection, getKeyList, hideTextAtIndex, isAllEmpty } from '@pureadmin/utils';
import { getAllRoleList, getDeptList, getRoleIds, getUserList } from '@/api/v1/system';
import { ElForm, ElFormItem, ElInput, ElMessageBox, ElProgress } from 'element-plus';
import { computed, h, onMounted, reactive, ref, type Ref, toRaw, watch } 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.currentPage;
setTimeout(() => {
loading.value = false;
}, 500);
}
const resetForm = formEl => {
if (!formEl) return;
formEl.resetFields();
form.deptId = '';
treeRef.value.onTreeReset();
onSearch();
};
function onTreeSelect({ id, selected }) {
form.deptId = selected ? id : '';
onSearch();
}
function formatHigherDeptOptions(treeList) {
// 根据返回数据的status字段值判断追加是否禁用disabled字段返回处理后的树结构用于上级部门级联选择器的展示实际开发中也是如此不可能前端需要的每个字段后端都会返回这时需要前端自行根据后端返回的某些字段做逻辑处理
if (!treeList || !treeList.length) return;
const newTreeList = [];
for (let i = 0; i < treeList.length; i++) {
treeList[i].disabled = treeList[i].status === 0 ? true : false;
formatHigherDeptOptions(treeList[i].children);
newTreeList.push(treeList[i]);
}
return newTreeList;
}
function openDialog(title = '新增', row?: FormItemProps) {
addDialog({
title: `${title}用户`,
props: {
formInline: {
title,
higherDeptOptions: formatHigherDeptOptions(higherDeptOptions.value),
parentId: row?.dept.id ?? 0,
nickname: row?.nickname ?? '',
username: row?.username ?? '',
password: row?.password ?? '',
phone: row?.phone ?? '',
email: row?.email ?? '',
sex: row?.sex ?? '',
status: row?.status ?? 1,
remark: row?.remark ?? '',
},
},
width: '46%',
draggable: true,
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,
};
}

View File

@ -1,5 +0,0 @@
/** 局部重置 ElProgress 的部分样式 */
.el-progress-bar__outer,
.el-progress-bar__inner {
border-radius: 0;
}

View File

@ -1,39 +0,0 @@
import { reactive } from 'vue';
import type { FormRules } from 'element-plus';
import { isEmail, isPhone } 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',
},
],
});

View File

@ -1,38 +0,0 @@
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 };