completepage: 🍻 多语言页面完成

This commit is contained in:
bunny 2024-09-30 16:21:41 +08:00
parent d373c138fb
commit 789190eff4
37 changed files with 1435 additions and 1572 deletions

View File

@ -130,7 +130,7 @@ class PureHttp {
message(data.message, { type: 'warning' });
router.push('/').then();
removeToken();
} else if (data.code >= 209 && data.code < 300) {
} else if (data.code >= 201 && data.code < 300) {
message(data.message, { type: 'warning' });
} else if (data.code > 300) {
message(data.message, { type: 'error' });

View File

@ -33,7 +33,7 @@ export const fetchUpdateI18n = (data: any) => {
* ---
*/
export const fetchDeleteI18n = (data: any) => {
return http.request<BaseResult<object>>('put', 'i18n/deleteI18n', { data });
return http.request<BaseResult<object>>('delete', 'i18n/deleteI18n', { data });
};
/**

View File

@ -56,3 +56,10 @@ export const refreshTokenApi = (data?: object) => {
export const fetchLogout = (data?: object) => {
return http.request<BaseResult<any>>('post', 'user/logout', { data });
};
/**
*
*/
export const fetchGetUserinfoById = (data?: object) => {
return http.request<BaseResult<UserResult>>('get', 'user/getUserinfoById', { params: data });
};

1
src/assets/svg/back.svg Normal file
View File

@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 48 48"><path fill="#2F88FF" fill-rule="evenodd" stroke="#000" stroke-linejoin="round" stroke-width="4" d="M44 40.836q-7.34-8.96-13.036-10.168t-10.846-.365V41L4 23.545 20.118 7v10.167q9.523.075 16.192 6.833 6.668 6.758 7.69 16.836Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 300 B

View File

@ -0,0 +1 @@
<svg width="1em" height="1em" fill="none" class="t-icon t-icon-calendar" viewBox="0 0 16 16"><path fill="currentColor" d="M10 3H6V1.5H5V3H3a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-2V1.5h-1zM5 5h1V4h4v1h1V4h2v2H3V4h2zM3 7h10v6H3z"/></svg>

After

Width:  |  Height:  |  Size: 261 B

1
src/assets/svg/hot.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 1024 1024"><path fill="#FF5D50" d="M428.698 107.315c-6.503 72.192-36.352 207.258-160.256 337.408 3.686-48.025-7.117-83.763-19.047-107.673-6.605-13.159-26.06-10.599-28.877 3.84-5.734 29.44-20.582 75.059-57.6 137.779-71.628 121.395-62.566 459.878 340.736 459.878S934.093 585.728 876.8 442.522c-37.376-93.44-93.952-152.525-128.82-182.324-11.417-9.779-29.132-1.945-29.593 13.056-.921 30.464-7.321 73.37-33.075 102.144-.666-52.787-38.144-208.384-202.445-296.857-23.296-12.544-51.763 2.457-54.17 28.774z"/><path fill="#FFDF99" d="M702.26 678.4c-4.2-45.056-60.673-166.554-212.634-246.426-10.599-5.58-23.092 3.124-21.504 15.002 6.246 46.848 12.953 140.493-24.064 184.73 4.044-40.397-18.125-73.83-36.66-94.31-8.396-9.217-23.552-4.66-25.497 7.68-3.533 22.322-12.851 56.268-36.557 97.945-42.086 74.035-86.989 188.672 124.57 294.656 10.956.563 22.17.87 33.74.87a618 618 0 0 0 32.717-.87C694.631 878.182 709.837 759.706 702.26 678.4"/></svg>

After

Width:  |  Height:  |  Size: 1004 B

View File

@ -0,0 +1 @@
<svg width="1em" height="1em" fill="none" class="t-icon t-icon-laptop" viewBox="0 0 16 16"><path fill="currentColor" d="M2.5 12a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h11a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1zm0-1h11V4h-11zM15 13H1v1h14z"/></svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@ -0,0 +1 @@
<svg width="1em" height="1em" fill="none" class="t-icon t-icon-service" viewBox="0 0 16 16"><path fill="currentColor" d="M2.52 6.37a5.5 5.5 0 0 1 10.98.13v4c0 .05 0 .1-.02.15A4.5 4.5 0 0 1 9 14.7H8v-1h1a3.5 3.5 0 0 0 3.4-2.7h-1.9a.5.5 0 0 1-.5-.5v-4c0-.28.22-.5.5-.5h1.93a4.5 4.5 0 0 0-8.86 0H5.5c.28 0 .5.22.5.5v4a.5.5 0 0 1-.5.5H3a.5.5 0 0 1-.5-.5v-4c0-.04 0-.09.02-.13M12.5 7H11v3h1.5zm-9 0v3H5V7z"/></svg>

After

Width:  |  Height:  |  Size: 409 B

1
src/assets/svg/shop.svg Normal file
View File

@ -0,0 +1 @@
<svg width="1em" height="1em" fill="none" class="t-icon t-icon-shop" viewBox="0 0 16 16"><path fill="currentColor" d="M8 1a2.5 2.5 0 0 0-2.5 2.5V5h-2a.5.5 0 0 0-.5.5v9c0 .28.22.5.5.5h9a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5h-2V3.5A2.5 2.5 0 0 0 8 1m1.5 5v2h1V6H12v8H4V6h1.5v2h1V6zm0-1h-3V3.5a1.5 1.5 0 1 1 3 0z"/></svg>

After

Width:  |  Height:  |  Size: 317 B

View File

@ -0,0 +1 @@
<svg width="1em" height="1em" fill="none" class="t-icon t-icon-user-avatar" viewBox="0 0 16 16"><path fill="currentColor" d="M8 10.5c1.24 0 2.42.31 3.5.88v1.12h1v-1.14a.94.94 0 0 0-.49-.84 8.48 8.48 0 0 0-8.02 0 .94.94 0 0 0-.49.84v1.14h1v-1.12A7.5 7.5 0 0 1 8 10.5M10.5 6a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0m-1 0a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0"/><path fill="currentColor" d="M2.5 1.5a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1v-11a1 1 0 0 0-1-1zm11 1v11h-11v-11z"/></svg>

After

Width:  |  Height:  |  Size: 482 B

View File

@ -1,161 +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';
import { inputValue } from '@/components/ReIcon/src/hooks';
type ParameterCSSProperties = (item?: string) => CSSProperties | undefined;
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 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 = '';
}
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);
}
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>
<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>
<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);
}
}
</style>

