2026-03-26 20:18:20 +08:00
|
|
|
<script setup lang="tsx">
|
2026-04-15 09:35:54 +08:00
|
|
|
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
|
|
|
|
import type { TableInstance } from 'element-plus';
|
|
|
|
|
import { ElButton, ElPopconfirm, ElSwitch, ElTag } from 'element-plus';
|
2026-03-26 20:18:20 +08:00
|
|
|
import dayjs from 'dayjs';
|
2026-04-15 09:35:54 +08:00
|
|
|
import type { FlatResponseData } from '@sa/axios';
|
|
|
|
|
import { userGenderRecord } from '@/constants/business';
|
2026-03-26 20:18:20 +08:00
|
|
|
import {
|
|
|
|
|
fetchBatchDeleteUser,
|
|
|
|
|
fetchDeleteDept,
|
|
|
|
|
fetchDeleteUser,
|
|
|
|
|
fetchGetDeptList,
|
|
|
|
|
fetchGetPostSimpleList,
|
|
|
|
|
fetchGetRoleSimpleList,
|
|
|
|
|
fetchGetUser,
|
|
|
|
|
fetchGetUserPage,
|
|
|
|
|
fetchUpdateUser,
|
|
|
|
|
fetchUpdateUserStatus
|
|
|
|
|
} from '@/service/api';
|
2026-04-15 09:35:54 +08:00
|
|
|
import { useUIPaginatedTable } from '@/hooks/common/table';
|
2026-03-26 20:18:20 +08:00
|
|
|
import BusinessTableActionCell from '@/components/custom/business-table-action-cell';
|
2026-04-14 16:33:47 +08:00
|
|
|
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
2026-04-15 09:35:54 +08:00
|
|
|
import { $t } from '@/locales';
|
|
|
|
|
import { buildMenuTree } from '@/views/system/shared/menu-tree';
|
|
|
|
|
import UserManagementRelation from '@/views/system/user-management-relation/index.vue';
|
2026-03-26 20:18:20 +08:00
|
|
|
import UserOperateDialog from './modules/user-operate-dialog.vue';
|
|
|
|
|
import UserOrgLeaderDialog from './modules/user-org-leader-dialog.vue';
|
|
|
|
|
import UserOrgOperateDialog from './modules/user-org-operate-dialog.vue';
|
|
|
|
|
import UserOrgPanel from './modules/user-org-panel.vue';
|
|
|
|
|
import UserResignedDialog from './modules/user-resigned-dialog.vue';
|
|
|
|
|
import UserResetPasswordDialog from './modules/user-reset-password-dialog.vue';
|
|
|
|
|
import UserSearch from './modules/user-search.vue';
|
|
|
|
|
|
2026-04-15 09:35:54 +08:00
|
|
|
defineOptions({ name: 'UserManage' });
|
2026-03-26 20:18:20 +08:00
|
|
|
|
|
|
|
|
function getInitSearchParams(): Api.SystemManage.UserSearchParams {
|
|
|
|
|
return {
|
|
|
|
|
pageNo: 1,
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
username: undefined,
|
|
|
|
|
mobile: undefined,
|
|
|
|
|
status: undefined,
|
|
|
|
|
deptId: undefined,
|
|
|
|
|
roleId: undefined
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createEmptyUserPageResult(): Promise<FlatResponseData<any, Api.SystemManage.UserList>> {
|
|
|
|
|
return Promise.resolve({
|
|
|
|
|
data: {
|
|
|
|
|
list: [],
|
|
|
|
|
total: 0
|
|
|
|
|
},
|
|
|
|
|
error: null,
|
|
|
|
|
response: undefined
|
|
|
|
|
} as unknown as FlatResponseData<any, Api.SystemManage.UserList>);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function transformUserPage(
|
|
|
|
|
response: FlatResponseData<any, Api.SystemManage.UserList>,
|
|
|
|
|
pageNo: number,
|
|
|
|
|
pageSize: number
|
|
|
|
|
) {
|
|
|
|
|
if (!response.error) {
|
|
|
|
|
return {
|
|
|
|
|
data: response.data.list,
|
|
|
|
|
pageNum: pageNo,
|
|
|
|
|
pageSize,
|
|
|
|
|
total: response.data.total
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
data: [],
|
|
|
|
|
pageNum: pageNo,
|
|
|
|
|
pageSize,
|
|
|
|
|
total: 0
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatTime(value?: number | null) {
|
|
|
|
|
if (!value) {
|
|
|
|
|
return '--';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return dayjs(value).format('YYYY-MM-DD HH:mm:ss');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getNullableLabel(value?: string | null) {
|
|
|
|
|
return value?.trim() || '--';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type UserResignedState = 'active' | 'pending' | 'resigned';
|
|
|
|
|
|
|
|
|
|
function getUserResignedState(row: Api.SystemManage.User): UserResignedState {
|
|
|
|
|
if (!row.resignedAt) {
|
|
|
|
|
return 'active';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return row.resignedAt > Date.now() ? 'pending' : 'resigned';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getResignedActionConfig(row: Api.SystemManage.User) {
|
|
|
|
|
const state = getUserResignedState(row);
|
|
|
|
|
|
|
|
|
|
if (state === 'active') {
|
|
|
|
|
return {
|
|
|
|
|
label: $t('page.system.user.resignUser'),
|
|
|
|
|
buttonType: 'warning' as const
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (state === 'pending') {
|
|
|
|
|
return {
|
|
|
|
|
label: $t('page.system.user.adjustResignUser'),
|
|
|
|
|
buttonType: 'warning' as const
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
label: $t('page.system.user.restoreUser'),
|
|
|
|
|
buttonType: 'success' as const
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const searchParams = reactive(getInitSearchParams());
|
|
|
|
|
const deptLoading = ref(false);
|
|
|
|
|
const userTableRef = ref<TableInstance>();
|
|
|
|
|
const userCheckedRowKeys = ref<number[]>([]);
|
|
|
|
|
const statusLoadingIds = ref<number[]>([]);
|
|
|
|
|
const deptList = ref<Api.SystemManage.Dept[]>([]);
|
|
|
|
|
const currentDeptId = ref<number | null>(null);
|
|
|
|
|
const operateVisible = ref(false);
|
|
|
|
|
const operateType = ref<UI.TableOperateType>('add');
|
|
|
|
|
const editingUserId = ref<number | null>(null);
|
|
|
|
|
const postOptions = ref<Api.SystemManage.PostSimple[]>([]);
|
|
|
|
|
const roleOptions = ref<Api.SystemManage.RoleSimple[]>([]);
|
|
|
|
|
const resetPasswordVisible = ref(false);
|
|
|
|
|
const resetPasswordUserId = ref<number | null>(null);
|
|
|
|
|
const resetPasswordUsername = ref<string | null>(null);
|
|
|
|
|
const resignedVisible = ref(false);
|
|
|
|
|
const resignedUserId = ref<number | null>(null);
|
|
|
|
|
const resignedUsername = ref<string | null>(null);
|
|
|
|
|
const resignedAt = ref<number | null>(null);
|
|
|
|
|
const orgOperateVisible = ref(false);
|
|
|
|
|
const orgOperateType = ref<UI.TableOperateType>('add');
|
|
|
|
|
const editingDeptData = ref<Api.SystemManage.Dept | null>(null);
|
|
|
|
|
const orgParentId = ref<number | null>(0);
|
|
|
|
|
const orgLeaderVisible = ref(false);
|
|
|
|
|
const leaderDeptData = ref<Api.SystemManage.Dept | null>(null);
|
2026-04-14 16:33:47 +08:00
|
|
|
const userManagementRelationVisible = ref(false);
|
2026-03-26 20:18:20 +08:00
|
|
|
|
|
|
|
|
const deptTree = computed(() => buildMenuTree(deptList.value));
|
|
|
|
|
const currentDept = computed(() => deptList.value.find(item => item.id === currentDeptId.value) ?? null);
|
|
|
|
|
const deptCount = computed(() => deptList.value.length);
|
|
|
|
|
|
2026-04-15 09:35:54 +08:00
|
|
|
const { columns, columnChecks, data, loading, getDataByPage, mobilePagination } = useUIPaginatedTable<
|
2026-03-26 20:18:20 +08:00
|
|
|
FlatResponseData<any, Api.SystemManage.UserList>,
|
|
|
|
|
Api.SystemManage.User
|
|
|
|
|
>({
|
|
|
|
|
paginationProps: {
|
|
|
|
|
currentPage: searchParams.pageNo,
|
|
|
|
|
pageSize: searchParams.pageSize
|
|
|
|
|
},
|
|
|
|
|
api: () => {
|
|
|
|
|
if (!currentDeptId.value) {
|
|
|
|
|
return createEmptyUserPageResult();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fetchGetUserPage({
|
|
|
|
|
...searchParams,
|
|
|
|
|
deptId: currentDeptId.value
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
transform: response => transformUserPage(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
|
|
|
|
|
onPaginationParamsChange: params => {
|
|
|
|
|
searchParams.pageNo = params.currentPage ?? 1;
|
|
|
|
|
searchParams.pageSize = params.pageSize ?? 10;
|
|
|
|
|
},
|
|
|
|
|
columns: () => [
|
2026-04-15 09:35:54 +08:00
|
|
|
{ prop: 'selection', type: 'selection', width: 48 },
|
|
|
|
|
{ prop: 'index', type: 'index', label: $t('common.index'), width: 64 },
|
|
|
|
|
{ prop: 'username', label: $t('page.system.user.userName'), minWidth: 140, showOverflowTooltip: true },
|
2026-03-26 20:18:20 +08:00
|
|
|
{
|
|
|
|
|
prop: 'nickname',
|
|
|
|
|
label: $t('page.system.user.nickName'),
|
|
|
|
|
minWidth: 120,
|
|
|
|
|
formatter: row => getNullableLabel(row.nickname)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
prop: 'deptName',
|
|
|
|
|
label: $t('page.system.user.deptName'),
|
|
|
|
|
minWidth: 180,
|
|
|
|
|
showOverflowTooltip: true,
|
|
|
|
|
formatter: row => getNullableLabel(row.deptName)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
prop: 'positionName',
|
|
|
|
|
label: $t('page.system.user.positionName'),
|
|
|
|
|
minWidth: 140,
|
|
|
|
|
showOverflowTooltip: true,
|
|
|
|
|
formatter: row => getNullableLabel(row.positionName)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
prop: 'mobile',
|
|
|
|
|
label: $t('page.system.user.userPhone'),
|
|
|
|
|
width: 140,
|
|
|
|
|
formatter: row => getNullableLabel(row.mobile)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
prop: 'email',
|
|
|
|
|
label: $t('page.system.user.userEmail'),
|
|
|
|
|
minWidth: 180,
|
|
|
|
|
showOverflowTooltip: true,
|
|
|
|
|
formatter: row => getNullableLabel(row.email)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
prop: 'sex',
|
|
|
|
|
label: $t('page.system.user.userGender'),
|
|
|
|
|
width: 100,
|
|
|
|
|
align: 'center',
|
|
|
|
|
formatter: row => {
|
|
|
|
|
const value = row.sex ?? 0;
|
|
|
|
|
const tagMap: Record<Api.SystemManage.UserGender, UI.ThemeColor> = {
|
|
|
|
|
0: 'info',
|
|
|
|
|
1: 'primary',
|
|
|
|
|
2: 'danger'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return <ElTag type={tagMap[value]}>{$t(userGenderRecord[value])}</ElTag>;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
prop: 'status',
|
|
|
|
|
label: $t('page.system.user.userStatus'),
|
|
|
|
|
width: 110,
|
|
|
|
|
align: 'center',
|
|
|
|
|
formatter: row => (
|
|
|
|
|
<ElSwitch
|
|
|
|
|
modelValue={row.status === 0}
|
|
|
|
|
loading={statusLoadingIds.value.includes(row.id)}
|
|
|
|
|
inlinePrompt
|
|
|
|
|
activeText={$t('page.system.common.status.enable')}
|
|
|
|
|
inactiveText={$t('page.system.common.status.disable')}
|
|
|
|
|
onChange={value => handleToggleStatus(row, Boolean(value))}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
prop: 'resignedAt',
|
|
|
|
|
label: $t('page.system.user.resignedAt'),
|
|
|
|
|
minWidth: 170,
|
|
|
|
|
formatter: row => formatTime(row.resignedAt)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
prop: 'resignedState',
|
|
|
|
|
label: $t('page.system.user.resignedState'),
|
|
|
|
|
width: 110,
|
|
|
|
|
align: 'center',
|
|
|
|
|
formatter: row => {
|
|
|
|
|
const state = getUserResignedState(row);
|
|
|
|
|
const stateMap: Record<UserResignedState, { type: UI.ThemeColor; label: App.I18n.I18nKey }> = {
|
2026-04-15 09:35:54 +08:00
|
|
|
active: { type: 'success', label: 'page.system.user.resignedStateEnum.active' },
|
|
|
|
|
pending: { type: 'warning', label: 'page.system.user.resignedStateEnum.pending' },
|
|
|
|
|
resigned: { type: 'info', label: 'page.system.user.resignedStateEnum.resigned' }
|
2026-03-26 20:18:20 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return <ElTag type={stateMap[state].type}>{$t(stateMap[state].label)}</ElTag>;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
prop: 'loginDate',
|
|
|
|
|
label: $t('page.system.user.loginDate'),
|
|
|
|
|
minWidth: 170,
|
|
|
|
|
formatter: row => formatTime(row.loginDate)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
prop: 'createTime',
|
|
|
|
|
label: $t('page.system.user.createTime'),
|
|
|
|
|
minWidth: 170,
|
|
|
|
|
formatter: row => formatTime(row.createTime)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
prop: 'operate',
|
|
|
|
|
label: $t('common.operate'),
|
|
|
|
|
width: 210,
|
|
|
|
|
align: 'center',
|
|
|
|
|
fixed: 'right',
|
|
|
|
|
formatter: row => (
|
|
|
|
|
<BusinessTableActionCell
|
|
|
|
|
actions={[
|
|
|
|
|
{
|
|
|
|
|
key: 'edit',
|
|
|
|
|
label: $t('common.edit'),
|
|
|
|
|
buttonType: 'primary',
|
|
|
|
|
onClick: () => openEdit(row.id)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'reset-password',
|
|
|
|
|
label: $t('page.system.user.resetPassword'),
|
|
|
|
|
buttonType: 'warning',
|
|
|
|
|
onClick: () => openResetPassword(row)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'resigned',
|
|
|
|
|
label: getResignedActionConfig(row).label,
|
|
|
|
|
buttonType: getResignedActionConfig(row).buttonType,
|
|
|
|
|
onClick: () => handleResignedAction(row)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'delete',
|
|
|
|
|
label: $t('common.delete'),
|
|
|
|
|
buttonType: 'danger',
|
|
|
|
|
onClick: () => handleDeleteAction(row)
|
|
|
|
|
}
|
|
|
|
|
]}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function loadDeptTree() {
|
|
|
|
|
deptLoading.value = true;
|
|
|
|
|
|
2026-04-15 09:35:54 +08:00
|
|
|
const { error, data: deptItems } = await fetchGetDeptList({
|
2026-03-26 20:18:20 +08:00
|
|
|
status: 0
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
deptLoading.value = false;
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
deptList.value = [];
|
|
|
|
|
currentDeptId.value = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
deptList.value = deptItems;
|
|
|
|
|
|
|
|
|
|
if (!deptItems.length) {
|
|
|
|
|
currentDeptId.value = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const matched = deptItems.find(item => item.id === currentDeptId.value);
|
|
|
|
|
currentDeptId.value = matched?.id ?? deptItems[0].id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadFormOptions() {
|
|
|
|
|
const [postResult, roleResult] = await Promise.all([fetchGetPostSimpleList(), fetchGetRoleSimpleList()]);
|
|
|
|
|
|
|
|
|
|
if (!postResult.error) {
|
|
|
|
|
postOptions.value = postResult.data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!roleResult.error) {
|
|
|
|
|
roleOptions.value = roleResult.data.filter(item => item.status === 0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function reloadUserTable(page = searchParams.pageNo) {
|
|
|
|
|
userCheckedRowKeys.value = [];
|
|
|
|
|
await getDataByPage(page);
|
|
|
|
|
await nextTick();
|
|
|
|
|
userTableRef.value?.clearSelection();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleDeptSelect(nodeData: Api.SystemManage.Dept) {
|
|
|
|
|
currentDeptId.value = nodeData.id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleUserSelectionChange(rows: Api.SystemManage.User[]) {
|
|
|
|
|
userCheckedRowKeys.value = rows.map(item => item.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openAdd() {
|
|
|
|
|
operateType.value = 'add';
|
|
|
|
|
editingUserId.value = null;
|
|
|
|
|
operateVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openEdit(id: number) {
|
|
|
|
|
operateType.value = 'edit';
|
|
|
|
|
editingUserId.value = id;
|
|
|
|
|
operateVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openResetPassword(row: Api.SystemManage.User) {
|
|
|
|
|
resetPasswordUserId.value = row.id;
|
|
|
|
|
resetPasswordUsername.value = row.username;
|
|
|
|
|
resetPasswordVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openResignedDialog(row: Api.SystemManage.User) {
|
|
|
|
|
resignedUserId.value = row.id;
|
|
|
|
|
resignedUsername.value = row.username;
|
|
|
|
|
resignedAt.value = row.resignedAt ?? null;
|
|
|
|
|
resignedVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openAddRootOrg() {
|
|
|
|
|
orgOperateType.value = 'add';
|
|
|
|
|
editingDeptData.value = null;
|
|
|
|
|
orgParentId.value = 0;
|
|
|
|
|
orgOperateVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openAddChildOrg(row: Api.SystemManage.Dept) {
|
|
|
|
|
orgOperateType.value = 'add';
|
|
|
|
|
editingDeptData.value = null;
|
|
|
|
|
orgParentId.value = row.id;
|
|
|
|
|
orgOperateVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openEditOrg(row: Api.SystemManage.Dept) {
|
|
|
|
|
orgOperateType.value = 'edit';
|
|
|
|
|
editingDeptData.value = row;
|
|
|
|
|
orgParentId.value = row.parentId;
|
|
|
|
|
orgOperateVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openOrgLeader(row: Api.SystemManage.Dept) {
|
|
|
|
|
leaderDeptData.value = row;
|
|
|
|
|
orgLeaderVisible.value = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleDeleteDeptAction(row: Api.SystemManage.Dept) {
|
2026-04-15 09:35:54 +08:00
|
|
|
const { error } = await fetchDeleteDept(row.id);
|
2026-03-26 20:18:20 +08:00
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentDeptId.value === row.id) {
|
|
|
|
|
currentDeptId.value = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.$message?.success($t('common.deleteSuccess'));
|
|
|
|
|
await loadDeptTree();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleDeleteAction(row: Api.SystemManage.User) {
|
|
|
|
|
try {
|
|
|
|
|
await window.$messageBox?.confirm($t('common.confirmDelete'), $t('common.warning'), {
|
|
|
|
|
confirmButtonText: $t('common.confirm'),
|
|
|
|
|
cancelButtonText: $t('common.cancel'),
|
|
|
|
|
type: 'warning'
|
|
|
|
|
});
|
|
|
|
|
} catch {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 09:35:54 +08:00
|
|
|
const { error } = await fetchDeleteUser(row.id);
|
2026-03-26 20:18:20 +08:00
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.$message?.success($t('common.deleteSuccess'));
|
|
|
|
|
await reloadUserTable();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function updateUserResignedAt(userId: number, value: number | null) {
|
|
|
|
|
const detailResult = await fetchGetUser(userId);
|
|
|
|
|
|
|
|
|
|
if (detailResult.error) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const user = detailResult.data;
|
|
|
|
|
|
2026-04-15 09:35:54 +08:00
|
|
|
const { error } = await fetchUpdateUser({
|
2026-03-26 20:18:20 +08:00
|
|
|
id: userId,
|
|
|
|
|
username: user.username,
|
|
|
|
|
nickname: user.nickname ?? null,
|
|
|
|
|
remark: user.remark ?? null,
|
|
|
|
|
deptId: user.deptId,
|
|
|
|
|
positionId: user.positionId ?? null,
|
|
|
|
|
resignedAt: value,
|
|
|
|
|
email: user.email ?? null,
|
|
|
|
|
mobile: user.mobile ?? null,
|
|
|
|
|
sex: user.sex ?? 0,
|
|
|
|
|
avatar: user.avatar ?? null
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return !error;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleRestoreUser(row: Api.SystemManage.User) {
|
|
|
|
|
try {
|
|
|
|
|
await window.$messageBox?.confirm($t('common.confirmDelete'), $t('page.system.user.restoreUser'), {
|
|
|
|
|
confirmButtonText: $t('common.confirm'),
|
|
|
|
|
cancelButtonText: $t('common.cancel'),
|
|
|
|
|
type: 'warning'
|
|
|
|
|
});
|
|
|
|
|
} catch {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const success = await updateUserResignedAt(row.id, null);
|
|
|
|
|
|
|
|
|
|
if (!success) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.$message?.success($t('common.updateSuccess'));
|
|
|
|
|
await reloadUserTable();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleResignedAction(row: Api.SystemManage.User) {
|
|
|
|
|
if (getUserResignedState(row) === 'resigned') {
|
|
|
|
|
await handleRestoreUser(row);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
openResignedDialog(row);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleBatchDelete() {
|
|
|
|
|
if (!userCheckedRowKeys.value.length) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 09:35:54 +08:00
|
|
|
const { error } = await fetchBatchDeleteUser(userCheckedRowKeys.value);
|
2026-03-26 20:18:20 +08:00
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.$message?.success($t('common.deleteSuccess'));
|
|
|
|
|
await reloadUserTable();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleToggleStatus(row: Api.SystemManage.User, enabled: boolean) {
|
|
|
|
|
statusLoadingIds.value = [...statusLoadingIds.value, row.id];
|
|
|
|
|
|
2026-04-15 09:35:54 +08:00
|
|
|
const { error } = await fetchUpdateUserStatus({
|
2026-03-26 20:18:20 +08:00
|
|
|
id: row.id,
|
|
|
|
|
status: enabled ? 0 : 1
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
statusLoadingIds.value = statusLoadingIds.value.filter(item => item !== row.id);
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
row.status = enabled ? 0 : 1;
|
|
|
|
|
window.$message?.success($t('common.updateSuccess'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleSearch() {
|
|
|
|
|
await reloadUserTable(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleResetSearch() {
|
|
|
|
|
const pageSize = searchParams.pageSize;
|
|
|
|
|
|
|
|
|
|
Object.assign(searchParams, getInitSearchParams(), {
|
|
|
|
|
pageSize,
|
|
|
|
|
deptId: currentDeptId.value ?? undefined
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await reloadUserTable(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleSubmitted() {
|
|
|
|
|
operateVisible.value = false;
|
|
|
|
|
await reloadUserTable();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleResignedSubmitted() {
|
|
|
|
|
resignedVisible.value = false;
|
|
|
|
|
await reloadUserTable();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleDeptSubmitted(deptId: number) {
|
|
|
|
|
orgOperateVisible.value = false;
|
|
|
|
|
await loadDeptTree();
|
|
|
|
|
currentDeptId.value = deptId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
watch(currentDeptId, async (value, oldValue) => {
|
|
|
|
|
if (!value || value === oldValue) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pageSize = searchParams.pageSize;
|
|
|
|
|
|
|
|
|
|
Object.assign(searchParams, getInitSearchParams(), {
|
|
|
|
|
pageSize,
|
|
|
|
|
deptId: value
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await reloadUserTable(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
await Promise.all([loadDeptTree(), loadFormOptions()]);
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div
|
|
|
|
|
class="min-h-560px gap-16px overflow-hidden xl:grid xl:grid-cols-[320px_minmax(0,1fr)] lt-xl:flex lt-xl:flex-col lt-xl:overflow-auto"
|
|
|
|
|
>
|
|
|
|
|
<div class="flex-col-stretch gap-16px xl:min-h-0">
|
|
|
|
|
<UserOrgPanel
|
|
|
|
|
:loading="deptLoading"
|
|
|
|
|
:tree-data="deptTree"
|
|
|
|
|
:current-dept-id="currentDeptId"
|
|
|
|
|
:total="deptCount"
|
|
|
|
|
@add-root="openAddRootOrg"
|
|
|
|
|
@add-child="openAddChildOrg"
|
|
|
|
|
@leader="openOrgLeader"
|
|
|
|
|
@edit="openEditOrg"
|
|
|
|
|
@delete="handleDeleteDeptAction"
|
|
|
|
|
@refresh="loadDeptTree"
|
|
|
|
|
@select="handleDeptSelect"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="flex-col-stretch gap-16px xl:min-h-0">
|
|
|
|
|
<UserSearch
|
|
|
|
|
v-model:model="searchParams"
|
|
|
|
|
:role-options="roleOptions"
|
|
|
|
|
:disabled="!currentDept"
|
|
|
|
|
@reset="handleResetSearch"
|
|
|
|
|
@search="handleSearch"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<ElCard class="card-wrapper xl:flex-1-hidden" body-class="user-table-card-body">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="flex items-center justify-between gap-12px">
|
|
|
|
|
<div class="flex flex-wrap items-center gap-8px">
|
|
|
|
|
<p>{{ $t('page.system.user.title') }}</p>
|
|
|
|
|
<ElTag v-if="currentDept" type="primary" effect="light">
|
|
|
|
|
{{ currentDept.name }}
|
|
|
|
|
</ElTag>
|
|
|
|
|
<ElTag effect="plain">{{ mobilePagination.total || data.length }}</ElTag>
|
|
|
|
|
</div>
|
|
|
|
|
<TableHeaderOperation v-model:columns="columnChecks" :loading="loading" @refresh="reloadUserTable">
|
|
|
|
|
<template #default>
|
|
|
|
|
<ElButton plain type="primary" :disabled="!currentDept" @click="openAdd">
|
|
|
|
|
<template #icon>
|
2026-04-15 09:35:54 +08:00
|
|
|
<icon-ic-round-plus class="text-icon" />
|
2026-03-26 20:18:20 +08:00
|
|
|
</template>
|
|
|
|
|
{{ $t('common.add') }}
|
|
|
|
|
</ElButton>
|
2026-04-14 16:33:47 +08:00
|
|
|
<ElButton plain type="primary" :disabled="!currentDept" @click="userManagementRelationVisible = true">
|
|
|
|
|
<template #icon>
|
2026-04-15 09:35:54 +08:00
|
|
|
<icon-ic-round-plus class="text-icon" />
|
2026-04-14 16:33:47 +08:00
|
|
|
</template>
|
2026-04-15 20:58:00 +08:00
|
|
|
管理链路
|
2026-04-14 16:33:47 +08:00
|
|
|
</ElButton>
|
2026-03-26 20:18:20 +08:00
|
|
|
<ElPopconfirm :title="$t('common.confirmDelete')" @confirm="handleBatchDelete">
|
|
|
|
|
<template #reference>
|
|
|
|
|
<ElButton type="danger" plain :disabled="userCheckedRowKeys.length === 0">
|
|
|
|
|
<template #icon>
|
2026-04-15 09:35:54 +08:00
|
|
|
<icon-ic-round-delete class="text-icon" />
|
2026-03-26 20:18:20 +08:00
|
|
|
</template>
|
|
|
|
|
{{ $t('common.batchDelete') }}
|
|
|
|
|
</ElButton>
|
|
|
|
|
</template>
|
|
|
|
|
</ElPopconfirm>
|
|
|
|
|
</template>
|
|
|
|
|
</TableHeaderOperation>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<template v-if="currentDept">
|
|
|
|
|
<div class="flex-1">
|
|
|
|
|
<ElTable
|
|
|
|
|
ref="userTableRef"
|
|
|
|
|
v-loading="loading"
|
|
|
|
|
height="100%"
|
|
|
|
|
border
|
|
|
|
|
row-key="id"
|
|
|
|
|
:data="data"
|
|
|
|
|
@selection-change="handleUserSelectionChange"
|
|
|
|
|
>
|
2026-04-15 09:35:54 +08:00
|
|
|
<ElTableColumn v-for="col in columns" :key="String(col.prop)" v-bind="col" />
|
2026-03-26 20:18:20 +08:00
|
|
|
</ElTable>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="mt-20px flex justify-end">
|
|
|
|
|
<ElPagination
|
|
|
|
|
v-if="mobilePagination.total"
|
|
|
|
|
layout="total,prev,pager,next,sizes"
|
|
|
|
|
v-bind="mobilePagination"
|
|
|
|
|
@current-change="mobilePagination['current-change']"
|
|
|
|
|
@size-change="mobilePagination['size-change']"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<div v-else class="h-full flex items-center justify-center">
|
2026-04-15 09:35:54 +08:00
|
|
|
<ElEmpty :description="$t('page.system.user.emptyOrg')" />
|
2026-03-26 20:18:20 +08:00
|
|
|
</div>
|
|
|
|
|
</ElCard>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<UserOperateDialog
|
|
|
|
|
v-model:visible="operateVisible"
|
|
|
|
|
:operate-type="operateType"
|
|
|
|
|
:user-id="editingUserId"
|
|
|
|
|
:current-dept-id="currentDeptId"
|
|
|
|
|
:dept-tree="deptTree"
|
|
|
|
|
:post-options="postOptions"
|
|
|
|
|
:role-options="roleOptions"
|
|
|
|
|
@submitted="handleSubmitted"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<UserResetPasswordDialog
|
|
|
|
|
v-model:visible="resetPasswordVisible"
|
|
|
|
|
:user-id="resetPasswordUserId"
|
|
|
|
|
:username="resetPasswordUsername"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<UserResignedDialog
|
|
|
|
|
v-model:visible="resignedVisible"
|
|
|
|
|
:user-id="resignedUserId"
|
|
|
|
|
:username="resignedUsername"
|
|
|
|
|
:resigned-at="resignedAt"
|
|
|
|
|
@submitted="handleResignedSubmitted"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<UserOrgOperateDialog
|
|
|
|
|
v-model:visible="orgOperateVisible"
|
|
|
|
|
:operate-type="orgOperateType"
|
|
|
|
|
:row-data="editingDeptData"
|
|
|
|
|
:parent-id="orgParentId"
|
|
|
|
|
:dept-tree="deptTree"
|
|
|
|
|
@submitted="handleDeptSubmitted"
|
|
|
|
|
/>
|
|
|
|
|
|
2026-04-15 09:35:54 +08:00
|
|
|
<UserOrgLeaderDialog v-model:visible="orgLeaderVisible" :dept="leaderDeptData" />
|
2026-04-14 16:33:47 +08:00
|
|
|
|
|
|
|
|
<BusinessFormDialog
|
|
|
|
|
v-model="userManagementRelationVisible"
|
2026-04-15 20:58:00 +08:00
|
|
|
title="用户管理链路"
|
2026-04-14 16:33:47 +08:00
|
|
|
preset="lg"
|
|
|
|
|
:show-footer="false"
|
|
|
|
|
max-body-height="70vh"
|
|
|
|
|
>
|
2026-04-15 09:35:54 +08:00
|
|
|
<UserManagementRelation :from-user-index="true" :dept-id="currentDeptId" />
|
2026-04-14 16:33:47 +08:00
|
|
|
</BusinessFormDialog>
|
2026-03-26 20:18:20 +08:00
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
:deep(.user-table-card-body) {
|
|
|
|
|
height: calc(100% - 56px);
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
2026-04-14 16:33:47 +08:00
|
|
|
|
2026-04-15 20:58:00 +08:00
|
|
|
// 管理链路对话框内的搜索框样式优化
|
2026-04-14 16:33:47 +08:00
|
|
|
:deep(.business-form-dialog) {
|
|
|
|
|
width: 800px;
|
|
|
|
|
}
|
2026-03-26 20:18:20 +08:00
|
|
|
</style>
|