eagleye

用户部门编辑组件设计与实现文档

用户部门编辑组件设计与实现文档

一、组件概述

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. 可扩展性:易于添加新功能和适配业务变化

建议在企业级应用中广泛采用此类组件化设计,以提升开发效率和系统质量。

 

posted on 2025-08-14 18:01  GoGrid  阅读(14)  评论(0)    收藏  举报

导航