初始化

This commit is contained in:
2026-03-26 20:18:20 +08:00
commit 120a5b4dfd
368 changed files with 35926 additions and 0 deletions

View File

@@ -0,0 +1,737 @@
<script setup lang="tsx">
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
import type { TableInstance } from 'element-plus';
import { ElButton, ElPopconfirm, ElSwitch, ElTag } from 'element-plus';
import dayjs from 'dayjs';
import type { FlatResponseData } from '@sa/axios';
import { userGenderRecord } from '@/constants/business';
import {
fetchBatchDeleteUser,
fetchDeleteDept,
fetchDeleteUser,
fetchGetDeptList,
fetchGetPostSimpleList,
fetchGetRoleSimpleList,
fetchGetUser,
fetchGetUserPage,
fetchUpdateUser,
fetchUpdateUserStatus
} from '@/service/api';
import { useUIPaginatedTable } from '@/hooks/common/table';
import BusinessTableActionCell from '@/components/custom/business-table-action-cell';
import { $t } from '@/locales';
import { buildMenuTree } from '@/views/system/shared/menu-tree';
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';
defineOptions({ name: 'UserManage' });
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);
const deptTree = computed(() => buildMenuTree(deptList.value));
const currentDept = computed(() => deptList.value.find(item => item.id === currentDeptId.value) ?? null);
const deptCount = computed(() => deptList.value.length);
const { columns, columnChecks, data, loading, getDataByPage, mobilePagination } = useUIPaginatedTable<
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: () => [
{ 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 },
{
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 }> = {
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' }
};
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;
const { error, data: deptItems } = await fetchGetDeptList({
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) {
const { error } = await fetchDeleteDept(row.id);
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;
}
const { error } = await fetchDeleteUser(row.id);
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;
const { error } = await fetchUpdateUser({
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;
}
const { error } = await fetchBatchDeleteUser(userCheckedRowKeys.value);
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];
const { error } = await fetchUpdateUserStatus({
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>
<icon-ic-round-plus class="text-icon" />
</template>
{{ $t('common.add') }}
</ElButton>
<ElPopconfirm :title="$t('common.confirmDelete')" @confirm="handleBatchDelete">
<template #reference>
<ElButton type="danger" plain :disabled="userCheckedRowKeys.length === 0">
<template #icon>
<icon-ic-round-delete class="text-icon" />
</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"
>
<ElTableColumn v-for="col in columns" :key="String(col.prop)" v-bind="col" />
</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">
<ElEmpty :description="$t('page.system.user.emptyOrg')" />
</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"
/>
<UserOrgLeaderDialog v-model:visible="orgLeaderVisible" :dept="leaderDeptData" />
</div>
</template>
<style lang="scss" scoped>
:deep(.user-table-card-body) {
height: calc(100% - 56px);
display: flex;
flex-direction: column;
}
</style>

View File

@@ -0,0 +1,363 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import { userGenderOptions } from '@/constants/business';
import {
fetchAssignUserRoles,
fetchCreateUser,
fetchGetUser,
fetchGetUserRoleIds,
fetchUpdateUser
} from '@/service/api';
import { useForm, useFormRules } from '@/hooks/common/form';
import { translateOptions } from '@/utils/common';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import BusinessFormSection from '@/components/custom/business-form-section.vue';
import { $t } from '@/locales';
defineOptions({ name: 'UserOperateDialog' });
interface Props {
operateType: UI.TableOperateType;
userId?: number | null;
currentDeptId?: number | null;
deptTree: Api.SystemManage.Dept[];
postOptions: Api.SystemManage.PostSimple[];
roleOptions: Api.SystemManage.RoleSimple[];
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted', userId?: number): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate } = useForm();
const { createRequiredRule, patternRules } = useFormRules();
const loading = ref(false);
const submitting = ref(false);
const title = computed(() => {
const titles: Record<UI.TableOperateType, string> = {
add: $t('page.system.user.addUser'),
edit: $t('page.system.user.editUser')
};
return titles[props.operateType];
});
const isEdit = computed(() => props.operateType === 'edit');
type Model = Api.SystemManage.SaveUserParams & {
roleIds: number[];
};
const model = ref<Model>(createDefaultModel());
const genderOptions = computed(() =>
translateOptions(userGenderOptions).map(item => ({
...item,
value: Number(item.value) as Api.SystemManage.UserGender
}))
);
const deptTreeProps = {
value: 'id',
label: 'name',
children: 'children'
} as const;
function createDefaultModel(): Model {
return {
username: '',
nickname: '',
remark: '',
deptId: props.currentDeptId ?? 0,
positionId: null,
email: '',
mobile: '',
sex: 1,
avatar: '',
password: '',
roleIds: []
};
}
function getNullableText(value?: string | null) {
return value?.trim() || null;
}
const rules = computed(() => {
const passwordRules = isEdit.value
? []
: [createRequiredRule($t('page.system.user.form.password')), patternRules.pwd];
return {
username: [createRequiredRule($t('page.system.user.form.userName')), patternRules.userName],
deptId: [createRequiredRule($t('page.system.user.form.deptName'))],
positionId: [createRequiredRule($t('page.system.user.form.positionName'))],
mobile: getNullableText(model.value.mobile) ? [patternRules.phone] : [],
email: getNullableText(model.value.email) ? [patternRules.email] : [],
password: passwordRules
} satisfies Record<string, App.Global.FormRule[]>;
});
async function handleInitModel() {
model.value = createDefaultModel();
if (!isEdit.value || !props.userId) {
return;
}
loading.value = true;
const [userResult, roleResult] = await Promise.all([fetchGetUser(props.userId), fetchGetUserRoleIds(props.userId)]);
loading.value = false;
if (userResult.error) {
return;
}
const user = userResult.data;
model.value = {
username: user.username,
nickname: user.nickname ?? '',
remark: user.remark ?? '',
deptId: user.deptId,
positionId: user.positionId ?? null,
email: user.email ?? '',
mobile: user.mobile ?? '',
sex: user.sex ?? 0,
avatar: user.avatar ?? '',
password: '',
roleIds: roleResult.error ? [] : roleResult.data
};
}
function closeDialog() {
visible.value = false;
}
async function handleSubmit() {
await validate();
const payload: Api.SystemManage.SaveUserParams = {
username: model.value.username.trim(),
nickname: getNullableText(model.value.nickname),
remark: getNullableText(model.value.remark),
deptId: model.value.deptId,
positionId: model.value.positionId,
email: getNullableText(model.value.email),
mobile: getNullableText(model.value.mobile),
sex: model.value.sex,
avatar: getNullableText(model.value.avatar)
};
if (!isEdit.value) {
payload.password = String(model.value.password ?? '').trim();
}
submitting.value = true;
let userId = props.userId ?? undefined;
if (isEdit.value && props.userId) {
const result = await fetchUpdateUser({ id: props.userId, ...payload });
if (result.error) {
submitting.value = false;
return;
}
} else {
const result = await fetchCreateUser(payload);
if (result.error) {
submitting.value = false;
return;
}
userId = result.data;
}
if (userId !== undefined) {
const roleResult = await fetchAssignUserRoles({
userId,
roleIds: model.value.roleIds
});
if (roleResult.error) {
submitting.value = false;
return;
}
}
submitting.value = false;
window.$message?.success(isEdit.value ? $t('common.updateSuccess') : $t('common.addSuccess'));
closeDialog();
emit('submitted', userId);
}
watch(visible, async value => {
if (!value) {
return;
}
await handleInitModel();
await nextTick();
formRef.value?.clearValidate();
});
</script>
<template>
<BusinessFormDialog
v-model="visible"
:title="title"
preset="lg"
:loading="loading"
:confirm-loading="submitting"
max-body-height="70vh"
@confirm="handleSubmit"
>
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top" autocomplete="off">
<input class="business-form-autofill-guard" type="text" name="fake-username" autocomplete="username" />
<input class="business-form-autofill-guard" type="password" name="fake-password" autocomplete="new-password" />
<BusinessFormSection :title="$t('page.system.user.sections.basicInfo')">
<ElRow :gutter="16">
<ElCol :span="12">
<ElFormItem :label="$t('page.system.user.userName')" prop="username">
<ElInput
v-model="model.username"
name="system-user-username"
autocomplete="off"
:placeholder="$t('page.system.user.form.userName')"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem :label="$t('page.system.user.nickName')" prop="nickname">
<ElInput
v-model="model.nickname"
name="system-user-nickname"
autocomplete="off"
:placeholder="$t('page.system.user.form.nickName')"
/>
</ElFormItem>
</ElCol>
<ElCol v-if="!isEdit" :span="12">
<ElFormItem :label="$t('page.system.user.password')" prop="password">
<ElInput
v-model="model.password"
name="system-user-password"
show-password
autocomplete="new-password"
:placeholder="$t('page.system.user.form.password')"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem :label="$t('page.system.user.userGender')" prop="sex">
<ElSelect v-model="model.sex" :placeholder="$t('page.system.user.form.userGender')">
<ElOption v-for="{ label, value } in genderOptions" :key="value" :label="label" :value="value" />
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem :label="$t('page.system.user.userPhone')" prop="mobile">
<ElInput
v-model="model.mobile"
name="system-user-mobile"
autocomplete="off"
:placeholder="$t('page.system.user.form.userPhone')"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem :label="$t('page.system.user.userEmail')" prop="email">
<ElInput
v-model="model.email"
name="system-user-email"
autocomplete="off"
:placeholder="$t('page.system.user.form.userEmail')"
/>
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem :label="$t('page.system.user.remark')" prop="remark">
<ElInput
v-model="model.remark"
type="textarea"
:rows="3"
:placeholder="$t('page.system.user.form.remark')"
/>
</ElFormItem>
</ElCol>
</ElRow>
</BusinessFormSection>
<BusinessFormSection :title="$t('page.system.user.sections.organizationInfo')">
<ElRow :gutter="16">
<ElCol :span="12">
<ElFormItem :label="$t('page.system.user.deptName')" prop="deptId">
<ElTreeSelect
v-model="model.deptId"
check-strictly
clearable
default-expand-all
:data="deptTree"
:props="deptTreeProps"
:render-after-expand="false"
:placeholder="$t('page.system.user.form.deptName')"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem :label="$t('page.system.user.positionName')" prop="positionId">
<ElSelect v-model="model.positionId" clearable :placeholder="$t('page.system.user.form.positionName')">
<ElOption v-for="item in postOptions" :key="item.id" :label="item.name" :value="item.id" />
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem :label="$t('page.system.user.userRole')" prop="roleIds">
<ElSelect
v-model="model.roleIds"
multiple
collapse-tags
collapse-tags-tooltip
:placeholder="$t('page.system.user.form.userRole')"
>
<ElOption v-for="item in roleOptions" :key="item.id" :label="item.name" :value="item.id" />
</ElSelect>
</ElFormItem>
</ElCol>
</ElRow>
</BusinessFormSection>
</ElForm>
</BusinessFormDialog>
</template>
<style scoped>
.business-form-autofill-guard {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
opacity: 0;
pointer-events: none;
}
</style>

View File

@@ -0,0 +1,264 @@
<script setup lang="tsx">
import { computed, ref, watch } from 'vue';
import { ElButton, ElEmpty, ElTag } from 'element-plus';
import dayjs from 'dayjs';
import {
fetchDeleteOrgLeaderRelation,
fetchGetOrgLeaderCandidateUsers,
fetchGetOrgLeaderListByDept,
fetchGetUserPage
} from '@/service/api';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import BusinessTableActionCell from '@/components/custom/business-table-action-cell';
import { $t } from '@/locales';
import UserOrgLeaderOperateDialog from './user-org-leader-operate-dialog.vue';
defineOptions({ name: 'UserOrgLeaderDialog' });
interface Props {
dept?: Api.SystemManage.Dept | null;
}
const props = defineProps<Props>();
const visible = defineModel<boolean>('visible', {
default: false
});
const loading = ref(false);
const relations = ref<Api.SystemManage.OrgLeaderRelation[]>([]);
const candidateUsers = ref<Api.SystemManage.OrgLeaderCandidateUser[]>([]);
const operateVisible = ref(false);
const operateType = ref<UI.TableOperateType>('add');
const editingData = ref<Api.SystemManage.OrgLeaderRelation | null>(null);
const title = computed(() => {
if (props.dept?.name) {
return `${$t('page.system.user.orgLeaderTitle')} / ${props.dept.name}`;
}
return $t('page.system.user.orgLeaderTitle');
});
const total = computed(() => relations.value.length);
function formatTime(value?: number | null) {
if (!value) {
return '--';
}
return dayjs(value).format('YYYY-MM-DD HH:mm:ss');
}
function isCandidateUser(item: unknown): item is Api.SystemManage.OrgLeaderCandidateUser {
if (!item || typeof item !== 'object') {
return false;
}
const record = item as Record<string, unknown>;
return typeof record.id === 'number' && typeof record.nickname === 'string' && typeof record.deptId === 'number';
}
function mapUsersToCandidateUsers(users: Api.SystemManage.User[]): Api.SystemManage.OrgLeaderCandidateUser[] {
const now = Date.now();
return users
.filter(item => !item.resignedAt || item.resignedAt > now)
.map(item => ({
id: item.id,
nickname: item.nickname?.trim() || item.username,
deptId: item.deptId,
deptName: item.deptName ?? null
}));
}
async function loadCandidateUsers(deptId: number) {
const candidateResult = await fetchGetOrgLeaderCandidateUsers(deptId);
if (!candidateResult.error && Array.isArray(candidateResult.data) && candidateResult.data.every(isCandidateUser)) {
return candidateResult.data;
}
const userResult = await fetchGetUserPage({
pageNo: 1,
pageSize: 200,
deptId,
status: 0
});
if (userResult.error) {
return [];
}
return mapUsersToCandidateUsers(userResult.data.list);
}
async function loadData() {
if (!props.dept?.id) {
relations.value = [];
candidateUsers.value = [];
return;
}
loading.value = true;
const [relationResult, candidates] = await Promise.all([
fetchGetOrgLeaderListByDept(props.dept.id),
loadCandidateUsers(props.dept.id)
]);
loading.value = false;
relations.value = relationResult.error ? [] : relationResult.data;
candidateUsers.value = candidates;
}
function openAdd() {
operateType.value = 'add';
editingData.value = null;
operateVisible.value = true;
}
function openEdit(row: Api.SystemManage.OrgLeaderRelation) {
operateType.value = 'edit';
editingData.value = row;
operateVisible.value = true;
}
async function handleDelete(row: Api.SystemManage.OrgLeaderRelation) {
const { error } = await fetchDeleteOrgLeaderRelation(row.id);
if (error) {
return;
}
window.$message?.success($t('common.deleteSuccess'));
await loadData();
}
async function handleDeleteAction(row: Api.SystemManage.OrgLeaderRelation) {
try {
await window.$messageBox?.confirm($t('common.confirmDelete'), $t('common.warning'), {
confirmButtonText: $t('common.confirm'),
cancelButtonText: $t('common.cancel'),
type: 'warning'
});
} catch {
return;
}
await handleDelete(row);
}
async function handleSubmitted() {
operateVisible.value = false;
await loadData();
}
watch(
() => [visible.value, props.dept?.id] as const,
async ([dialogVisible, deptId]) => {
if (!dialogVisible || !deptId) {
return;
}
await loadData();
}
);
</script>
<template>
<BusinessFormDialog v-model="visible" :title="title" preset="lg" :show-footer="false">
<div class="flex-col-stretch gap-16px">
<div class="flex items-center justify-between gap-12px">
<div class="flex items-center gap-8px">
<ElTag type="primary" effect="light">{{ dept?.name || $t('common.noData') }}</ElTag>
<ElTag effect="plain">{{ total }}</ElTag>
</div>
<div class="flex items-center gap-8px">
<ElButton type="primary" plain size="small" @click="openAdd">
<template #icon>
<icon-ic-round-plus class="text-icon" />
</template>
{{ $t('common.add') }}
</ElButton>
<ElButton plain size="small" @click="loadData">
<template #icon>
<icon-mdi-refresh class="text-icon" />
</template>
{{ $t('common.refresh') }}
</ElButton>
</div>
</div>
<div v-loading="loading" class="min-h-260px">
<template v-if="relations.length">
<div class="min-h-260px">
<ElTable border :data="relations" :max-height="420">
<ElTableColumn type="index" :label="$t('common.index')" width="64" />
<ElTableColumn prop="userNickname" :label="$t('page.system.user.orgLeader')" min-width="140" />
<ElTableColumn
prop="effectiveFrom"
:label="$t('page.system.user.effectiveFrom')"
min-width="170"
:formatter="row => formatTime(row.effectiveFrom)"
/>
<ElTableColumn
prop="effectiveUntil"
:label="$t('page.system.user.effectiveUntil')"
min-width="170"
:formatter="row => formatTime(row.effectiveUntil)"
/>
<ElTableColumn
prop="remark"
:label="$t('page.system.user.relationRemark')"
min-width="180"
show-overflow-tooltip
/>
<ElTableColumn
prop="createTime"
:label="$t('page.system.user.createTime')"
min-width="170"
:formatter="row => formatTime(row.createTime)"
/>
<ElTableColumn :label="$t('common.operate')" width="196" align="center" fixed="right">
<template #default="{ row }">
<BusinessTableActionCell
:actions="[
{
key: 'edit',
label: $t('common.edit'),
buttonType: 'primary',
onClick: () => openEdit(row)
},
{
key: 'delete',
label: $t('common.delete'),
buttonType: 'danger',
onClick: () => handleDeleteAction(row)
}
]"
/>
</template>
</ElTableColumn>
</ElTable>
</div>
</template>
<div v-else class="min-h-260px flex items-center justify-center">
<ElEmpty :description="$t('page.system.user.emptyLeader')" />
</div>
</div>
</div>
<UserOrgLeaderOperateDialog
v-model:visible="operateVisible"
:operate-type="operateType"
:row-data="editingData"
:dept="dept"
:candidate-users="candidateUsers"
@submitted="handleSubmitted"
/>
</BusinessFormDialog>
</template>

View File

@@ -0,0 +1,224 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import dayjs from 'dayjs';
import { fetchCreateOrgLeaderRelation, fetchUpdateOrgLeaderRelation } from '@/service/api';
import { useForm, useFormRules } from '@/hooks/common/form';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import BusinessFormSection from '@/components/custom/business-form-section.vue';
import { $t } from '@/locales';
defineOptions({ name: 'UserOrgLeaderOperateDialog' });
interface Props {
operateType: UI.TableOperateType;
rowData?: Api.SystemManage.OrgLeaderRelation | null;
dept?: Api.SystemManage.Dept | null;
candidateUsers: Api.SystemManage.OrgLeaderCandidateUser[];
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate } = useForm();
const { createRequiredRule } = useFormRules();
const submitting = ref(false);
const isEdit = computed(() => props.operateType === 'edit');
const title = computed(() => {
const titleMap: Record<UI.TableOperateType, string> = {
add: $t('page.system.user.addLeader'),
edit: $t('page.system.user.editLeader')
};
return titleMap[props.operateType];
});
type Model = {
userId: number | null;
effectiveFrom: Date | null;
effectiveUntil: Date | null;
remark: string;
};
const model = ref<Model>(createDefaultModel());
const candidateOptions = computed(() => {
const options = [...props.candidateUsers];
const rowData = props.rowData;
if (isEdit.value && rowData && !options.some(item => item.id === rowData.userId)) {
options.unshift({
id: rowData.userId,
nickname: rowData.userNickname,
deptId: rowData.deptId,
deptName: props.dept?.name ?? null
});
}
return options;
});
const rules = {
userId: createRequiredRule($t('page.system.user.form.candidateUser'))
} satisfies Partial<Record<keyof Model, App.Global.FormRule>>;
function createCurrentTime() {
return dayjs().second(0).millisecond(0).toDate();
}
function createDefaultModel(): Model {
return {
userId: null,
effectiveFrom: createCurrentTime(),
effectiveUntil: null,
remark: ''
};
}
function initModel() {
model.value = createDefaultModel();
if (isEdit.value && props.rowData) {
model.value = {
userId: props.rowData.userId,
effectiveFrom: props.rowData.effectiveFrom ? dayjs(props.rowData.effectiveFrom).toDate() : createCurrentTime(),
effectiveUntil: props.rowData.effectiveUntil ? dayjs(props.rowData.effectiveUntil).toDate() : null,
remark: props.rowData.remark ?? ''
};
}
}
function closeDialog() {
visible.value = false;
}
async function handleSubmit() {
if (!props.dept?.id) {
return;
}
await validate();
const effectiveFrom = model.value.effectiveFrom ? dayjs(model.value.effectiveFrom).valueOf() : null;
const effectiveUntil = model.value.effectiveUntil ? dayjs(model.value.effectiveUntil).valueOf() : null;
if (effectiveFrom && effectiveUntil && effectiveFrom > effectiveUntil) {
window.$message?.warning($t('common.pleaseCheckValue'));
return;
}
const payload: Api.SystemManage.SaveOrgLeaderRelationParams = {
deptId: props.dept.id,
userId: Number(model.value.userId),
effectiveFrom,
effectiveUntil,
remark: model.value.remark.trim() || null
};
submitting.value = true;
const request =
isEdit.value && props.rowData
? fetchUpdateOrgLeaderRelation({ id: props.rowData.id, ...payload })
: fetchCreateOrgLeaderRelation(payload);
const { error } = await request;
submitting.value = false;
if (error) {
return;
}
window.$message?.success($t(isEdit.value ? 'common.updateSuccess' : 'common.addSuccess'));
closeDialog();
emit('submitted');
}
watch(visible, async value => {
if (!value) {
return;
}
initModel();
await nextTick();
formRef.value?.clearValidate();
});
</script>
<template>
<BusinessFormDialog
v-model="visible"
:title="title"
preset="md"
:confirm-loading="submitting"
:scrollbar="false"
@confirm="handleSubmit"
>
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
<BusinessFormSection :title="$t('page.system.user.sections.basicInfo')">
<ElRow :gutter="16">
<ElCol :span="12">
<ElFormItem :label="$t('page.system.user.deptName')">
<ElInput :model-value="dept?.name || $t('common.noData')" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem :label="$t('page.system.user.candidateUser')" prop="userId">
<ElSelect
v-model="model.userId"
class="w-full"
filterable
:placeholder="$t('page.system.user.form.candidateUser')"
>
<ElOption v-for="item in candidateOptions" :key="item.id" :label="item.nickname" :value="item.id" />
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem :label="$t('page.system.user.effectiveFrom')" prop="effectiveFrom">
<ElDatePicker
v-model="model.effectiveFrom"
class="w-full"
type="datetime"
clearable
:placeholder="$t('page.system.user.form.effectiveFrom')"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem :label="$t('page.system.user.effectiveUntil')" prop="effectiveUntil">
<ElDatePicker
v-model="model.effectiveUntil"
class="w-full"
type="datetime"
clearable
:placeholder="$t('page.system.user.form.effectiveUntil')"
/>
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem :label="$t('page.system.user.relationRemark')" prop="remark">
<ElInput
v-model="model.remark"
type="textarea"
:rows="4"
:placeholder="$t('page.system.user.form.relationRemark')"
/>
</ElFormItem>
</ElCol>
</ElRow>
</BusinessFormSection>
</ElForm>
</BusinessFormDialog>
</template>

View File

@@ -0,0 +1,250 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import { commonStatusOptions } from '@/constants/business';
import { fetchCreateDept, fetchUpdateDept } from '@/service/api';
import { useForm, useFormRules } from '@/hooks/common/form';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import { $t } from '@/locales';
defineOptions({ name: 'UserOrgOperateDialog' });
interface Props {
operateType: UI.TableOperateType;
rowData?: Api.SystemManage.Dept | null;
parentId?: number | null;
deptTree: Api.SystemManage.Dept[];
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted', deptId: number): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate } = useForm();
const { createRequiredRule } = useFormRules();
const submitting = ref(false);
const isEdit = computed(() => props.operateType === 'edit');
const title = computed(() => {
if (isEdit.value) {
return $t('page.system.user.editOrg');
}
return props.parentId && props.parentId > 0 ? $t('page.system.user.addChildOrg') : $t('page.system.user.addOrg');
});
const orgTypeOptions: CommonType.Option<Api.SystemManage.DeptOrgType, App.I18n.I18nKey>[] = [
{ value: 'company', label: 'page.system.user.orgType.company' },
{ value: 'dept', label: 'page.system.user.orgType.dept' },
{ value: 'direction', label: 'page.system.user.orgType.direction' },
{ value: 'team', label: 'page.system.user.orgType.team' }
];
type Model = Api.SystemManage.SaveDeptParams;
const model = ref<Model>(createDefaultModel());
const treeProps = {
value: 'id',
label: 'name',
children: 'children'
} as const;
const parentTree = computed(() => {
const filteredTree = filterDeptTree(props.deptTree, props.rowData?.id ?? null);
return [
{
id: 0,
name: $t('page.system.user.topLevelOrg'),
parentId: -1,
orgType: 'company' as const,
status: 0,
children: filteredTree
}
];
});
const expandParentTree = computed(() => !isEdit.value && (props.parentId ?? 0) === 0);
const rules = {
name: createRequiredRule($t('page.system.user.form.orgName')),
parentId: createRequiredRule($t('page.system.user.form.parentOrg')),
orgType: createRequiredRule($t('page.system.user.form.orgTypeLabel')),
sort: createRequiredRule($t('page.system.user.form.orgSort')),
status: createRequiredRule($t('page.system.user.form.userStatus'))
} satisfies Partial<Record<keyof Model, App.Global.FormRule>>;
function createDefaultModel(): Model {
return {
name: '',
parentId: props.parentId ?? 0,
orgType: 'dept',
code: '',
sort: 0,
status: 0
};
}
function filterDeptTree(tree: Api.SystemManage.Dept[], excludedId: number | null): Api.SystemManage.Dept[] {
if (!excludedId) {
return tree;
}
return tree
.filter(item => item.id !== excludedId)
.map(item => ({
...item,
children: item.children ? filterDeptTree(item.children, excludedId) : item.children
}));
}
function initModel() {
if (isEdit.value && props.rowData) {
model.value = {
name: props.rowData.name,
parentId: props.rowData.parentId,
orgType: props.rowData.orgType,
code: props.rowData.code ?? '',
sort: props.rowData.sort ?? 0,
status: props.rowData.status
};
return;
}
model.value = createDefaultModel();
}
function closeDialog() {
visible.value = false;
}
async function handleSubmit() {
await validate();
submitting.value = true;
const payload: Api.SystemManage.SaveDeptParams = {
name: model.value.name.trim(),
parentId: model.value.parentId,
orgType: model.value.orgType,
code: String(model.value.code ?? '').trim() || null,
sort: model.value.sort,
status: model.value.status
} as Api.SystemManage.SaveDeptParams;
if (isEdit.value && props.rowData) {
const { error } = await fetchUpdateDept({
id: props.rowData.id,
...payload
});
submitting.value = false;
if (error) {
return;
}
window.$message?.success($t('common.updateSuccess'));
closeDialog();
emit('submitted', props.rowData.id);
return;
}
const { error, data } = await fetchCreateDept(payload);
submitting.value = false;
if (error) {
return;
}
window.$message?.success($t('common.addSuccess'));
closeDialog();
emit('submitted', Number(data));
}
watch(visible, async value => {
if (!value) {
return;
}
initModel();
await nextTick();
formRef.value?.clearValidate();
});
</script>
<template>
<BusinessFormDialog
v-model="visible"
:title="title"
preset="md"
:confirm-loading="submitting"
:scrollbar="false"
@confirm="handleSubmit"
>
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
<ElRow :gutter="16">
<ElCol :span="24">
<ElFormItem :label="$t('page.system.user.orgName')" prop="name">
<ElInput v-model="model.name" :placeholder="$t('page.system.user.form.orgName')" />
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem :label="$t('page.system.user.deptName')" prop="parentId">
<ElTreeSelect
v-model="model.parentId"
check-strictly
:data="parentTree"
:default-expand-all="expandParentTree"
:props="treeProps"
:render-after-expand="false"
:placeholder="$t('page.system.user.form.parentOrg')"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem :label="$t('page.system.user.orgTypeLabel')" prop="orgType">
<ElSelect v-model="model.orgType" :placeholder="$t('page.system.user.form.orgTypeLabel')">
<ElOption v-for="item in orgTypeOptions" :key="item.value" :label="$t(item.label)" :value="item.value" />
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem :label="$t('page.system.user.orgCode')" prop="code">
<ElInput v-model="model.code" :placeholder="$t('page.system.user.form.orgCode')" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem :label="$t('page.system.user.orgSort')" prop="sort">
<ElInputNumber
v-model="model.sort"
class="w-full"
:min="0"
:placeholder="$t('page.system.user.form.orgSort')"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem :label="$t('page.system.user.userStatus')" prop="status">
<ElRadioGroup v-model="model.status" class="business-form-radio-group">
<ElRadio v-for="item in commonStatusOptions" :key="item.value" :value="item.value">
{{ $t(item.label) }}
</ElRadio>
</ElRadioGroup>
</ElFormItem>
</ElCol>
</ElRow>
</ElForm>
</BusinessFormDialog>
</template>

View File

@@ -0,0 +1,163 @@
<script setup lang="ts">
import { markRaw, ref, watch } from 'vue';
import type { TreeInstance } from 'element-plus';
import { $t } from '@/locales';
import IconMdiAccountGroup from '~icons/mdi/account-group';
import IconMdiDomain from '~icons/mdi/domain';
import IconMdiOfficeBuilding from '~icons/mdi/office-building';
import IconMdiSourceBranch from '~icons/mdi/source-branch';
defineOptions({ name: 'UserOrgPanel' });
interface Props {
loading: boolean;
treeData: Api.SystemManage.Dept[];
currentDeptId?: number | null;
total: number;
}
defineProps<Props>();
interface Emits {
(e: 'refresh'): void;
(e: 'select', dept: Api.SystemManage.Dept): void;
(e: 'addRoot'): void;
(e: 'addChild', dept: Api.SystemManage.Dept): void;
(e: 'leader', dept: Api.SystemManage.Dept): void;
(e: 'edit', dept: Api.SystemManage.Dept): void;
(e: 'delete', dept: Api.SystemManage.Dept): void;
}
const emit = defineEmits<Emits>();
const keyword = ref('');
const treeRef = ref<TreeInstance | null>(null);
function filterNode(value: string, nodeData: Api.SystemManage.Dept) {
if (!value.trim()) {
return true;
}
return nodeData.name.toLowerCase().includes(value.trim().toLowerCase());
}
function getOrgIcon(orgType: Api.SystemManage.DeptOrgType) {
const iconMap: Record<Api.SystemManage.DeptOrgType, object> = {
company: markRaw(IconMdiDomain),
dept: markRaw(IconMdiOfficeBuilding),
direction: markRaw(IconMdiSourceBranch),
team: markRaw(IconMdiAccountGroup)
};
return iconMap[orgType];
}
watch(keyword, value => {
treeRef.value?.filter(value);
});
</script>
<template>
<ElCard class="card-wrapper xl:flex-1-hidden" body-class="user-org-card-body">
<template #header>
<div class="flex items-center justify-between gap-12px">
<div class="flex items-center gap-8px">
<p>{{ $t('page.system.user.orgTitle') }}</p>
<ElTag effect="plain">{{ total }}</ElTag>
</div>
<div class="flex items-center gap-8px">
<ElButton type="primary" plain size="small" @click="emit('addRoot')">
<template #icon>
<icon-ic-round-plus class="text-icon" />
</template>
{{ $t('common.add') }}
</ElButton>
<ElButton plain size="small" @click="emit('refresh')">
<template #icon>
<icon-mdi-refresh class="text-icon" />
</template>
{{ $t('common.refresh') }}
</ElButton>
</div>
</div>
</template>
<div class="mb-12px">
<ElInput v-model="keyword" clearable :placeholder="$t('page.system.user.orgFilterPlaceholder')">
<template #prefix>
<icon-ic-round-search class="text-icon" />
</template>
</ElInput>
</div>
<ElScrollbar v-loading="loading" class="flex-1">
<ElTree
ref="treeRef"
node-key="id"
highlight-current
:current-node-key="currentDeptId ?? undefined"
:data="treeData"
default-expand-all
:expand-on-click-node="false"
:props="{ label: 'name', children: 'children' }"
:filter-node-method="filterNode as any"
@node-click="emit('select', $event)"
>
<template #default="{ data: nodeData }">
<div class="group min-w-0 flex flex-1 items-center gap-8px pr-8px">
<component :is="getOrgIcon(nodeData.orgType)" class="shrink-0 text-16px text-primary" />
<span class="min-w-0 flex-1 truncate text-14px">{{ nodeData.name }}</span>
<div class="flex items-center opacity-0 transition-opacity duration-200 group-hover:opacity-100">
<ElTooltip :content="$t('page.system.user.orgLeader')">
<ElButton link type="primary" class="user-org-action-btn" @click.stop="emit('leader', nodeData)">
<icon-mdi-account-tie-outline class="text-14px" />
</ElButton>
</ElTooltip>
<ElTooltip :content="$t('common.add')">
<ElButton link type="primary" class="user-org-action-btn" @click.stop="emit('addChild', nodeData)">
<icon-mdi-plus class="text-14px" />
</ElButton>
</ElTooltip>
<ElTooltip :content="$t('common.edit')">
<ElButton link type="primary" class="user-org-action-btn" @click.stop="emit('edit', nodeData)">
<icon-mdi-pencil-outline class="text-14px" />
</ElButton>
</ElTooltip>
<ElPopconfirm :title="$t('common.confirmDelete')" @confirm="emit('delete', nodeData)">
<template #reference>
<span class="inline-flex" @click.stop>
<ElTooltip :content="$t('common.delete')">
<ElButton link type="danger" class="user-org-action-btn">
<icon-mdi-delete-outline class="text-14px" />
</ElButton>
</ElTooltip>
</span>
</template>
</ElPopconfirm>
</div>
</div>
</template>
</ElTree>
</ElScrollbar>
</ElCard>
</template>
<style scoped lang="scss">
:deep(.user-org-card-body) {
height: calc(100% - 56px);
display: flex;
flex-direction: column;
}
:deep(.user-org-action-btn) {
padding: 2px;
min-width: auto;
height: auto;
margin-left: 2px;
line-height: 1;
}
:deep(.user-org-action-btn:first-child) {
margin-left: 0;
}
</style>

View File

@@ -0,0 +1,127 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import { REG_PWD } from '@/constants/reg';
import { fetchUpdateUserPassword } from '@/service/api';
import { useForm, useFormRules } from '@/hooks/common/form';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import { $t } from '@/locales';
defineOptions({ name: 'UserResetPasswordDialog' });
interface Props {
userId?: number | null;
username?: string | null;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate } = useForm();
const { createRequiredRule, createConfirmPwdRule } = useFormRules();
const submitting = ref(false);
const title = computed(() => $t('page.system.user.resetPassword'));
const displayUsername = computed(() => props.username?.trim() || $t('common.noData'));
const model = ref({
password: '',
confirmPassword: ''
});
const rules = computed<Record<'password' | 'confirmPassword', App.Global.FormRule[]>>(() => ({
password: [
createRequiredRule($t('page.system.user.form.newPassword')),
{ pattern: REG_PWD, message: $t('form.pwd.invalid') }
],
confirmPassword: createConfirmPwdRule(model.value.password)
}));
function initModel() {
model.value.password = '';
model.value.confirmPassword = '';
}
function closeDialog() {
visible.value = false;
}
async function handleSubmit() {
if (!props.userId) {
return;
}
await validate();
submitting.value = true;
const { error } = await fetchUpdateUserPassword({
id: props.userId,
password: model.value.password.trim()
});
submitting.value = false;
if (error) {
return;
}
window.$message?.success($t('common.updateSuccess'));
closeDialog();
emit('submitted');
}
watch(visible, async value => {
if (!value) {
return;
}
initModel();
await nextTick();
formRef.value?.clearValidate();
});
</script>
<template>
<BusinessFormDialog
v-model="visible"
:title="title"
preset="sm"
:confirm-loading="submitting"
:scrollbar="false"
@confirm="handleSubmit"
>
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
<ElRow :gutter="16">
<ElCol :span="24">
<ElFormItem :label="$t('page.system.user.userName')">
<ElInput :model-value="displayUsername" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem :label="$t('page.system.user.newPassword')" prop="password">
<ElInput v-model="model.password" show-password :placeholder="$t('page.system.user.form.newPassword')" />
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem :label="$t('page.system.user.confirmPassword')" prop="confirmPassword">
<ElInput
v-model="model.confirmPassword"
show-password
:placeholder="$t('page.system.user.form.confirmPassword')"
/>
</ElFormItem>
</ElCol>
</ElRow>
</ElForm>
</BusinessFormDialog>
</template>

View File

@@ -0,0 +1,140 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import dayjs from 'dayjs';
import { fetchGetUser, fetchUpdateUser } from '@/service/api';
import { useForm } from '@/hooks/common/form';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import { $t } from '@/locales';
defineOptions({ name: 'UserResignedDialog' });
interface Props {
userId?: number | null;
username?: string | null;
resignedAt?: number | null;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef } = useForm();
const submitting = ref(false);
const title = computed(() => {
if (props.resignedAt && props.resignedAt > Date.now()) {
return $t('page.system.user.adjustResignUser');
}
return $t('page.system.user.resignUser');
});
const displayUsername = computed(() => props.username?.trim() || $t('common.noData'));
const model = ref({
resignedAt: null as Date | null
});
function createCurrentTime() {
return dayjs().second(0).millisecond(0).toDate();
}
function initModel() {
model.value.resignedAt = props.resignedAt ? dayjs(props.resignedAt).toDate() : createCurrentTime();
}
function closeDialog() {
visible.value = false;
}
async function handleSubmit() {
if (!props.userId) {
return;
}
submitting.value = true;
const detailResult = await fetchGetUser(props.userId);
if (detailResult.error) {
submitting.value = false;
return;
}
const user = detailResult.data;
const { error } = await fetchUpdateUser({
id: props.userId,
username: user.username,
nickname: user.nickname ?? null,
remark: user.remark ?? null,
deptId: user.deptId,
positionId: user.positionId ?? null,
resignedAt: model.value.resignedAt ? dayjs(model.value.resignedAt).valueOf() : null,
email: user.email ?? null,
mobile: user.mobile ?? null,
sex: user.sex ?? 0,
avatar: user.avatar ?? null
});
submitting.value = false;
if (error) {
return;
}
window.$message?.success($t('common.updateSuccess'));
closeDialog();
emit('submitted');
}
watch(visible, async value => {
if (!value) {
return;
}
initModel();
await nextTick();
formRef.value?.clearValidate();
});
</script>
<template>
<BusinessFormDialog
v-model="visible"
:title="title"
preset="sm"
:confirm-loading="submitting"
:scrollbar="false"
@confirm="handleSubmit"
>
<ElForm ref="formRef" :model="model" label-position="top">
<ElRow :gutter="16">
<ElCol :span="12">
<ElFormItem :label="$t('page.system.user.userName')">
<ElInput :model-value="displayUsername" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem :label="$t('page.system.user.resignedAt')">
<ElDatePicker
v-model="model.resignedAt"
class="w-full"
type="datetime"
clearable
:placeholder="$t('page.system.user.form.resignedAt')"
/>
</ElFormItem>
</ElCol>
</ElRow>
</ElForm>
</BusinessFormDialog>
</template>

View File

@@ -0,0 +1,91 @@
<script setup lang="ts">
import { commonStatusOptions } from '@/constants/business';
import { translateOptions } from '@/utils/common';
import TableSearchPanel from '@/components/custom/table-search-panel.vue';
import { $t } from '@/locales';
defineOptions({ name: 'UserSearch' });
interface Props {
roleOptions: Api.SystemManage.RoleSimple[];
disabled?: boolean;
}
withDefaults(defineProps<Props>(), {
disabled: false
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
defineEmits<Emits>();
const model = defineModel<Api.SystemManage.UserSearchParams>('model', { required: true });
</script>
<template>
<TableSearchPanel
:model="model"
:disabled="disabled"
:action-col-lg="8"
:action-col-md="8"
@reset="$emit('reset')"
@search="$emit('search')"
>
<ElCol :lg="8" :md="8" :sm="12">
<ElFormItem :label="$t('page.system.user.userName')" prop="username">
<ElInput
v-model="model.username"
clearable
:disabled="disabled"
:placeholder="$t('page.system.user.form.userName')"
/>
</ElFormItem>
</ElCol>
<ElCol :lg="8" :md="8" :sm="12">
<ElFormItem :label="$t('page.system.user.userPhone')" prop="mobile">
<ElInput
v-model="model.mobile"
clearable
:disabled="disabled"
:placeholder="$t('page.system.user.form.userPhone')"
/>
</ElFormItem>
</ElCol>
<ElCol :lg="8" :md="8" :sm="12">
<ElFormItem :label="$t('page.system.user.userStatus')" prop="status">
<ElSelect
v-model="model.status"
clearable
:disabled="disabled"
:placeholder="$t('page.system.user.form.userStatus')"
>
<ElOption
v-for="{ label, value } in translateOptions(commonStatusOptions)"
:key="value"
:label="label"
:value="value"
/>
</ElSelect>
</ElFormItem>
</ElCol>
<template #extra>
<ElCol :lg="8" :md="8" :sm="12">
<ElFormItem :label="$t('page.system.user.userRole')" prop="roleId">
<ElSelect
v-model="model.roleId"
clearable
filterable
:disabled="disabled"
:placeholder="$t('page.system.user.form.userRole')"
>
<ElOption v-for="item in roleOptions" :key="item.id" :label="item.name" :value="item.id" />
</ElSelect>
</ElFormItem>
</ElCol>
</template>
</TableSearchPanel>
</template>