mirror of
https://gitee.com/ccnetcore/Yi
synced 2026-04-12 12:16:38 +08:00
feat(project): 添加vben5前端
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
ele版本会使用这个文件 只是为了不报错`未找到对应组件`才新建的这个文件
|
||||
无实际意义
|
||||
</div>
|
||||
</template>
|
||||
210
Yi.Vben5.Vue3/apps/web-antd/src/views/system/user/data.tsx
Normal file
210
Yi.Vben5.Vue3/apps/web-antd/src/views/system/user/data.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import type { FormSchemaGetter } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { DictEnum } from '@vben/constants';
|
||||
import { getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { z } from '#/adapter/form';
|
||||
import { getDictOptions } from '#/utils/dict';
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'userName',
|
||||
label: '用户账号',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'nick',
|
||||
label: '用户昵称',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'phone',
|
||||
label: '手机号码',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
getPopupContainer,
|
||||
options: getDictOptions(DictEnum.SYS_NORMAL_DISABLE),
|
||||
},
|
||||
fieldName: 'state',
|
||||
label: '用户状态',
|
||||
},
|
||||
{
|
||||
component: 'RangePicker',
|
||||
fieldName: 'creationTime',
|
||||
label: '创建时间',
|
||||
},
|
||||
];
|
||||
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{
|
||||
field: 'userName',
|
||||
title: '账号',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'nick',
|
||||
title: '昵称',
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
field: 'icon',
|
||||
title: '头像',
|
||||
slots: { default: 'avatar' },
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
field: 'deptName',
|
||||
title: '部门',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'phone',
|
||||
title: '手机号',
|
||||
formatter({ cellValue }) {
|
||||
return cellValue || '暂无';
|
||||
},
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: 'state',
|
||||
title: '状态',
|
||||
slots: { default: 'status' },
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'creationTime',
|
||||
title: '创建时间',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
title: '操作',
|
||||
resizable: false,
|
||||
width: 'auto',
|
||||
},
|
||||
];
|
||||
|
||||
export const drawerSchema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
show: () => false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
fieldName: 'id',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'userName',
|
||||
label: '用户账号',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'InputPassword',
|
||||
fieldName: 'password',
|
||||
label: '用户密码',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'nick',
|
||||
label: '用户昵称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'TreeSelect',
|
||||
// 在drawer里更新 这里不需要默认的componentProps
|
||||
defaultValue: undefined,
|
||||
fieldName: 'deptId',
|
||||
label: '所属部门',
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'phone',
|
||||
label: '手机号码',
|
||||
defaultValue: undefined,
|
||||
rules: z
|
||||
.string()
|
||||
.regex(/^1[3-9]\d{9}$/, '请输入正确的手机号码')
|
||||
.optional()
|
||||
.or(z.literal('')),
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'email',
|
||||
defaultValue: undefined,
|
||||
label: '邮箱',
|
||||
/**
|
||||
* z.literal 是 Zod 中的一种类型,用于定义一个特定的字面量值。
|
||||
* 它可以用于确保输入的值与指定的字面量完全匹配。
|
||||
* 例如,你可以使用 z.literal 来确保某个字段的值只能是特定的字符串、数字、布尔值等。
|
||||
* 即空字符串也可通过校验
|
||||
*/
|
||||
rules: z.string().email('请输入正确的邮箱').optional().or(z.literal('')),
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
buttonStyle: 'solid',
|
||||
options: getDictOptions(DictEnum.SYS_USER_SEX),
|
||||
optionType: 'button',
|
||||
},
|
||||
defaultValue: '0',
|
||||
fieldName: 'sex',
|
||||
formItemClass: 'col-span-2 lg:col-span-1',
|
||||
label: '性别',
|
||||
},
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
buttonStyle: 'solid',
|
||||
options: [
|
||||
{ label: '启用', value: true },
|
||||
{ label: '禁用', value: false },
|
||||
],
|
||||
optionType: 'button',
|
||||
},
|
||||
defaultValue: true,
|
||||
fieldName: 'state',
|
||||
formItemClass: 'col-span-2 lg:col-span-1',
|
||||
label: '状态',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
disabled: true,
|
||||
getPopupContainer,
|
||||
mode: 'multiple',
|
||||
optionFilterProp: 'label',
|
||||
optionLabelProp: 'label',
|
||||
placeholder: '请先选择部门',
|
||||
},
|
||||
fieldName: 'postIds',
|
||||
label: '岗位',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
getPopupContainer,
|
||||
mode: 'multiple',
|
||||
optionFilterProp: 'title',
|
||||
optionLabelProp: 'title',
|
||||
},
|
||||
fieldName: 'roleIds',
|
||||
label: '角色',
|
||||
},
|
||||
{
|
||||
component: 'Textarea',
|
||||
fieldName: 'remark',
|
||||
formItemClass: 'items-start',
|
||||
label: '备注',
|
||||
},
|
||||
];
|
||||
131
Yi.Vben5.Vue3/apps/web-antd/src/views/system/user/dept-tree.vue
Normal file
131
Yi.Vben5.Vue3/apps/web-antd/src/views/system/user/dept-tree.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import type { DeptGetListOutputDto } from '#/api/system/user/model';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { SyncOutlined } from '@ant-design/icons-vue';
|
||||
import { Empty, InputSearch, Skeleton, Tree } from 'ant-design-vue';
|
||||
|
||||
import { getDeptTree } from '#/api/system/user';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
withDefaults(defineProps<{ showSearch?: boolean }>(), { showSearch: true });
|
||||
|
||||
const emit = defineEmits<{
|
||||
/**
|
||||
* 点击刷新按钮的事件
|
||||
*/
|
||||
reload: [];
|
||||
/**
|
||||
* 点击节点的事件
|
||||
*/
|
||||
select: [];
|
||||
}>();
|
||||
|
||||
const selectDeptId = defineModel('selectDeptId', {
|
||||
required: true,
|
||||
type: Array as PropType<string[]>,
|
||||
});
|
||||
|
||||
const searchValue = defineModel('searchValue', {
|
||||
type: String,
|
||||
default: '',
|
||||
});
|
||||
|
||||
/** 部门数据源 */
|
||||
const deptTreeArray = ref<DeptGetListOutputDto[]>([]);
|
||||
/** 骨架屏加载 */
|
||||
const showTreeSkeleton = ref<boolean>(true);
|
||||
|
||||
async function loadTree() {
|
||||
showTreeSkeleton.value = true;
|
||||
searchValue.value = '';
|
||||
selectDeptId.value = [];
|
||||
|
||||
const ret = await getDeptTree();
|
||||
deptTreeArray.value = ret;
|
||||
showTreeSkeleton.value = false;
|
||||
}
|
||||
|
||||
async function handleReload() {
|
||||
await loadTree();
|
||||
emit('reload');
|
||||
}
|
||||
|
||||
onMounted(loadTree);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$attrs.class">
|
||||
<Skeleton
|
||||
:loading="showTreeSkeleton"
|
||||
:paragraph="{ rows: 8 }"
|
||||
active
|
||||
class="p-[8px]"
|
||||
>
|
||||
<div
|
||||
class="bg-background flex h-full flex-col overflow-y-auto rounded-lg"
|
||||
>
|
||||
<!-- 固定在顶部 必须加上bg-background背景色 否则会产生'穿透'效果 -->
|
||||
<div
|
||||
v-if="showSearch"
|
||||
class="bg-background z-100 sticky left-0 top-0 p-[8px]"
|
||||
>
|
||||
<InputSearch
|
||||
v-model:value="searchValue"
|
||||
:placeholder="$t('pages.common.search')"
|
||||
size="small"
|
||||
allow-clear
|
||||
>
|
||||
<template #enterButton>
|
||||
<a-button @click="handleReload">
|
||||
<SyncOutlined class="text-primary" />
|
||||
</a-button>
|
||||
</template>
|
||||
</InputSearch>
|
||||
</div>
|
||||
<div class="h-full overflow-x-hidden px-[8px]">
|
||||
<Tree
|
||||
v-bind="$attrs"
|
||||
v-if="deptTreeArray.length > 0"
|
||||
v-model:selected-keys="selectDeptId"
|
||||
:class="$attrs.class"
|
||||
:field-names="{
|
||||
title: 'deptName',
|
||||
key: 'id',
|
||||
children: 'children',
|
||||
}"
|
||||
:show-line="{ showLeafIcon: false }"
|
||||
:tree-data="deptTreeArray as any"
|
||||
:virtual="false"
|
||||
default-expand-all
|
||||
@select="$emit('select')"
|
||||
>
|
||||
<template #title="{ deptName }">
|
||||
<span v-if="deptName.includes(searchValue)">
|
||||
{{ deptName.substring(0, deptName.indexOf(searchValue)) }}
|
||||
<span class="text-primary">{{ searchValue }}</span>
|
||||
{{
|
||||
deptName.substring(
|
||||
deptName.indexOf(searchValue) + searchValue.length,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span v-else>{{ deptName }}</span>
|
||||
</template>
|
||||
</Tree>
|
||||
<!-- 仅本人数据权限 可以考虑直接不显示 -->
|
||||
<div v-else class="mt-5">
|
||||
<Empty
|
||||
:image="Empty.PRESENTED_IMAGE_SIMPLE"
|
||||
description="无部门数据"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Skeleton>
|
||||
</div>
|
||||
</template>
|
||||
298
Yi.Vben5.Vue3/apps/web-antd/src/views/system/user/index.vue
Normal file
298
Yi.Vben5.Vue3/apps/web-antd/src/views/system/user/index.vue
Normal file
@@ -0,0 +1,298 @@
|
||||
<script setup lang="ts">
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { User } from '#/api/system/user/model';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
Dropdown,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Space,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
|
||||
import {
|
||||
userExport,
|
||||
userList,
|
||||
userRemove,
|
||||
userUpdate,
|
||||
} from '#/api/system/user';
|
||||
import { TableSwitch } from '#/components/table';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
|
||||
import { columns, querySchema } from './data';
|
||||
import DeptTree from './dept-tree.vue';
|
||||
import userDrawer from './user-drawer.vue';
|
||||
import userImportModal from './user-import-modal.vue';
|
||||
import userInfoModal from './user-info-modal.vue';
|
||||
import userResetPwdModal from './user-reset-pwd-modal.vue';
|
||||
|
||||
/**
|
||||
* 导入
|
||||
*/
|
||||
const [UserImpotModal, userImportModalApi] = useVbenModal({
|
||||
connectedComponent: userImportModal,
|
||||
});
|
||||
|
||||
function handleImport() {
|
||||
userImportModalApi.open();
|
||||
}
|
||||
|
||||
// 左边部门用
|
||||
const selectDeptId = ref<string[]>([]);
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
schema: querySchema(),
|
||||
commonConfig: {
|
||||
labelWidth: 80,
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
handleReset: async () => {
|
||||
selectDeptId.value = [];
|
||||
|
||||
const { formApi, reload } = tableApi;
|
||||
await formApi.resetForm();
|
||||
const formValues = formApi.form.values;
|
||||
formApi.setLatestSubmissionValues(formValues);
|
||||
await reload(formValues);
|
||||
},
|
||||
// 日期选择格式化
|
||||
fieldMappingTime: [
|
||||
[
|
||||
'creationTime',
|
||||
['startTime', 'endTime'],
|
||||
['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// 高亮
|
||||
highlight: true,
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
// 点击行选中
|
||||
trigger: 'default',
|
||||
checkMethod: ({ row }) => row?.id !== 1,
|
||||
},
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 部门树选择处理
|
||||
if (selectDeptId.value.length === 1) {
|
||||
formValues.deptId = selectDeptId.value[0];
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'deptId');
|
||||
}
|
||||
|
||||
return await userList({
|
||||
SkipCount: page.currentPage,
|
||||
MaxResultCount: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
headerCellConfig: {
|
||||
height: 44,
|
||||
},
|
||||
cellConfig: {
|
||||
height: 48,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
id: 'system-user-index',
|
||||
};
|
||||
// @ts-expect-error 类型实例化过深
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
const [UserDrawer, userDrawerApi] = useVbenDrawer({
|
||||
connectedComponent: userDrawer,
|
||||
});
|
||||
|
||||
function handleAdd() {
|
||||
userDrawerApi.setData({});
|
||||
userDrawerApi.open();
|
||||
}
|
||||
|
||||
function handleEdit(row: User) {
|
||||
userDrawerApi.setData({ id: row.id });
|
||||
userDrawerApi.open();
|
||||
}
|
||||
|
||||
async function handleDelete(row: User) {
|
||||
await userRemove([row.id]);
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
function handleMultiDelete() {
|
||||
const rows = tableApi.grid.getCheckboxRecords();
|
||||
const ids = rows.map((row: User) => row.id);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
okType: 'danger',
|
||||
content: `确认删除选中的${ids.length}条记录吗?`,
|
||||
onOk: async () => {
|
||||
await userRemove(ids);
|
||||
await tableApi.query();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleDownloadExcel() {
|
||||
commonDownloadExcel(userExport, '用户管理', tableApi.formApi.form.values, {
|
||||
fieldMappingTime: formOptions.fieldMappingTime,
|
||||
});
|
||||
}
|
||||
|
||||
const [UserInfoModal, userInfoModalApi] = useVbenModal({
|
||||
connectedComponent: userInfoModal,
|
||||
});
|
||||
function handleUserInfo(row: User) {
|
||||
userInfoModalApi.setData({ userId: row.id });
|
||||
userInfoModalApi.open();
|
||||
}
|
||||
|
||||
const [UserResetPwdModal, userResetPwdModalApi] = useVbenModal({
|
||||
connectedComponent: userResetPwdModal,
|
||||
});
|
||||
|
||||
function handleResetPwd(record: User) {
|
||||
userResetPwdModalApi.setData({ record });
|
||||
userResetPwdModalApi.open();
|
||||
}
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-[8px]">
|
||||
<DeptTree
|
||||
v-model:select-dept-id="selectDeptId"
|
||||
class="w-[260px]"
|
||||
@reload="() => tableApi.reload()"
|
||||
@select="() => tableApi.reload()"
|
||||
/>
|
||||
<BasicTable class="flex-1 overflow-hidden" table-title="用户列表">
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button
|
||||
v-access:code="['system:user:export']"
|
||||
@click="handleDownloadExcel"
|
||||
>
|
||||
{{ $t('pages.common.export') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
v-access:code="['system:user:import']"
|
||||
@click="handleImport"
|
||||
>
|
||||
{{ $t('pages.common.import') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
:disabled="!vxeCheckboxChecked(tableApi)"
|
||||
danger
|
||||
type="primary"
|
||||
v-access:code="['system:user:remove']"
|
||||
@click="handleMultiDelete"
|
||||
>
|
||||
{{ $t('pages.common.delete') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
v-access:code="['system:user:add']"
|
||||
@click="handleAdd"
|
||||
>
|
||||
{{ $t('pages.common.add') }}
|
||||
</a-button>
|
||||
</Space>
|
||||
</template>
|
||||
<template #avatar="{ row }">
|
||||
<!-- 可能要判断空字符串情况 所以没有使用?? -->
|
||||
<Avatar :src="row.icon || preferences.app.defaultAvatar" />
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<TableSwitch
|
||||
v-model:value="row.state"
|
||||
:api="() => userUpdate(row)"
|
||||
:disabled="
|
||||
row.id === '1' || !hasAccessByCodes(['system:user:edit'])
|
||||
"
|
||||
@reload="() => tableApi.query()"
|
||||
/>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<template v-if="row.id !== '1'">
|
||||
<Space>
|
||||
<ghost-button
|
||||
v-access:code="['system:user:edit']"
|
||||
@click.stop="handleEdit(row)"
|
||||
>
|
||||
{{ $t('pages.common.edit') }}
|
||||
</ghost-button>
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
placement="left"
|
||||
title="确认删除?"
|
||||
@confirm="handleDelete(row)"
|
||||
>
|
||||
<ghost-button
|
||||
danger
|
||||
v-access:code="['system:user:remove']"
|
||||
@click.stop=""
|
||||
>
|
||||
{{ $t('pages.common.delete') }}
|
||||
</ghost-button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
<Dropdown placement="bottomRight">
|
||||
<template #overlay>
|
||||
<Menu>
|
||||
<MenuItem key="1" @click="handleUserInfo(row)">
|
||||
用户信息
|
||||
</MenuItem>
|
||||
<span v-access:code="['system:user:resetPwd']">
|
||||
<MenuItem key="2" @click="handleResetPwd(row)">
|
||||
重置密码
|
||||
</MenuItem>
|
||||
</span>
|
||||
</Menu>
|
||||
</template>
|
||||
<a-button size="small" type="link">
|
||||
{{ $t('pages.common.more') }}
|
||||
</a-button>
|
||||
</Dropdown>
|
||||
</template>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
<UserImpotModal @reload="tableApi.query()" />
|
||||
<UserDrawer @reload="tableApi.query()" />
|
||||
<UserInfoModal />
|
||||
<UserResetPwdModal />
|
||||
</Page>
|
||||
</template>
|
||||
@@ -0,0 +1,348 @@
|
||||
<script setup lang="ts">
|
||||
import type { Role } from '#/api/system/user/model';
|
||||
|
||||
import { computed, h, onMounted, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import { addFullName, cloneDeep, getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { Tag } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { configInfoByKey } from '#/api/system/config';
|
||||
import { postOptionSelect } from '#/api/system/post';
|
||||
import { roleOptionSelect } from '#/api/system/role';
|
||||
import {
|
||||
findUserInfo,
|
||||
getDeptTree,
|
||||
userAdd,
|
||||
userUpdate,
|
||||
} from '#/api/system/user';
|
||||
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
|
||||
import { authScopeOptions } from '#/views/system/role/data';
|
||||
|
||||
import { drawerSchema } from './data';
|
||||
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
|
||||
const isUpdate = ref(false);
|
||||
const title = computed(() => {
|
||||
return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');
|
||||
});
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-2',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 80,
|
||||
},
|
||||
schema: drawerSchema(),
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
/**
|
||||
* 生成角色的自定义label
|
||||
* 也可以用option插槽来做
|
||||
* renderComponentContent: () => ({
|
||||
option: ({value, label, [disabled, key, title]}) => '',
|
||||
}),
|
||||
*/
|
||||
function genRoleOptionlabel(role: Role) {
|
||||
const found = authScopeOptions.find((item) => item.value === role.dataScope);
|
||||
if (!found) {
|
||||
return role.roleName;
|
||||
}
|
||||
return h('div', { class: 'flex items-center gap-[6px]' }, [
|
||||
h('span', null, role.roleName),
|
||||
h(Tag, { color: found.color }, () => found.label),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据部门ID加载岗位列表
|
||||
* @param deptId 部门ID
|
||||
*/
|
||||
async function setupPostOptions(deptId?: string) {
|
||||
if (!deptId) {
|
||||
// 没有选择部门时,显示提示
|
||||
formApi.updateSchema([
|
||||
{
|
||||
componentProps: {
|
||||
disabled: true,
|
||||
options: [],
|
||||
placeholder: '请先选择部门',
|
||||
},
|
||||
fieldName: 'postIds',
|
||||
},
|
||||
]);
|
||||
// 清空已选岗位
|
||||
formApi.setFieldValue('postIds', []);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const postListResp = await postOptionSelect(deptId);
|
||||
// 确保返回的是数组
|
||||
const postList = Array.isArray(postListResp) ? postListResp : [];
|
||||
const options = postList.map((item) => ({
|
||||
label: item.postName,
|
||||
value: item.id,
|
||||
}));
|
||||
const placeholder = options.length > 0 ? '请选择岗位' : '该部门暂无岗位';
|
||||
formApi.updateSchema([
|
||||
{
|
||||
componentProps: {
|
||||
disabled: options.length === 0,
|
||||
options,
|
||||
placeholder,
|
||||
},
|
||||
fieldName: 'postIds',
|
||||
},
|
||||
]);
|
||||
// 部门变化时清空已选岗位
|
||||
formApi.setFieldValue('postIds', []);
|
||||
} catch (error) {
|
||||
console.error('加载岗位信息失败:', error);
|
||||
formApi.updateSchema([
|
||||
{
|
||||
componentProps: {
|
||||
disabled: true,
|
||||
options: [],
|
||||
placeholder: '加载岗位失败',
|
||||
},
|
||||
fieldName: 'postIds',
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化部门选择
|
||||
*/
|
||||
async function setupDeptSelect() {
|
||||
try {
|
||||
// updateSchema
|
||||
const deptTree = await getDeptTree();
|
||||
// 确保返回的是数组
|
||||
const deptList = Array.isArray(deptTree) ? deptTree : [];
|
||||
// 选中后显示在输入框的值 即父节点 / 子节点
|
||||
addFullName(deptList, 'deptName', ' / ');
|
||||
formApi.updateSchema([
|
||||
{
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
fieldNames: {
|
||||
label: 'deptName',
|
||||
key: 'id',
|
||||
value: 'id',
|
||||
children: 'children',
|
||||
},
|
||||
getPopupContainer,
|
||||
placeholder: '请选择',
|
||||
showSearch: true,
|
||||
treeData: deptList,
|
||||
treeDefaultExpandAll: true,
|
||||
treeLine: { showLeafIcon: false },
|
||||
// 筛选的字段
|
||||
treeNodeFilterProp: 'deptName',
|
||||
// 选中后显示在输入框的值
|
||||
treeNodeLabelProp: 'fullName',
|
||||
// 部门选择变化时加载对应岗位
|
||||
onChange: (value: string) => {
|
||||
setupPostOptions(value);
|
||||
},
|
||||
},
|
||||
fieldName: 'deptId',
|
||||
},
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('加载部门树失败:', error);
|
||||
// 加载失败时设置空树
|
||||
formApi.updateSchema([
|
||||
{
|
||||
componentProps: {
|
||||
placeholder: '加载部门失败',
|
||||
treeData: [],
|
||||
},
|
||||
fieldName: 'deptId',
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const defaultPassword = ref('');
|
||||
onMounted(async () => {
|
||||
const password = await configInfoByKey('sys.user.initPassword');
|
||||
if (password) {
|
||||
defaultPassword.value = password;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 新增时候 从参数设置获取默认密码
|
||||
*/
|
||||
async function loadDefaultPassword(update: boolean) {
|
||||
if (!update && defaultPassword.value) {
|
||||
formApi.setFieldValue('password', defaultPassword.value);
|
||||
}
|
||||
}
|
||||
|
||||
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
|
||||
{
|
||||
initializedGetter: defaultFormValueGetter(formApi),
|
||||
currentGetter: defaultFormValueGetter(formApi),
|
||||
},
|
||||
);
|
||||
|
||||
const [BasicDrawer, drawerApi] = useVbenDrawer({
|
||||
onBeforeClose,
|
||||
onClosed: handleClosed,
|
||||
onConfirm: handleConfirm,
|
||||
async onOpenChange(isOpen) {
|
||||
if (!isOpen) {
|
||||
// 需要重置岗位选择
|
||||
formApi.updateSchema([
|
||||
{
|
||||
componentProps: {
|
||||
disabled: true,
|
||||
options: [],
|
||||
placeholder: '请先选择部门',
|
||||
},
|
||||
fieldName: 'postIds',
|
||||
},
|
||||
]);
|
||||
return null;
|
||||
}
|
||||
drawerApi.drawerLoading(true);
|
||||
|
||||
try {
|
||||
const { id } = drawerApi.getData() as { id?: number | string };
|
||||
isUpdate.value = !!id;
|
||||
/** update时 禁用用户名修改 不显示密码框 */
|
||||
formApi.updateSchema([
|
||||
{ componentProps: { disabled: isUpdate.value }, fieldName: 'userName' },
|
||||
{
|
||||
dependencies: { show: () => !isUpdate.value, triggerFields: ['id'] },
|
||||
fieldName: 'password',
|
||||
},
|
||||
]);
|
||||
|
||||
let user: any | null = null;
|
||||
|
||||
if (isUpdate.value && id) {
|
||||
// 编辑模式:从用户详情中获取用户信息(含岗位、角色ID)
|
||||
user = await findUserInfo(id);
|
||||
}
|
||||
|
||||
// 角色下拉统一使用 roleOptionSelect
|
||||
const roleListResp = await roleOptionSelect();
|
||||
const allRoles = Array.isArray(roleListResp) ? (roleListResp as Role[]) : [];
|
||||
|
||||
const userRoles = user?.roles ?? [];
|
||||
const posts = user?.posts ?? [];
|
||||
|
||||
const postIds = posts.map((item: any) => item.id);
|
||||
const roleIds = userRoles.map((item: any) => item.roleId ?? item.id);
|
||||
|
||||
const postOptions = posts.map((item: any) => ({
|
||||
label: item.postName,
|
||||
value: item.id,
|
||||
}));
|
||||
|
||||
formApi.updateSchema([
|
||||
{
|
||||
componentProps: {
|
||||
// title用于选中后回填到输入框 默认为label
|
||||
optionLabelProp: 'title',
|
||||
options: allRoles.map((item: any) => ({
|
||||
label: genRoleOptionlabel(item),
|
||||
// title用于选中后回填到输入框 默认为label
|
||||
title: item.roleName,
|
||||
value: item.roleId ?? item.id,
|
||||
})),
|
||||
},
|
||||
fieldName: 'roleIds',
|
||||
},
|
||||
]);
|
||||
|
||||
// 部门选择、初始密码
|
||||
const promises = [
|
||||
setupDeptSelect(),
|
||||
loadDefaultPassword(isUpdate.value),
|
||||
];
|
||||
|
||||
if (user) {
|
||||
// 编辑模式:使用用户已有的岗位数据
|
||||
formApi.updateSchema([
|
||||
{
|
||||
componentProps: {
|
||||
disabled: false,
|
||||
options: postOptions,
|
||||
placeholder: '请选择岗位',
|
||||
},
|
||||
fieldName: 'postIds',
|
||||
},
|
||||
]);
|
||||
// 处理用户数据,确保 phone 字段是字符串类型
|
||||
const userData = {
|
||||
...user,
|
||||
// 将数字类型的 phone 转换为字符串,null/undefined 转为空字符串
|
||||
phone: user.phone != null ? String(user.phone) : '',
|
||||
};
|
||||
promises.push(
|
||||
// 添加基础信息
|
||||
formApi.setValues(userData),
|
||||
// 添加角色和岗位
|
||||
formApi.setFieldValue('postIds', postIds),
|
||||
formApi.setFieldValue('roleIds', roleIds),
|
||||
);
|
||||
} else {
|
||||
// 新增模式:等待选择部门后再加载岗位
|
||||
await setupPostOptions();
|
||||
}
|
||||
|
||||
// 并行处理
|
||||
await Promise.all(promises);
|
||||
await markInitialized();
|
||||
} catch (error) {
|
||||
console.error('加载用户信息失败:', error);
|
||||
} finally {
|
||||
drawerApi.drawerLoading(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
async function handleConfirm() {
|
||||
try {
|
||||
drawerApi.lock(true);
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
const data = cloneDeep(await formApi.getValues());
|
||||
await (isUpdate.value ? userUpdate(data) : userAdd(data));
|
||||
resetInitialized();
|
||||
emit('reload');
|
||||
drawerApi.close();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
drawerApi.lock(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClosed() {
|
||||
formApi.resetForm();
|
||||
resetInitialized();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicDrawer :title="title" class="w-[600px]">
|
||||
<BasicForm />
|
||||
</BasicDrawer>
|
||||
</template>
|
||||
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import type { UploadFile } from 'ant-design-vue/es/upload/interface';
|
||||
|
||||
import { h, ref, unref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { ExcelIcon, InBoxIcon } from '@vben/icons';
|
||||
|
||||
import { Modal, Switch, Upload } from 'ant-design-vue';
|
||||
|
||||
import { downloadImportTemplate, userImportData } from '#/api/system/user';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
|
||||
const UploadDragger = Upload.Dragger;
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
onCancel: handleCancel,
|
||||
onConfirm: handleSubmit,
|
||||
});
|
||||
|
||||
const fileList = ref<UploadFile[]>([]);
|
||||
const checked = ref(false);
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
modalApi.modalLoading(true);
|
||||
if (fileList.value.length !== 1) {
|
||||
handleCancel();
|
||||
return;
|
||||
}
|
||||
const data = {
|
||||
file: fileList.value[0]!.originFileObj as Blob,
|
||||
updateSupport: unref(checked),
|
||||
};
|
||||
const { code, msg } = await userImportData(data);
|
||||
let modal = Modal.success;
|
||||
if (code === 200) {
|
||||
emit('reload');
|
||||
} else {
|
||||
modal = Modal.error;
|
||||
}
|
||||
handleCancel();
|
||||
modal({
|
||||
content: h('div', {
|
||||
class: 'max-h-[260px] overflow-y-auto',
|
||||
innerHTML: msg, // 后台已经处理xss问题
|
||||
}),
|
||||
title: '提示',
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
modalApi.close();
|
||||
} finally {
|
||||
modalApi.modalLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalApi.close();
|
||||
fileList.value = [];
|
||||
checked.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal
|
||||
:close-on-click-modal="false"
|
||||
:fullscreen-button="false"
|
||||
title="用户导入"
|
||||
>
|
||||
<!-- z-index不设置会遮挡模板下载loading -->
|
||||
<!-- 手动处理 而不是放入文件就上传 -->
|
||||
<UploadDragger
|
||||
v-model:file-list="fileList"
|
||||
:before-upload="() => false"
|
||||
:max-count="1"
|
||||
:show-upload-list="true"
|
||||
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
|
||||
>
|
||||
<p class="ant-upload-drag-icon flex items-center justify-center">
|
||||
<InBoxIcon class="text-primary size-[48px]" />
|
||||
</p>
|
||||
<p class="ant-upload-text">点击或者拖拽到此处上传文件</p>
|
||||
</UploadDragger>
|
||||
<div class="mt-2 flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>允许导入xlsx, xls文件</span>
|
||||
<a-button
|
||||
type="link"
|
||||
@click="commonDownloadExcel(downloadImportTemplate, '用户导入模板')"
|
||||
>
|
||||
<div class="flex items-center gap-[4px]">
|
||||
<ExcelIcon />
|
||||
<span>下载模板</span>
|
||||
</div>
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span :class="{ 'text-red-500': checked }">
|
||||
是否更新/覆盖已存在的用户数据
|
||||
</span>
|
||||
<Switch v-model:checked="checked" />
|
||||
</div>
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
@@ -0,0 +1,122 @@
|
||||
<script setup lang="ts">
|
||||
import type { User } from '#/api/system/user/model';
|
||||
|
||||
import { computed, shallowRef } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Avatar, Descriptions, DescriptionsItem, Tag } from 'ant-design-vue';
|
||||
|
||||
import { findUserInfo } from '#/api/system/user';
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
onOpenChange: handleOpenChange,
|
||||
onClosed() {
|
||||
currentUser.value = null;
|
||||
},
|
||||
});
|
||||
|
||||
const currentUser = shallowRef<null | User>(null);
|
||||
|
||||
async function handleOpenChange(open: boolean) {
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
modalApi.modalLoading(true);
|
||||
|
||||
const { userId } = modalApi.getData() as { userId: number | string };
|
||||
const response = await findUserInfo(userId);
|
||||
|
||||
// 新接口直接返回完整的用户数据,包含posts和roles数组
|
||||
currentUser.value = response as User;
|
||||
|
||||
modalApi.modalLoading(false);
|
||||
}
|
||||
|
||||
const sexLabel = computed(() => {
|
||||
if (!currentUser.value) {
|
||||
return '-';
|
||||
}
|
||||
const { sex } = currentUser.value;
|
||||
if (sex === 'Man') return '男';
|
||||
if (sex === 'Woman') return '女';
|
||||
return '-';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal :footer="false" :fullscreen-button="false" title="用户信息">
|
||||
<Descriptions v-if="currentUser" size="small" :column="1" bordered>
|
||||
<DescriptionsItem label="用户ID">
|
||||
{{ currentUser.id }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="头像">
|
||||
<Avatar v-if="currentUser.icon" :src="currentUser.icon" :size="48" />
|
||||
<span v-else>-</span>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="姓名">
|
||||
{{ currentUser.name || '-' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="昵称">
|
||||
{{ currentUser.nick || '-' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="用户名">
|
||||
{{ currentUser.userName || '-' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="年龄">
|
||||
{{ currentUser.age || '-' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="性别">
|
||||
{{ sexLabel }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="用户状态">
|
||||
<Tag :color="currentUser.state ? 'success' : 'error'">
|
||||
{{ currentUser.state ? '启用' : '禁用' }}
|
||||
</Tag>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="手机号">
|
||||
{{ currentUser.phone || '-' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="邮箱">
|
||||
{{ currentUser.email || '-' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="地址">
|
||||
{{ currentUser.address || '-' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="IP地址">
|
||||
{{ currentUser.ip || '-' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="个人简介">
|
||||
{{ currentUser.introduction || '-' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="岗位">
|
||||
<div
|
||||
v-if="currentUser.posts && currentUser.posts.length > 0"
|
||||
class="flex flex-wrap gap-0.5"
|
||||
>
|
||||
<Tag v-for="item in currentUser.posts" :key="item.postId">
|
||||
{{ item.postName }}
|
||||
</Tag>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="角色">
|
||||
<div
|
||||
v-if="currentUser.roles && currentUser.roles.length > 0"
|
||||
class="flex flex-wrap gap-0.5"
|
||||
>
|
||||
<Tag v-for="item in currentUser.roles" :key="item.roleId">
|
||||
{{ item.roleName }}
|
||||
</Tag>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="创建时间">
|
||||
{{ currentUser.creationTime }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="备注">
|
||||
{{ currentUser.remark || '-' }}
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</BasicModal>
|
||||
</template>
|
||||
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import type { ResetPwdParam, User } from '#/api/system/user/model';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal, z } from '@vben/common-ui';
|
||||
|
||||
import { Descriptions, DescriptionsItem } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { userResetPassword } from '#/api/system/user';
|
||||
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
onClosed: handleClosed,
|
||||
onConfirm: handleSubmit,
|
||||
onOpenChange: handleOpenChange,
|
||||
});
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
show: () => false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
fieldName: 'id',
|
||||
label: '用户ID',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'InputPassword',
|
||||
componentProps: {
|
||||
placeholder: '请输入新的密码, 密码长度为5 - 20',
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: '新的密码',
|
||||
rules: z
|
||||
.string()
|
||||
.min(5, { message: '密码长度为5 - 20' })
|
||||
.max(20, { message: '密码长度为5 - 20' }),
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
commonConfig: {
|
||||
labelWidth: 80,
|
||||
},
|
||||
});
|
||||
|
||||
const currentUser = ref<null | User>(null);
|
||||
async function handleOpenChange(open: boolean) {
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
modalApi.modalLoading(true);
|
||||
|
||||
const { record } = modalApi.getData() as { record: User };
|
||||
currentUser.value = record;
|
||||
await formApi.setValues({ id: record.id });
|
||||
|
||||
modalApi.modalLoading(false);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
modalApi.modalLoading(true);
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
const data = await formApi.getValues();
|
||||
await userResetPassword(data as ResetPwdParam);
|
||||
emit('reload');
|
||||
handleClosed();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
modalApi.modalLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClosed() {
|
||||
modalApi.close();
|
||||
await formApi.resetForm();
|
||||
currentUser.value = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal
|
||||
:close-on-click-modal="false"
|
||||
:fullscreen-button="false"
|
||||
title="重置密码"
|
||||
>
|
||||
<div class="flex flex-col gap-[12px]">
|
||||
<Descriptions v-if="currentUser" size="small" :column="1" bordered>
|
||||
<DescriptionsItem label="用户ID">
|
||||
{{ currentUser.id }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="用户名">
|
||||
{{ currentUser.userName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="昵称">
|
||||
{{ currentUser.nick }}
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
<BasicForm />
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
Reference in New Issue
Block a user