View File

@ -1,23 +1,183 @@
<script lang="ts" setup>
import LocalSelect from '@/components/ReIcon/src/LocalSelect.vue';
import { inputValue } from '@/components/ReIcon/src/hooks';
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>
<LocalSelect />
<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;

View File

@ -1,8 +1,6 @@
import type { iconType } from './types';
import { type Component, defineComponent, h, ref } from 'vue';
import { FontIcon, IconifyIconOffline, IconifyIconOnline } from '../index';
export const inputValue = ref();
import type { iconType } from "./types";
import { h, defineComponent, type Component } from "vue";
import { IconifyIconOnline, IconifyIconOffline, FontIcon } from "../index";
/**
* `iconfont` `svg` `iconify`
@ -12,48 +10,52 @@ export const inputValue = ref();
* @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
});
}
});
}
}

View File

@ -0,0 +1,17 @@
<script lang="ts" setup>
defineProps({
index: {
type: Number,
default: 0,
},
image: {
type: String,
},
});
</script>
<template>
<el-image :initial-index="index" :preview-src-list="[image]" :src="image" class="w-[50px] h-[50px]" fit="fill" loading="lazy" preview-teleported />
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,14 @@
<script lang="ts" setup>
import { $t } from '@/plugins/i18n';
defineProps({
status: {
type: Boolean,
},
});
</script>
<template>
<el-tag v-show="!status" effect="dark" size="large" type="success">{{ $t('status.enable') }}</el-tag>
<el-tag v-show="status" effect="dark" size="large" type="danger">{{ $t('status.disable') }}</el-tag>
</template>

View File

@ -0,0 +1,134 @@
<script lang="ts" setup>
import userAvatarIcon from '@/assets/svg/user_avatar.svg?component';
import { columns } from './columns';
import TablePlus from '@/components/TableBar/src/TablePlus.vue';
import { onMounted, ref } from 'vue';
import TableImage from '@/components/Table/TableImage.vue';
import { fetchGetUserinfoById } from '@/api/v1/user';
import { $t } from '@/plugins/i18n';
const props = defineProps({
userId: { type: String as PropType<String> },
});
const userinfo = ref();
const loading = ref(false);
/**
* * 获取用户信息
*/
const getUserInfo = async () => {
// ID
if (!props.userId) return;
loading.value = true;
// web
const result = await fetchGetUserinfoById({ id: props.userId });
if (result.code === 200) {
userinfo.value = result.data;
}
loading.value = false;
};
onMounted(() => {
getUserInfo();
});
</script>
<template>
<div class="list-card-item">
<div v-if="userId && userinfo" class="list-card-item_detail bg-bg_color">
<el-row justify="space-between">
<div class="list-card-item_detail--logo">
<userAvatarIcon />
</div>
<el-tag :color="userinfo.status ? '#F67676' : '#00a870'" class="mx-1 list-card-item_detail--operation--tag" effect="dark">
{{ $t('user_status') }}{{ userinfo.status ? $t('disable') : $t('normal') }}
</el-tag>
</el-row>
<p class="list-card-item_detail--name text-text_color_primary">{{ $t('user_details') }}</p>
<TablePlus :column="columns" :data-list="[userinfo]" :loading="loading">
<template #avatar>
<table-image :image="userinfo.avatar" />
</template>
</TablePlus>
</div>
<el-empty v-else description="无数据" />
</div>
</template>
<style lang="scss" scoped>
.list-card-item {
display: flex;
flex-direction: column;
margin-bottom: 12px;
overflow: hidden;
cursor: pointer;
border-radius: 3px;
&_detail {
flex: 1;
min-height: 140px;
padding: 24px 32px;
&--logo {
display: flex;
align-items: center;
justify-content: center;
width: 46px;
height: 46px;
font-size: 26px;
color: #0052d9;
background: #e0ebff;
border-radius: 50%;
&__disabled {
color: #a1c4ff;
}
}
&--operation {
display: flex;
height: 100%;
&--tag {
border: 0;
}
}
&--name {
margin: 24px 0 8px;
font-size: 16px;
font-weight: 400;
}
&--desc {
display: -webkit-box;
height: 40px;
margin-bottom: 24px;
overflow: hidden;
font-size: 12px;
line-height: 20px;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
}
&--desc {
-webkit-line-clamp: 2;
}
}
&__disabled {
.list-card-item_detail--name,
.list-card-item_detail--desc {
color: var(--el-text-color-disabled);
}
.list-card-item_detail--operation--tag {
color: #bababa;
}
}
}
</style>

View File

@ -0,0 +1,29 @@
import { $t } from '@/plugins/i18n';
import UserinfoDialog from '@/components/Table/Userinfo/UserinfoDialog.vue';
import { addDialog } from '@/components/BaseDialog/index'; // 表格列字段
// 表格列字段
export const columns = [
{ label: $t('id'), prop: 'id' },
{ label: $t('avatar'), prop: 'avatar', slot: 'avatar' },
{ label: $t('nickName'), prop: 'nickName' },
{ label: $t('username'), prop: 'username' },
{ label: $t('email'), prop: 'email', width: 180 },
{ label: $t('phone'), prop: 'phone', width: 180 },
{ label: $t('sex'), prop: 'sex' },
{ label: $t('personDescription'), prop: 'personDescription', width: 180 },
{ label: $t('table.createTime'), prop: 'createTime', width: '160' },
{ label: $t('table.updateTime'), prop: 'updateTime', width: '160' },
];
/**
* *
* @param userId
*/
export const selectUserinfo = async (userId: string) => {
addDialog({
title: '查看用户信息',
draggable: true,
contentRenderer: (): JSX.Element => <UserinfoDialog userId={userId} />,
});
};

View File

