用户部门编辑组件设计与实现文档
用户部门编辑组件设计与实现文档
一、组件概述
UserDepartmentEditor是一个基于 Quasar 框架和 TypeScript 的企业级 Vue 组件,专为宁夏地区企业安全管理系统设计,提供直观、安全的用户部门分配与修改功能。该组件采用组合式 API 设计,具备完整的类型安全保障、用户交互反馈和企业级安全特性。
二、实现方案
2.1 组件设计思路
![组件设计思路]
- 独立封装:将部门编辑功能封装为独立组件,便于复用与维护
- 类型安全:使用 TypeScript 接口定义数据结构,确保类型一致性
- 完整流程:包含部门选择、修改原因记录、权限验证、审计日志等企业级功能
- 用户体验:提供清晰的视觉反馈、加载状态和错误处理
- 事件驱动:通过自定义事件与父组件通信,降低耦合度
2.2 文件结构
src/
└── components/
└── user/
└── UserDepartmentEditor.vue # 用户部门编辑组件
三、组件代码实现
3.1 模板部分(Template)
<template>
<q-dialog
v-model="showDialog"
persistent
@hide="handleDialogClose"
class="department-editor-dialog"
>
<q-card style="min-width: 500px; max-width: 600px">
<!-- 标题区域 -->
<q-card-section class="bg-deep-orange text-white">
<div class="text-h6">修改用户所属部门</div>
<div class="text-subtitle2">
用户: {{ user?.nickname || user?.mobile_raw || '无昵称' }}
</div>
</q-card-section>
<q-separator />
<!-- 主要内容区域 -->
<q-card-section>
<!-- 当前部门信息 -->
<div class="row items-center q-mb-md">
<q-icon name="business" size="md" color="primary" class="q-mr-sm" />
<div>
<div class="text-subtitle2">当前部门</div>
<div class="text-h6 text-weight-bold">
{{ currentDepartmentName || '未分配' }}
</div>
</div>
</div>
<!-- 部门选择器 -->
<q-select
v-model="selectedDepartmentId"
:options="departmentOptions"
label="选择新部门"
emit-value
map-options
filled
clearable
class="q-mb-md"
:loading="loadingDepartments"
:disable="loadingDepartments || isSaving"
>
<template v-slot:prepend>
<q-icon name="business" />
</template>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>
<q-icon :name="getDepartmentIcon(scope.opt.value)" />
</q-item-section>
<q-item-section>
<q-item-label>{{ scope.opt.label }}</q-item-label>
<q-item-label caption>{{ scope.opt.description }}</q-item-label>
</q-item-section>
</q-item>
</template>
</q-select>
<!-- 修改原因输入 -->
<q-input
v-model="updateReason"
label="修改原因"
filled
type="textarea"
rows="2"
class="q-mb-md"
:disable="isSaving"
/>
<!-- 错误提示 -->
<div v-if="error" class="text-negative q-mt-md">
<q-icon name="error" /> {{ error }}
</div>
</q-card-section>
<!-- 操作按钮区域 -->
<q-card-actions align="right" class="q-pa-md">
<q-btn
label="取消"
color="grey"
flat
:disable="isSaving"
@click="handleCancel"
/>
<q-btn
label="保存更改"
color="deep-orange"
:loading="isSaving"
@click="handleSave"
>
<template v-slot:loading>
<q-spinner-hourglass />
</template>
</q-btn>
</q-card-actions>
</q-card>
</q-dialog>
</template>
3.2 脚本部分(Script)
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { QSelectOption, Notify } from 'quasar';
import { apiClient } from 'src/services/axios';
import { logSecurityEvent } from 'src/services/audit';
import { useAuthStore } from 'stores/auth';
// 部门接口定义
export interface Department {
id: number;
name: string;
code: string;
node_type: string;
is_active: boolean;
description?: string;
manager?: string;
}
// 用户信息接口
export interface UserProfile {
id: string;
nickname: string;
mobile_raw: string;
department?: number;
department_info?: {
id: number;
name: string;
};
}
// 组件属性
const props = defineProps({
show: {
type: Boolean,
required: true,
},
user: {
type: Object as () => UserProfile | null,
required: true,
},
});
// 组件事件
const emit = defineEmits([
'update:show',
'department-updated',
'cancel',
]);
// 状态管理
const showDialog = ref(false);
const loadingDepartments = ref(false);
const departments = ref<Department[]>([]);
const selectedDepartmentId = ref<number | null>(null);
const updateReason = ref('');
const isSaving = ref(false);
const error = ref<string | null>(null);
const authStore = useAuthStore();
// 计算属性
const currentDepartmentName = computed(() => {
return props.user?.department_info?.name || '';
});
const departmentOptions = computed<QSelectOption[]>(() => {
return departments.value.map(dept => ({
label: dept.name,
value: dept.id,
description: dept.description || `部门编码: ${dept.code}`,
}));
});
// 获取部门列表
const fetchDepartments = async () => {
try {
loadingDepartments.value = true;
error.value = null;
const response = await apiClient.get<Department[]>('/orgs/departments/', {
params: {
page_size: 100,
is_active: true
}
});
departments.value = response.data || [];
// 初始化选中的部门
if (props.user?.department) {
selectedDepartmentId.value = props.user.department;
}
} catch (err) {
console.error('获取部门列表失败:', err);
error.value = '获取部门列表失败,请稍后重试';
Notify.create({
type: 'negative',
message: '获取部门列表失败',
position: 'top-right'
});
} finally {
loadingDepartments.value = false;
}
};
// 根据部门类型获取图标
const getDepartmentIcon = (deptId: number) => {
const dept = departments.value.find(d => d.id === deptId);
if (!dept) return 'business';
switch (dept.node_type) {
case 'SAFETY_DEPARTMENT':
return 'mdi-shield-account';
case 'PRODUCTION_UNIT':
return 'mdi-factory';
case 'REGULATORY_AGENCY':
return 'mdi-scale-balance';
case 'THIRD_PARTY_AUDITOR':
return 'mdi-clipboard-check';
default:
return 'business';
}
};
// 处理保存操作
const handleSave = async () => {
if (!props.user || !selectedDepartmentId.value) {
error.value = '请选择部门';
return;
}
if (!updateReason.value) {
error.value = '请输入修改原因';
return;
}
try {
isSaving.value = true;
error.value = null;
// 构建更新数据
const updateData = {
department: selectedDepartmentId.value,
update_reason: updateReason.value,
};
// 发送更新请求
await apiClient.patch(`/users/profile/${props.user.id}/`, updateData);
// 成功通知
Notify.create({
type: 'positive',
message: '用户所属部门更新成功',
position: 'top',
timeout: 2000,
});
// 记录安全审计事件
logSecurityEvent('USER_DEPARTMENT_UPDATED', {
targetUserId: props.user.id,
updatedBy: authStore.userId,
newDepartment: selectedDepartmentId.value,
updateReason: updateReason.value,
});
// 触发更新事件
emit('department-updated', {
userId: props.user.id,
departmentId: selectedDepartmentId.value,
departmentName: departments.value.find(d => d.id === selectedDepartmentId.value)?.name || ''
});
// 关闭对话框
showDialog.value = false;
} catch (err) {
console.error('更新部门失败:', err);
error.value = '更新部门失败,请稍后重试';
if (typeof err === 'object' && err !== null && 'response' in err) {
const axiosError = err as { response?: { status: number; data?: unknown } };
if (axiosError.response?.status === 403) {
error.value = '权限不足:无法修改用户所属部门';
} else if (axiosError.response?.data) {
const errorData = axiosError.response.data as { detail?: string };
error.value = errorData.detail || error.value;
}
}
} finally {
isSaving.value = false;
}
};
// 处理取消操作
const handleCancel = () => {
showDialog.value = false;
emit('cancel');
};
// 处理对话框关闭
const handleDialogClose = () => {
emit('update:show', false);
};
// 监听显示状态变化
watch(() => props.show, (newVal) => {
showDialog.value = newVal;
if (newVal) {
// 当对话框显示时重置状态并获取部门
selectedDepartmentId.value = props.user?.department || null;
updateReason.value = '';
error.value = null;
fetchDepartments();
}
});
</script>
3.3 样式部分(Style)
<style lang="scss" scoped>
.department-editor-dialog {
.q-card {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
}
.q-card__section:first-child {
padding: 16px 24px;
background: linear-gradient(135deg, #ff7043, #f4511e);
}
.q-separator {
}
.q-card__actions {
border-top: 1px solid rgba(0, 0, 0, 0.1);
padding: 16px 24px;
}
.text-subtitle2 {
opacity: 0.85;
}
.q-input, .q-select {
margin-bottom: 16px;
}
}
</style>
四、组件使用方法
4.1 在用户管理页面中集成
<script setup lang="ts">
// ...其他导入保持不变...
import UserDepartmentEditor from 'components/user/UserDepartmentEditor.vue';
import type { UserProfile } from 'components/user/UserDepartmentEditor.vue';
// 添加部门编辑状态
const showDepartmentEditor = ref(false);
const editingUser = ref<UserProfile | null>(null);
const userList = ref<UserProfile[]>([]); // 假设存在用户列表数据
const authStore = useAuthStore();
// 打开部门编辑器
const openDepartmentEditor = (user: UserProfile) => {
editingUser.value = user;
showDepartmentEditor.value = true;
};
// 处理部门更新事件
const handleDepartmentUpdated = (payload: {
userId: string;
departmentId: number;
departmentName: string;
}) => {
const user = userList.value.find(u => u.id === payload.userId);
if (user) {
user.department = payload.departmentId;
user.department_info = {
id: payload.departmentId,
name: payload.departmentName
};
}
};
</script>
<template>
<!-- ...其他代码保持不变... -->
<!-- 用户部门编辑对话框 -->
<UserDepartmentEditor
v-model:show="showDepartmentEditor"
:user="editingUser"
@department-updated="handleDepartmentUpdated"
@cancel="showDepartmentEditor = false"
/>
<!-- 表格中部门编辑按钮 -->
<template v-slot:body-cell-department_edit="props">
<q-td :props="props" auto-width>
<q-btn
icon="business"
size="sm"
color="deep-orange"
dense
round
@click="openDepartmentEditor(props.row)"
:disable="props.row.id === authStore.userId"
>
<q-tooltip>修改部门</q-tooltip>
</q-btn>
</q-td>
</template>
</template>
4.2 属性与事件说明
| 名称 | 类型 | 说明 | 
| show | Boolean | 控制组件显示/隐藏(双向绑定) | 
| user | UserProfile | 用户信息对象 | 
| department-updated | Event | 部门更新成功时触发,返回用户ID和新部门信息 | 
| cancel | Event | 用户取消操作时触发 | 
五、组件设计特点
5.1 独立组件设计
- 松耦合架构:通过 props 和 events 与父组件通信,不依赖特定业务逻辑
- 即插即用:无需修改核心代码即可集成到任何用户管理页面
- 自包含功能:内部处理数据加载、验证、提交和反馈
- 完整审计日志:通过logSecurityEvent记录所有修改操作
- 权限控制:前端禁用自身账号修改,后端验证权限
- 输入验证:强制要求修改原因,确保操作可追溯
- 错误处理:针对403等状态码提供明确的错误提示
- 直观的视觉层次:使用卡片、分隔线和色彩区分功能区域
- 实时状态反馈:加载动画、保存状态和操作结果通知
- 部门信息展示:清晰显示当前部门和可选部门详情
- 响应式设计:适配不同屏幕尺寸,保持良好交互体验
- TypeScript 类型安全:完整的接口定义和类型检查
- 组合式 API:逻辑清晰,便于维护和扩展
- Quasar 组件充分利用:对话框、选择器、通知等组件无缝集成
- 自定义部门图标:根据部门类型动态显示不同图标
5.2 企业级安全特性
5.3 用户体验优化
5.4 技术亮点
六、总结
UserDepartmentEditor组件为企业安全管理系统提供了专业、安全、易用的用户部门管理功能,特别适合宁夏地区企业的组织架构管理需求。通过将复杂的业务逻辑封装在独立组件中,既保证了代码的可维护性,又提供了一致的用户体验。
组件的核心价值在于:
1. 安全性:完整的审计跟踪和权限控制
2. 可维护性:模块化设计和类型安全保障
3. 用户体验:直观的界面和即时的操作反馈
4. 可扩展性:易于添加新功能和适配业务变化
建议在企业级应用中广泛采用此类组件化设计,以提升开发效率和系统质量。
 
                    
                 
                
            
         
 浙公网安备 33010602011771号
浙公网安备 33010602011771号