refactor: 个人中心页面重构和布局代码优化

This commit is contained in:
Ray.Hao
2026-06-21 14:09:27 +08:00
parent 7ffcf8820e
commit 845d117f08
68 changed files with 4026 additions and 3418 deletions

View File

@@ -69,19 +69,32 @@ onBeforeMount(() => {
<style lang="scss" scoped>
.dept-card {
height: 100%;
background: transparent;
border: 0;
border-radius: inherit;
box-shadow: none;
:deep(.el-card__body) {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
padding: 12px;
}
}
.dept-card__search {
margin-bottom: 12px;
margin-bottom: 10px;
}
.dept-card__tree {
flex: 1;
flex: 1 1 auto;
min-height: 0;
overflow: auto;
:deep(.el-tree-node__content) {
height: 32px;
}
}
</style>

View File

@@ -1,11 +1,11 @@
<!-- 用户管理 -->
<template>
<div class="page-container user-page">
<aside class="user-aside" :class="{ 'is-collapsed': sidebarCollapsed }">
<div class="user-aside__inner">
<div class="page-container page-container--split user-page">
<aside class="page-aside" :class="{ 'is-collapsed': sidebarCollapsed }">
<div class="page-aside__inner">
<UserDeptTree v-model="params.deptId" @node-click="handleQuery" />
</div>
<button class="user-aside__toggle" @click="sidebarCollapsed = !sidebarCollapsed">
<button class="page-aside__toggle" @click="sidebarCollapsed = !sidebarCollapsed">
<el-icon :size="14">
<ArrowLeft v-if="!sidebarCollapsed" />
<ArrowRight v-else />
@@ -13,15 +13,15 @@
</button>
</aside>
<div class="user-main">
<div class="page-main">
<el-card class="page-search" shadow="never">
<el-form ref="queryFormRef" :model="params" :inline="true" label-width="auto">
<el-form-item label="关键字" prop="keywords">
<el-input
v-model="params.keywords"
class="user-search__keyword"
placeholder="用户名/昵称/手机号"
clearable
style="width: 180px"
@keyup.enter="handleQuery"
/>
</el-form-item>
@@ -179,7 +179,7 @@
type="primary"
size="small"
link
@click="handleResetPassword(scope.row as UserItem)"
@click="openResetPasswordDialog(scope.row as UserItem)"
>
重置密码
</el-button>
@@ -275,6 +275,53 @@
</template>
</el-drawer>
<!-- 重置密码 -->
<el-dialog
v-model="resetPasswordDialog.visible"
title="重置密码"
:width="resetPasswordDialogWidth"
append-to-body
@closed="resetResetPasswordForm"
>
<div class="mb-16px">
用户:{{ resetPasswordDialog.nickname || resetPasswordDialog.username || "-" }}
<span v-if="resetPasswordDialog.nickname && resetPasswordDialog.username">
{{ resetPasswordDialog.username }}
</span>
</div>
<el-form
ref="resetPasswordFormRef"
:model="resetPasswordForm"
:rules="resetPasswordRules"
label-width="84px"
>
<el-form-item label="新密码" prop="password">
<el-input
v-model="resetPasswordForm.password"
type="password"
show-password
autocomplete="new-password"
placeholder="请输入新密码"
@keyup.enter="handleResetPasswordSubmit"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button
type="primary"
:loading="resetPasswordSubmitting"
@click="handleResetPasswordSubmit"
>
确 定
</el-button>
<el-button @click="closeResetPasswordDialog">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 用户导入 -->
<UserImportDialog v-model="importDialogVisible" @import-success="handleQuery()" />
</div>
@@ -309,6 +356,7 @@ const { toggle: toggleFullscreen } = useFullscreen(tableWrapperRef);
const queryFormRef = ref<FormInstance>();
const userFormRef = ref<FormInstance>();
const resetPasswordFormRef = ref<FormInstance>();
const sidebarCollapsed = ref(false);
/** 分页表格数据管理 */
@@ -333,6 +381,7 @@ const dialogState = reactive({
});
const importDialogVisible = ref(false);
const resetPasswordSubmitting = ref(false);
const initialFormData: UserForm = {
status: CommonStatus.ENABLED,
@@ -340,11 +389,30 @@ const initialFormData: UserForm = {
const formData = reactive<UserForm>({ ...initialFormData });
type ResetPasswordForm = {
password: string;
};
const resetPasswordDialog = reactive({
visible: false,
userId: "",
username: "",
nickname: "",
});
const resetPasswordForm = reactive<ResetPasswordForm>({
password: "",
});
const deptOptions = ref<OptionItem[]>([]);
const roleOptions = ref<OptionItem[]>([]);
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "600px" : "90%"));
const resetPasswordDialogWidth = computed(() =>
appStore.device === DeviceEnum.DESKTOP ? "420px" : "90%"
);
const rules: FormRules<UserForm> = {
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
nickname: [{ required: true, message: "请输入用户昵称", trigger: "blur" }],
@@ -354,6 +422,13 @@ const rules: FormRules<UserForm> = {
mobile: [{ pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号码", trigger: "blur" }],
};
const resetPasswordRules: FormRules<ResetPasswordForm> = {
password: [
{ required: true, message: "请输入新密码", trigger: "blur" },
{ min: 6, message: "密码至少需要6位字符", trigger: "blur" },
],
};
/**
* 取昵称/用户名首字母作为头像占位文本。
*
@@ -513,170 +588,66 @@ function openImportDialog(): void {
}
/**
* 重置用户密码
* 打开重置密码弹窗
*
* @param row 用户行数据
*/
async function handleResetPassword(row: UserItem): Promise<void> {
try {
const { value } = await ElMessageBox.prompt(
`请输入用户【${row.username}】的新密码`,
"重置密码",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
inputPattern: /.{6,}/,
inputErrorMessage: "密码至少需要6位字符",
}
);
await UserAPI.resetPassword(row.id, value);
ElMessage.success("密码重置成功");
} catch {
// 用户取消,无需处理
}
function openResetPasswordDialog(row: UserItem): void {
resetPasswordDialog.userId = row.id;
resetPasswordDialog.username = row.username ?? "";
resetPasswordDialog.nickname = row.nickname ?? "";
resetPasswordDialog.visible = true;
nextTick(() => {
resetPasswordFormRef.value?.clearValidate();
});
}
/**
* 关闭重置密码弹窗。
*/
function closeResetPasswordDialog(): void {
resetPasswordDialog.visible = false;
}
/**
* 重置密码表单状态。
*/
function resetResetPasswordForm(): void {
resetPasswordFormRef.value?.resetFields();
resetPasswordFormRef.value?.clearValidate();
resetPasswordForm.password = "";
resetPasswordDialog.userId = "";
resetPasswordDialog.username = "";
resetPasswordDialog.nickname = "";
}
/**
* 提交重置密码。
*/
const handleResetPasswordSubmit = useDebounceFn(async () => {
const valid = await resetPasswordFormRef.value?.validate().then(
() => true,
() => false
);
if (!valid || !resetPasswordDialog.userId) return;
resetPasswordSubmitting.value = true;
try {
await UserAPI.resetPassword(resetPasswordDialog.userId, resetPasswordForm.password);
ElMessage.success("密码重置成功");
closeResetPasswordDialog();
} finally {
resetPasswordSubmitting.value = false;
}
}, 300);
onMounted(() => {
handleQuery();
});
</script>
<style lang="scss" scoped>
.user-page {
display: flex;
flex-direction: row;
gap: 10px;
min-height: 0;
overflow: hidden;
--page-aside-width: 224px;
}
.user-aside {
position: relative;
display: flex;
flex: 0 0 var(--page-aside-width);
flex-direction: column;
width: var(--page-aside-width);
min-width: 0;
min-height: 0;
overflow: visible;
background: var(--content-bg);
border: 1px solid var(--card-border);
border-radius: var(--card-radius);
box-shadow: var(--card-shadow);
transition:
flex-basis 0.2s,
width 0.2s,
margin-right 0.2s,
border-color 0.2s;
&.is-collapsed {
flex-basis: 0;
width: 0;
margin-right: -10px;
border-color: transparent;
box-shadow: none;
.user-aside__inner {
pointer-events: none;
opacity: 0;
}
.user-aside__toggle {
right: -18px;
border-left: 0;
border-radius: 0 6px 6px 0;
}
}
}
.user-aside__toggle {
position: absolute;
top: 50%;
right: -10px;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 48px;
padding: 0;
margin: 0;
font: inherit;
color: var(--el-text-color-secondary);
cursor: pointer;
background: var(--content-bg);
border: 1px solid var(--card-border);
border-radius: 10px;
box-shadow: 0 4px 12px rgb(15 23 42 / 8%);
transform: translateY(-50%);
transition:
color 0.15s,
right 0.2s;
&:hover {
color: var(--el-color-primary);
}
}
.user-aside__inner {
height: 100%;
min-height: 0;
overflow: hidden;
transition: opacity 0.15s;
}
.user-aside__inner :deep(.dept-card) {
height: 100%;
background: transparent;
border: 0;
border-radius: inherit;
box-shadow: none;
> .el-card__body {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
padding: 12px;
}
}
.user-aside__inner :deep(.dept-card__tree) {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
}
.user-main {
display: flex;
flex: 1 1 0;
flex-direction: column;
gap: var(--page-gap);
min-width: 0;
min-height: 0;
overflow: hidden;
}
.user-main :deep(.page-search) {
.el-card__body {
padding-top: 12px;
padding-bottom: 12px;
}
.user-search__keyword {
width: 180px;
}
}
.user-aside__inner :deep(.dept-card__search) {
margin-bottom: 10px;
}
.user-aside__inner :deep(.el-tree-node__content) {
height: 32px;
}
.user-name-cell {
display: inline-flex;
gap: 8px;