@ -1,13 +1,29 @@
import { defineStore } from 'pinia';
import { addMenu, deletedMenuByIds, updateMenu } from '@/api/v1/system';
import { addMenu, deletedMenuByIds, getMenuList, updateMenu } from '@/api/v1/system';
import { storeMessage } from '@/utils/message';
import { handleTree } from '@/utils/tree';
export const userRouterStore = defineStore('routerStore', {
state() {
return {};
return {
datalist: [],
loading: false,
};
},
getters: {},
actions: {
/**
* *
*/
async getMenuList() {
const result = await getMenuList();
if (result.code === 200) {
this.datalist = handleTree(result.data as any);
return true;
}
return false;
},
/**
* *
* @param data

View File

@ -95,7 +95,6 @@ export const closeAllMessage = (): void => ElMessage.closeAll();
*/
export const storeMessage = (result: BaseResult<any>) => {
if (result.code !== 200) {
message(result.message, { type: 'warning' });
return false;
}
message(result.message, { type: 'success' });

View File

@ -27,7 +27,7 @@ defineExpose({ ruleFormRef });
<el-form ref="ruleFormRef" :model="form" :rules="rules" isDefault-icon label-position="left" label-width="135px">
<el-form-item label="选择添加语言分类" prop="typeName">
<el-select v-model="form.typeName" filterable placeholder="选择添加语言分类">
<el-option v-for="item in i18nTypeStore.datalist" :key="item.typeName" :label="item.typeName" :value="item.typeName" />
<el-option v-for="item in i18nTypeStore.datalist" :key="item.id" :label="item.typeName" :value="item.typeName" />
</el-select>
</el-form-item>

View File

@ -11,6 +11,7 @@ import PureTable from '@pureadmin/table';
import { columns } from '@/views/i18n/i18n-setting/utils/columns';
import Refresh from '@iconify-icons/ep/refresh';
import { $t } from '@/plugins/i18n';
import { selectUserinfo } from '@/components/Table/Userinfo/columns';
const tableRef = ref();
const pageFormRef = ref();
@ -43,14 +44,6 @@ const onPageSizeChange = async (value: number) => {
await onSearch();
};
/**
* * 选择框点击的行
* @param row
*/
const onSelectionChange = (row: any) => {
ids.value = row.map((item: any) => item.id);
};
onMounted(() => {
onSearch();
});
@ -95,10 +88,17 @@ onMounted(() => {
row-key="id"
showOverflowTooltip
table-layout="auto"
@selection-change="onSelectionChange"
@page-size-change="onPageSizeChange"
@page-current-change="onCurrentPageChange"
>
<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 v-show="row.menuType !== 3" :icon="useRenderIcon(AddFill)" :size="size" class="reset-margin" link type="primary"> {{ $t('add_new') }} </el-button>

View File

@ -10,8 +10,8 @@ export const columns: TableColumnList = [
{ label: $t('i18n.typeName'), prop: 'typeName' },
{ label: $t('table.updateTime'), prop: 'updateTime' },
{ label: $t('table.createTime'), prop: 'createTime' },
{ label: $t('table.createUser'), prop: 'createUser' },
{ label: $t('table.updateUser'), prop: 'updateUser' },
{ 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' },
];

View File

@ -1,15 +1,14 @@
import { h, ref } from 'vue';
import { userI18nStore } from '@/store/i18n/i18n';
import { messageBox } from '@/utils/message';
import { addDialog, closeDialog } from '@/components/BaseDialog/index';
import { deviceDetection } from '@pureadmin/utils';
import I18nDialog from '@/views/i18n/i18n-setting/i18n-dialog.vue';
import type { FormProps } from '@/views/i18n/i18n-setting/utils/types';
import { $t } from '@/plugins/i18n';
import { messageBox } from '@/utils/message';
export const formRef = ref();
const i18nStore = userI18nStore();
export const ids = ref<string[]>([]);
/**
* *
@ -26,7 +25,7 @@ export const onSearch = async () => {
*/
export const onAdd = () => {
addDialog({
title: `${$t('add_multilingual')}`,
title: $t('add_multilingual'),
width: '30%',
props: { formInline: { keyName: '', translation: '', typeName: '' } },
draggable: true,
@ -36,7 +35,7 @@ export const onAdd = () => {
contentRenderer: () => h(I18nDialog, { ref: formRef }),
footerButtons: [
{
label: '取消',
label: $t('cancel'),
text: true,
bg: true,
btnClick: ({ dialog: { options, index } }) => {
@ -61,7 +60,7 @@ export const onAdd = () => {
},
},
{
label: '继续添加',
label: $t('continue_adding'),
type: 'success',
text: true,
bg: true,
@ -86,9 +85,9 @@ export const onUpdate = (row: any) => {
const id = row.id;
addDialog({
title: `更新多语言`,
title: $t('update_multilingual'),
width: '30%',
props: { formInline: { keyName: row.keyName, translation: row.translation, typeId: row.typeId } },
props: { formInline: { keyName: row.keyName, translation: row.translation, typeName: row.typeName } },
draggable: true,
fullscreen: deviceDetection(),
fullscreenIcon: true,
@ -107,22 +106,20 @@ export const onUpdate = (row: any) => {
},
});
};
/**
* *
*/
export const onDelete = async () => {
export const onDelete = async (row: any) => {
const isConfirm = await messageBox({
message: '是否确认批量删除(此操作不可逆)',
title: '删除警告',
message: $t('confirm_delete'),
title: $t('delete_warning'),
showMessage: false,
confirmMessage: '删除成功',
cancelMessage: '取消删除',
confirmMessage: $t('delete_success'),
cancelMessage: $t('cancel_delete'),
});
if (isConfirm) {
const data = ids.value;
await i18nStore.deleteI18n(data);
await i18nStore.deleteI18n([row.id]);
await onSearch();
}
};

View File

@ -12,6 +12,8 @@ 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';
const tableRef = ref();
const formRef = ref();
@ -59,6 +61,8 @@ onMounted(() => {
:size="size"
adaptive
align-whole="center"
border
highlight-current-row
row-key="id"
showOverflowTooltip
table-layout="auto"
@ -67,6 +71,14 @@ onMounted(() => {
<TableIsDefaultTag :status="row.isDefault" />
</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)"> 修改 </el-button>
<el-button :icon="useRenderIcon(AddFill)" :size="size" class="reset-margin" link type="primary" @click="onAdd"> 新增 </el-button>

View File

@ -1,21 +1,22 @@
import { reactive, ref } from 'vue';
import { $t } from '@/plugins/i18n';
export const editMap = ref({});
export const columns: TableColumnList = [
{ label: 'id', prop: 'id' },
{ label: 'i18n_languageName', prop: 'typeName' },
{ label: 'i18n_summary', prop: 'summary' },
{ label: 'isDefault', prop: 'isDefault', slot: 'isDefault' },
{ label: 'updateTime', prop: 'updateTime' },
{ label: 'createTime', prop: 'createTime' },
{ label: 'createUser', prop: 'createUser' },
{ label: 'updateUser', prop: 'updateUser' },
{ label: 'operation', fixed: 'right', width: 210, slot: 'operation' },
{ label: $t('id'), prop: 'id' },
{ label: $t('i18n_typeName'), prop: 'typeName' },
{ label: $t('i18n_summary'), prop: 'summary' },
{ label: $t('isDefault'), prop: 'isDefault', slot: 'isDefault' },
{ label: $t('table.updateTime'), prop: 'updateTime' },
{ label: $t('table.createTime'), prop: 'createTime' },
{ 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({
typeName: [{ required: true, message: '填写语言名称(例如zh)', trigger: 'blur' }],
summary: [{ required: true, message: '填写语言详情', trigger: 'blur' }],
typeName: [{ required: true, message: `${$t('input')}${$t('i18n_typeName')}`, trigger: 'blur' }],
summary: [{ required: true, message: `${$t('input')}${$t('i18n_summary')}`, trigger: 'blur' }],
});

View File

@ -3,8 +3,8 @@ import { addDialog } from '@/components/BaseDialog/index';
import AddI18nType from '@/views/i18n/i18n-type-setting/i18n-type-dialog.vue';
import { userI18nTypeStore } from '@/store/i18n/i18nType';
import { h, ref } from 'vue';
import type { AddFormItemProps } from '@/views/i18n/i18n-type-setting/utils/types';
import { messageBox } from '@/utils/message';
import type { FormItemProps } from '@/views/i18n/i18n-type-setting/utils/types';
export const formRef = ref();
const i18nTypeStore = userI18nTypeStore();
@ -34,7 +34,7 @@ export function onAdd() {
closeOnClickModal: false,
contentRenderer: () => h(AddI18nType, { ref: formRef }),
beforeSure: (done, { options }) => {
const form = options.props.formInline as AddFormItemProps;
const form = options.props.formInline as FormItemProps;
formRef.value.formRef.validate(async (valid: any) => {
if (!valid) return;
@ -62,7 +62,7 @@ export function onUpdate(row: any) {
closeOnClickModal: false,
contentRenderer: () => h(AddI18nType, { ref: formRef }),
beforeSure: (done, { options }) => {
const form = options.props.formInline as AddFormItemProps;
const form = options.props.formInline as FormItemProps;
formRef.value.formRef.validate(async (valid: any) => {
if (!valid) return;

View File

@ -5,33 +5,11 @@ import { formRules } from '@/views/system/menu/utils/rule';
import { FormProps } from '@/views/system/menu/utils/types';
import { IconSelect } from '@/components/ReIcon';
import Segmented from '@/components/ReSegmented';
import ReAnimateSelector from '@/components/AnimateSelector';
import { fixedTagOptions, frameLoadingOptions, hiddenTagOptions, keepAliveOptions, menuTypeOptions, showLinkOptions, showParentOptions } from '@/enums';
import { $t } from '@/plugins/i18n';
import { menuTypeOptions, showLinkOptions } from '@/enums';
import { userMenuIconStore } from '@/store/modules/menuIcon';
const props = withDefaults(defineProps<FormProps>(), {
formInline: () => ({
menuType: 0,
higherMenuOptions: [],
parentId: 0,
title: '',
name: '',
path: '',
component: '',
rank: 99,
redirect: '',
icon: '',
enterTransition: '',
leaveTransition: '',
frameSrc: '',
frameLoading: true,
keepAlive: false,
hiddenTag: false,
fixedTag: false,
showLink: true,
showParent: false,
}),
formInline: () => ({}),
});
const menuIconStore = userMenuIconStore();
@ -66,7 +44,7 @@ defineExpose({ menuFormRef: ruleFormRef });
placeholder="请选择上级菜单"
>
<template #default="{ node, data }">
<span>{{ $t(data.title) }}</span>
<span>{{ data.title }}</span>
<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
</template>
</el-cascader>
@ -96,70 +74,26 @@ defineExpose({ menuFormRef: ruleFormRef });
</re-col>
<re-col :sm="24" :value="12" :xs="24">
<el-form-item label="菜单排序">
<el-form-item v-model="newFormInline.rank" label="菜单排序" prop="rank">
<el-input-number v-model="newFormInline.rank" :max="9999" :min="1" class="!w-full" controls-position="right" />
</el-form-item>
</re-col>
<re-col v-show="newFormInline.menuType === 0" :sm="24" :value="12" :xs="24">
<el-form-item label="路由重定向">
<el-input v-model="newFormInline.redirect" clearable placeholder="请输入默认跳转地址" />
</el-form-item>
</re-col>
<re-col :sm="24" :value="12" :xs="24">
<el-form-item label="菜单图标">
<el-form-item label="菜单图标" prop="icon">
<IconSelect v-model="newFormInline.icon" class="w-full" />
</el-form-item>
</re-col>
<re-col v-show="newFormInline.menuType < 2" :sm="24" :value="12" :xs="24">
<el-form-item label="进场动画">
<ReAnimateSelector v-model="newFormInline.enterTransition" placeholder="请选择页面进场加载动画" />
</el-form-item>
</re-col>
<re-col v-show="newFormInline.menuType < 2" :sm="24" :value="12" :xs="24">
<el-form-item label="离场动画">
<ReAnimateSelector v-model="newFormInline.leaveTransition" placeholder="请选择页面离场加载动画" />
</el-form-item>
</re-col>
<re-col v-show="newFormInline.menuType === 1" :sm="24" :value="12" :xs="24">
<!-- iframe -->
<el-form-item label="链接地址">
<el-form-item label="链接地址" prop="frameSrc">
<el-input v-model="newFormInline.frameSrc" clearable placeholder="请输入 iframe 链接地址" />
</el-form-item>
</re-col>
<re-col v-if="newFormInline.menuType === 1" :sm="24" :value="12" :xs="24">
<el-form-item label="加载动画">
<Segmented :modelValue="newFormInline.frameLoading ? 0 : 1" :options="frameLoadingOptions" @change="({ option: { value } }) => (newFormInline.frameLoading = value)" />
</el-form-item>
</re-col>
<re-col :sm="24" :value="12" :xs="24">
<el-form-item label="菜单">
<Segmented :modelValue="newFormInline.showLink ? 0 : 1" :options="showLinkOptions" @change="({ option: { value } }) => (newFormInline.showLink = value)" />
</el-form-item>
</re-col>
<re-col :sm="24" :value="12" :xs="24">
<el-form-item label="父级菜单">
<Segmented :modelValue="newFormInline.showParent ? 0 : 1" :options="showParentOptions" @change="({ option: { value } }) => (newFormInline.showParent = value)" />
</el-form-item>
</re-col>
<re-col v-show="newFormInline.menuType < 2" :sm="24" :value="12" :xs="24">
<el-form-item label="缓存页面">
<Segmented :modelValue="newFormInline.keepAlive ? 0 : 1" :options="keepAliveOptions" @change="({ option: { value } }) => (newFormInline.keepAlive = value)" />
</el-form-item>
</re-col>
<re-col v-show="newFormInline.menuType < 2" :sm="24" :value="12" :xs="24">
<el-form-item label="标签页">
<Segmented :modelValue="newFormInline.hiddenTag ? 1 : 0" :options="hiddenTagOptions" @change="({ option: { value } }) => (newFormInline.hiddenTag = value)" />
</el-form-item>
</re-col>
<re-col v-show="newFormInline.menuType < 2" :sm="24" :value="12" :xs="24">
<el-form-item label="固定标签页">
<Segmented :modelValue="newFormInline.fixedTag ? 0 : 1" :options="fixedTagOptions" @change="({ option: { value } }) => (newFormInline.fixedTag = value)" />
<el-form-item label="是否显示">
<Segmented :modelValue="newFormInline.visible ? 0 : 1" :options="showLinkOptions" @change="({ option: { value } }) => (newFormInline.visible = value)" />
</el-form-item>
</re-col>
</el-row>

View File

@ -7,10 +7,11 @@ 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 { dataList, handleDelete, loading, onSearch, openDialog, resetForm } from '@/views/system/menu/utils/hook';
import { handleDelete, onAdd, onSearch, onUpdate, resetForm } from '@/views/system/menu/utils/hook';
import form from '@/views/role/form.vue';
import PureTable from '@pureadmin/table';
import { columns } from '@/views/system/menu/utils/rule';
import { userRouterStore } from '@/store/modules/router';
defineOptions({
name: 'SystemMenu',
@ -18,6 +19,7 @@ defineOptions({
const formRef = ref();
const tableRef = ref();
const routerStore = userRouterStore();
onMounted(() => {
onSearch();
@ -31,23 +33,23 @@ onMounted(() => {
<el-input v-model="form.title" class="!w-[180px]" clearable placeholder="输入菜单名称" />
</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('ri:search-line')" :loading="routerStore.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" :isExpandAll="false" :tableRef="tableRef?.getTableRef()" title="菜单管理" @fullscreen="tableRef.setAdaptive()" @refresh="onSearch">
<template #buttons>
<el-button :icon="useRenderIcon(AddFill)" type="primary" @click="openDialog()"> 新增菜单</el-button>
<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="dataList"
:data="routerStore.datalist"
:header-cell-style="{ background: 'var(--el-fill-color-light)', color: 'var(--el-text-color-primary)' }"
:loading="loading"
:loading="routerStore.loading"
:size="size"
adaptive
align-whole="center"
@ -56,10 +58,8 @@ onMounted(() => {
table-layout="auto"
>
<template #operation="{ row }">
<el-button :icon="useRenderIcon(EditPen)" :size="size" class="reset-margin" link type="primary" @click="openDialog('修改', row)"> 修改 </el-button>
<el-button v-show="row.menuType !== 3" :icon="useRenderIcon(AddFill)" :size="size" class="reset-margin" link type="primary" @click="openDialog('新增', { parentId: row.id } as any)">
新增
</el-button>
<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>
<el-popconfirm :title="`是否确认删除菜单名称为${$t(row.title)}的这条数据${row?.children?.length > 0 ? '注意下级菜单也会一并删除,请谨慎操作' : ''}`" @confirm="handleDelete(row)">
<template #reference>
<el-button :icon="useRenderIcon(Delete)" :size="size" class="reset-margin" link type="primary"> 删除 </el-button>

View File

@ -1,13 +1,10 @@
import editForm from '../form.vue';
import { handleTree } from '@/utils/tree';
import { message } from '@/utils/message';
import { getMenuList } from '@/api/v1/system';
import { $t } from '@/plugins/i18n';
import { addDialog } from '@/components/BaseDialog/index';
import { h, reactive, ref } from 'vue';
import type { FormItemProps } from './types';
import { cloneDeep, deviceDetection, isAllEmpty } from '@pureadmin/utils';
import { cloneDeep, deviceDetection } from '@pureadmin/utils';
import { userRouterStore } from '@/store/modules/router';
const routerStore = userRouterStore();
@ -15,8 +12,6 @@ export const form = reactive({
title: '',
});
export const formRef = ref();
export const dataList = ref([]);
export const loading = ref(true);
/**
*
@ -38,28 +33,19 @@ export const getMenuType = (type, text = false) => {
*
* @param formEl
*/
export const resetForm = async formEl => {
export const resetForm = async (formEl: any) => {
if (!formEl) return;
formEl.resetFields();
await onSearch();
};
/**
* *
*/
export const onSearch = async () => {
loading.value = true;
// 获取菜单数据
const result: any = await getMenuList();
if (result.code !== 200) message(result.message, { type: 'error' });
// 前端搜索菜单名称
if (!isAllEmpty(form.title)) {
result.data = result.data.filter(item => $t(item.title).includes(form.title));
}
// 处理成树结构
dataList.value = handleTree(result.data);
loading.value = false;
routerStore.loading = true;
await routerStore.getMenuList();
routerStore.loading = false;
};
export const formatHigherMenuOptions = (treeList: any) => {
@ -73,30 +59,25 @@ export const formatHigherMenuOptions = (treeList: any) => {
return newTreeList;
};
export function openDialog(title = '新增', row?: FormItemProps) {
/**
* *
*/
export function onAdd(parentId: any = 0) {
addDialog({
title: `${title}菜单`,
title: `新增菜单`,
props: {
formInline: {
menuType: row?.menuType ?? 0,
higherMenuOptions: formatHigherMenuOptions(cloneDeep(dataList.value)),
parentId: row?.parentId ?? 0,
title: row?.title ?? '',
name: row?.name ?? '',
path: row?.path ?? '',
component: row?.component ?? '',
rank: row?.rank ?? 99,
redirect: row?.redirect ?? '',
icon: row?.icon ?? '',
enterTransition: row?.enterTransition ?? '',
leaveTransition: row?.leaveTransition ?? '',
frameSrc: row?.frameSrc ?? '',
frameLoading: row?.frameLoading ?? true,
keepAlive: row?.keepAlive ?? false,
hiddenTag: row?.hiddenTag ?? false,
fixedTag: row?.fixedTag ?? false,
showLink: row?.showLink ?? true,
showParent: row?.showParent ?? false,
menuType: 0,
higherMenuOptions: formatHigherMenuOptions(cloneDeep(routerStore.datalist)),
parentId,
title: '',
name: '',
path: '',
component: '',
rank: 99,
icon: '',
frameSrc: '',
visible: true,
},
},
width: '45%',
@ -104,7 +85,7 @@ export function openDialog(title = '新增', row?: FormItemProps) {
fullscreen: deviceDetection(),
fullscreenIcon: true,
closeOnClickModal: false,
contentRenderer: () => h(editForm, { ref: formRef, formInline: null }),
contentRenderer: () => h(editForm, { ref: formRef }),
beforeSure: (done, { options }) => {
const menuFormRef = formRef.value.menuFormRef;
const curData = options.props.formInline as FormItemProps;
@ -112,24 +93,67 @@ export function openDialog(title = '新增', row?: FormItemProps) {
if (!valid) return;
delete curData.higherMenuOptions;
let result: boolean;
if (title === '新增') {
result = await routerStore.addMenu(curData);
} else {
curData.id = row.id;
result = await routerStore.updateMenu(curData);
}
console.log(curData);
// 刷新表格数据
if (result) {
done();
await onSearch();
}
// const result = await routerStore.addMenu(curData);
// // 刷新表格数据
// if (result) {
// done();
// await onSearch();
// }
});
},
});
}
/**
* *
* @param row
*/
export const onUpdate = (row?: FormItemProps) => {
addDialog({
title: `更新菜单`,
props: {
formInline: {
menuType: row?.menuType,
higherMenuOptions: formatHigherMenuOptions(cloneDeep(routerStore.datalist)),
parentId: row?.parentId,
title: row?.title,
name: row?.name,
path: row?.path,
component: row?.component,
rank: row?.rank,
icon: row?.icon,
frameSrc: row?.frameSrc,
visible: row?.visible,
},
},
width: '45%',
draggable: true,
fullscreen: deviceDetection(),
fullscreenIcon: true,
closeOnClickModal: false,
contentRenderer: () => h(editForm, { ref: formRef }),
beforeSure: (done, { options }) => {
const menuFormRef = formRef.value.menuFormRef;
const curData = options.props.formInline as FormItemProps;
menuFormRef.validate(async (valid: any) => {
if (!valid) return;
delete curData.higherMenuOptions;
curData.id = row.id;
console.log(row);
// const result = await routerStore.updateMenu(curData);
// // 刷新表格数据
// if (result) {
// done();
// await onSearch();
// }
});
},
});
};
/**
* *
* @param row

View File

@ -9,17 +9,9 @@ interface FormItemProps {
path: string;
component: string;
rank: number;
redirect: string;
icon: string;
enterTransition: string;
leaveTransition: string;
frameSrc: string;
frameLoading: boolean;
keepAlive: boolean;
hiddenTag: boolean;
fixedTag: boolean;
showLink: boolean;
showParent: boolean;
visible: boolean;
}
interface FormProps {

View File

@ -1,176 +1,119 @@
<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";
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: ""
})
formInline: () => ({
title: '新增',
higherDeptOptions: [],
parentId: 0,
nickname: '',
username: '',
password: '',
phone: '',
email: '',
sex: '',
status: 1,
remark: '',
}),
});
const sexOptions = [
{
value: 0,
label: "男"
},
{
value: 1,
label: "女"
}
{
value: 0,
label: '男',
},
{
value: 1,
label: '女',
},
];
const ruleFormRef = ref();
const { switchStyle } = usePublicHooks();
const newFormInline = ref(props.formInline);
function getRef() {
return ruleFormRef.value;
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>
<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 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="邮箱" 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 :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>
<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,53 +1,42 @@
<script lang="ts" setup>
import { ref } from "vue";
import ReCol from "@/components/MyCol";
import { RoleFormProps } from "../utils/types";
import { ref } from 'vue';
import ReCol from '@/components/MyCol';
import { RoleFormProps } from '../utils/types';
const props = withDefaults(defineProps<RoleFormProps>(), {
formInline: () => ({
username: "",
nickname: "",
roleOptions: [],
ids: []
})
formInline: () => ({
username: '',
nickname: '',
roleOptions: [],
ids: [],
}),
});
const newFormInline = ref(props.formInline);
</script>
<template>
<el-form :model="newFormInline">
<el-row :gutter="30">
<!-- <re-col>
<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>
<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,21 +1,21 @@
<script setup lang="ts">
import { ref } from "vue";
import tree from "./tree.vue";
import { useUser } from "./utils/hook";
import { PureTableBar } from "@/components/RePureTableBar";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
<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";
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"
name: 'SystemUser',
});
const treeRef = ref();
@ -23,253 +23,139 @@ 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
form,
loading,
columns,
dataList,
treeData,
treeLoading,
selectedNum,
pagination,
buttonClass,
deviceDetection,
onSearch,
resetForm,
onbatchDel,
openDialog,
onTreeSelect,
handleUpdate,
handleDelete,
handleUpload,
handleReset,
handleRole,
handleSizeChange,
onSelectionCancel,
handleCurrentChange,
handleSelectionChange,
} = useUser(tableRef, treeRef);
</script>
<template>
<div :class="['flex', 'justify-between', deviceDetection() && 'flex-wrap']">
<tree
ref="treeRef"
:class="['mr-2', deviceDetection() ? 'w-full' : 'min-w-[200px]']"
:treeData="treeData"
:treeLoading="treeLoading"
@tree-select="onTreeSelect"
/>
<div
:class="[deviceDetection() ? ['w-full', 'mt-2'] : 'w-[calc(100%-200px)]']"
>
<el-form
ref="formRef"
:inline="true"
:model="form"
class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px] overflow-auto"
>
<el-form-item label="用户名称:" prop="username">
<el-input
v-model="form.username"
placeholder="请输入用户名称"
clearable
class="!w-[180px]"
/>
</el-form-item>
<el-form-item label="手机号码:" prop="phone">
<el-input
v-model="form.phone"
placeholder="请输入手机号码"
clearable
class="!w-[180px]"
/>
</el-form-item>
<el-form-item label="状态:" prop="status">
<el-select
v-model="form.status"
placeholder="请选择"
clearable
class="!w-[180px]"
>
<el-option label="已开启" value="1" />
<el-option label="已关闭" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:icon="useRenderIcon('ri:search-line')"
:loading="loading"
@click="onSearch"
>
搜索
</el-button>
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
重置
</el-button>
</el-form-item>
</el-form>
<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
title="用户管理(仅演示,操作后不生效)"
:columns="columns"
@refresh="onSearch"
>
<template #buttons>
<el-button
type="primary"
:icon="useRenderIcon(AddFill)"
@click="openDialog()"
>
新增用户
</el-button>
</template>
<template v-slot="{ size, dynamicColumns }">
<div
v-if="selectedNum > 0"
v-motion-fade
class="bg-[var(--el-fill-color-light)] w-full h-[46px] mb-2 pl-4 flex items-center"
>
<div class="flex-auto">
<span
style="font-size: var(--el-font-size-base)"
class="text-[rgba(42,46,54,0.5)] dark:text-[rgba(220,220,242,0.5)]"
>
已选 {{ selectedNum }}
</span>
<el-button type="primary" text @click="onSelectionCancel">
取消选择
</el-button>
</div>
<el-popconfirm title="是否确认删除?" @confirm="onbatchDel">
<template #reference>
<el-button type="danger" text class="mr-1">
批量删除
</el-button>
</template>
</el-popconfirm>
</div>
<pure-table
ref="tableRef"
row-key="id"
adaptive
:adaptiveConfig="{ offsetBottom: 108 }"
align-whole="center"
table-layout="auto"
:loading="loading"
:size="size"
:data="dataList"
:columns="dynamicColumns"
:pagination="{ ...pagination, size }"
:header-cell-style="{
background: 'var(--el-fill-color-light)',
color: 'var(--el-text-color-primary)'
}"
@selection-change="handleSelectionChange"
@page-size-change="handleSizeChange"
@page-current-change="handleCurrentChange"
>
<template #operation="{ row }">
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(EditPen)"
@click="openDialog('修改', row)"
>
修改
</el-button>
<el-popconfirm
:title="`是否确认删除用户编号为${row.id}的这条数据`"
@confirm="handleDelete(row)"
>
<template #reference>
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(Delete)"
>
删除
</el-button>
</template>
</el-popconfirm>
<el-dropdown>
<el-button
class="ml-3 mt-[2px]"
link
type="primary"
:size="size"
:icon="useRenderIcon(More)"
@click="handleUpdate(row)"
/>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<el-button
:class="buttonClass"
link
type="primary"
:size="size"
:icon="useRenderIcon(Upload)"
@click="handleUpload(row)"
>
上传头像
</el-button>
</el-dropdown-item>
<el-dropdown-item>
<el-button
:class="buttonClass"
link
type="primary"
:size="size"
:icon="useRenderIcon(Password)"
@click="handleReset(row)"
>
重置密码
</el-button>
</el-dropdown-item>
<el-dropdown-item>
<el-button
:class="buttonClass"
link
type="primary"
:size="size"
:icon="useRenderIcon(Role)"
@click="handleRole(row)"
>
分配角色
</el-button>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</pure-table>
</template>
</PureTableBar>
</div>
</div>
<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 scoped lang="scss">
<style lang="scss" scoped>
:deep(.el-dropdown-menu__item i) {
margin: 0;
margin: 0;
}
:deep(.el-button:focus-visible) {
outline: none;
outline: none;
}
.main-content {
margin: 24px 24px 0 !important;
margin: 24px 24px 0 !important;
}
.search-form {
:deep(.el-form-item) {
margin-bottom: 12px;
}
:deep(.el-form-item) {
margin-bottom: 12px;
}
}
</style>

View File

@ -1,141 +1,106 @@
<script setup lang="ts">
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { ref, computed, watch, getCurrentInstance } from "vue";
<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 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";
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[];
id: number;
name: string;
highlight?: boolean;
children?: Tree[];
}
defineProps({
treeLoading: Boolean,
treeData: Array
treeLoading: Boolean,
treeData: Array,
});
const emit = defineEmits(["tree-select"]);
const emit = defineEmits(['tree-select']);
const treeRef = ref();
const isExpand = ref(true);
const searchValue = ref("");
const searchValue = ref('');
const highlightMap = ref({});
const { proxy } = getCurrentInstance();
const defaultProps = {
children: "children",
label: "name"
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"
];
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);
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 })
);
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;
}
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);
highlightMap.value = {};
searchValue.value = '';
toggleRowExpansionAll(true);
}
watch(searchValue, val => {
treeRef.value!.filter(val);
treeRef.value!.filter(val);
});
defineExpose({ onTreeReset });
</script>
<template>
<div
v-loading="treeLoading"
class="h-full bg-bg_color overflow-hidden relative"
:style="{ minHeight: `calc(100vh - 141px)` }"
>
<div class="flex items-center h-[34px]">
<el-input
v-model="searchValue"
class="ml-2"
size="small"
placeholder="请输入部门名称"
clearable
>
<template #suffix>
<el-icon class="el-input__icon">
<IconifyIconOffline
v-show="searchValue.length === 0"
icon="ri:search-line"
/>
</el-icon>
</template>
</el-input>
<el-dropdown :hide-on-click="false">
<IconifyIconOffline
class="w-[28px] cursor-pointer"
width="18px"
:icon="More2Fill"
/>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<el-button
:class="buttonClass"
link
type="primary"
:icon="useRenderIcon(isExpand ? ExpandIcon : UnExpandIcon)"
@click="toggleRowExpansionAll(isExpand ? false : true)"
>
{{ isExpand ? "折叠全部" : "展开全部" }}
</el-button>
</el-dropdown-item>
<!-- <el-dropdown-item>
<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
@ -146,70 +111,46 @@ defineExpose({ onTreeReset });
重置状态
</el-button>
</el-dropdown-item> -->
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<el-divider />
<el-scrollbar height="calc(90vh - 88px)">
<el-tree
ref="treeRef"
:data="treeData"
node-key="id"
size="small"
:props="defaultProps"
default-expand-all
:expand-on-click-node="false"
:filter-node-method="filterNode"
@node-click="nodeClick"
>
<template #default="{ node, data }">
<div
:class="[
'rounded',
'flex',
'items-center',
'select-none',
'hover:text-primary',
searchValue.trim().length > 0 &&
node.label.includes(searchValue) &&
'text-red-500',
highlightMap[node.id]?.highlight ? 'dark:text-primary' : ''
]"
:style="{
color: highlightMap[node.id]?.highlight
? 'var(--el-color-primary)'
: '',
background: highlightMap[node.id]?.highlight
? 'var(--el-color-primary-light-7)'
: 'transparent'
}"
>
<IconifyIconOffline
:icon="
data.type === 1
? OfficeBuilding
: data.type === 2
? LocationCompany
: Dept
"
/>
<span class="!w-[120px] !truncate" :title="node.label">
{{ node.label }}
</span>
</div>
</template>
</el-tree>
</el-scrollbar>
</div>
</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;
margin: 0;
}
:deep(.el-tree) {
--el-tree-node-hover-bg-color: transparent;
--el-tree-node-hover-bg-color: transparent;
}
</style>

View File

@ -1,538 +1,456 @@
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";
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([]);
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 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 handleUpdate(row) {
console.log(row);
}
function handleDelete(row) {
message(`您删除了用户编号为${row.id}的这条数据`, { type: "success" });
onSearch();
}
function handleDelete(row) {
message(`您删除了用户编号为${row.id}的这条数据`, { type: 'success' });
onSearch();
}
function handleSizeChange(val: number) {
console.log(`${val} items per page`);
}
function handleSizeChange(val: number) {
console.log(`${val} items per page`);
}
function handleCurrentChange(val: number) {
console.log(`current page: ${val}`);
}
function handleCurrentChange(val: number) {
console.log(`current page: ${val}`);
}
/** 当CheckBox选择项发生变化时会触发该事件 */
function handleSelectionChange(val) {
selectedNum.value = val.length;
// 重置表格高度
tableRef.value.setAdaptive();
}
/** 当CheckBox选择项发生变化时会触发该事件 */
function handleSelectionChange(val) {
selectedNum.value = val.length;
// 重置表格高度
tableRef.value.setAdaptive();
}
/** 取消选择 */
function onSelectionCancel() {
selectedNum.value = 0;
// 用于多选表格,清空用户的选择
tableRef.value.getTableRef().clearSelection();
}
/** 取消选择 */
function onSelectionCancel() {
selectedNum.value = 0;
// 用于多选表格,清空用户的选择
tableRef.value.getTableRef().clearSelection();
}
/** 批量删除 */
function onbatchDel() {
// 返回当前选中的行
const curSelected = tableRef.value.getTableRef().getSelectionRows();
// 接下来根据实际业务通过选中行的某项数据比如下面的id调用接口进行批量删除
message(`已删除用户编号为 ${getKeyList(curSelected, "id")} 的数据`, {
type: "success"
});
tableRef.value.getTableRef().clearSelection();
onSearch();
}
/** 批量删除 */
function 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;
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);
}
setTimeout(() => {
loading.value = false;
}, 500);
}
const resetForm = formEl => {
if (!formEl) return;
formEl.resetFields();
form.deptId = "";
treeRef.value.onTreeReset();
onSearch();
};
const resetForm = formEl => {
if (!formEl) return;
formEl.resetFields();
form.deptId = '';
treeRef.value.onTreeReset();
onSearch();
};
function onTreeSelect({ id, selected }) {
form.deptId = selected ? id : "";
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 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 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(); // 刷新表格数据
}
function chores() {
message(`${title}了用户名称为${curData.username}的这条数据`, {
type: 'success',
});
done(); // 关闭弹框
onSearch(); // 刷新表格数据
}
FormRef.validate(valid => {
if (valid) {
console.log("curData", curData);
// 表单规则校验通过
if (title === "新增") {
// 实际开发先调用新增接口,再进行下面操作
chores();
} else {
// 实际开发先调用修改接口,再进行下面操作
chores();
}
}
});
}
});
}
FormRef.validate(valid => {
if (valid) {
console.log('curData', curData);
// 表单规则校验通过
if (title === '新增') {
// 实际开发先调用新增接口,再进行下面操作
chores();
} else {
// 实际开发先调用修改接口,再进行下面操作
chores();
}
}
});
},
});
}
const cropRef = ref();
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()
});
}
/** 上传头像 */
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)
);
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(); // 刷新表格数据
}
});
}
});
}
/** 重置密码 */
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(); // 关闭弹框
}
});
}
/** 分配角色 */
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();
onMounted(async () => {
treeLoading.value = true;
onSearch();
// 归属部门
const { data } = await getDeptList();
higherDeptOptions.value = handleTree(data);
treeData.value = handleTree(data);
treeLoading.value = false;
// 归属部门
const { data } = await getDeptList();
higherDeptOptions.value = handleTree(data);
treeData.value = handleTree(data);
treeLoading.value = false;
// 角色列表
roleOptions.value = (await getAllRoleList()).data;
});
// 角色列表
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
};
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,39 +1,39 @@
import { reactive } from "vue";
import type { FormRules } from "element-plus";
import { isPhone, isEmail } from "@pureadmin/utils";
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"
}
]
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,36 +1,38 @@
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;
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;
formInline: FormItemProps;
}
interface RoleFormItemProps {
username: string;
nickname: string;
/** 角色列表 */
roleOptions: any[];
/** 选中的角色列表 */
ids: Record<number, unknown>[];
username: string;
nickname: string;
/** 角色列表 */
roleOptions: any[];
/** 选中的角色列表 */
ids: Record<number, unknown>[];
}
interface RoleFormProps {
formInline: RoleFormItemProps;
formInline: RoleFormItemProps;
}
export type { FormItemProps, FormProps, RoleFormItemProps, RoleFormProps };