ForcePilot/web/src/components/DepartmentManagementComponent.vue

550 lines
15 KiB
Vue
Raw Normal View History

<template>
<div class="department-management">
<!-- 头部区域 -->
<div class="header-section">
<div class="header-content">
<div class="section-title">部门管理</div>
<p class="section-description">管理系统部门部门下的用户会被隔离管理</p>
</div>
<a-button type="primary" @click="showAddDepartmentModal" class="add-btn lucide-icon-btn">
<template #icon><Plus :size="16" /></template>
添加部门
</a-button>
</div>
<!-- 主内容区域 -->
<div class="content-section">
<a-spin :spinning="departmentManagement.loading">
<div v-if="departmentManagement.error" class="error-message">
<a-alert type="error" :message="departmentManagement.error" show-icon />
</div>
<template v-if="departmentManagement.departments.length > 0">
<a-table
:dataSource="departmentManagement.departments"
:columns="columns"
:rowKey="(record) => record.id"
:pagination="false"
class="department-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'">
<div class="department-name">
<span class="name-text">{{ record.name }}</span>
</div>
</template>
<template v-if="column.key === 'description'">
<span class="description-text">{{ record.description || '-' }}</span>
</template>
<template v-if="column.key === 'userCount'">
<span>{{ record.user_count ?? 0 }} </span>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-tooltip title="编辑部门">
<a-button
type="text"
size="small"
@click="showEditDepartmentModal(record)"
class="action-btn lucide-icon-btn"
>
<Pencil :size="14" />
</a-button>
</a-tooltip>
<a-tooltip title="删除部门">
<a-button
type="text"
size="small"
danger
@click="confirmDeleteDepartment(record)"
:disabled="record.id === 1"
class="action-btn lucide-icon-btn"
>
<Trash2 :size="14" />
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
</a-table>
</template>
<div v-else class="empty-state">
<a-empty description="暂无部门数据" />
</div>
</a-spin>
</div>
<!-- 部门表单模态框 -->
<a-modal
v-model:open="departmentManagement.modalVisible"
:title="departmentManagement.modalTitle"
@ok="handleDepartmentFormSubmit"
:confirmLoading="departmentManagement.loading"
@cancel="departmentManagement.modalVisible = false"
:maskClosable="false"
width="520px"
class="department-modal"
>
<a-form layout="vertical" class="department-form">
<a-form-item label="部门名称" required class="form-item">
<a-input
v-model:value="departmentManagement.form.name"
placeholder="请输入部门名称"
size="large"
:maxlength="50"
/>
</a-form-item>
<a-form-item label="部门描述" class="form-item">
<a-textarea
v-model:value="departmentManagement.form.description"
placeholder="请输入部门描述(可选)"
:rows="3"
:maxlength="255"
show-count
/>
</a-form-item>
<a-divider v-if="!departmentManagement.editMode" />
<template v-if="!departmentManagement.editMode">
<div class="admin-section-title">
<Users :size="16" />
<span>部门管理员</span>
</div>
<p class="admin-section-hint">
创建部门时必须同时创建管理员该管理员将负责管理本部门用户
</p>
<a-form-item label="管理员用户ID" required class="form-item">
<a-input
v-model:value="departmentManagement.form.adminUserId"
placeholder="请输入管理员用户ID3-20位字母/数字/下划线)"
size="large"
:maxlength="20"
@blur="checkAdminUserId"
/>
<div v-if="departmentManagement.form.userIdError" class="error-text">
{{ departmentManagement.form.userIdError }}
</div>
<div v-else class="help-text">此ID将用于登录</div>
</a-form-item>
<a-form-item label="密码" required class="form-item">
<a-input-password
v-model:value="departmentManagement.form.adminPassword"
placeholder="请输入管理员密码"
size="large"
:maxlength="50"
/>
</a-form-item>
<a-form-item label="确认密码" required class="form-item">
<a-input-password
v-model:value="departmentManagement.form.adminConfirmPassword"
placeholder="请再次输入密码"
size="large"
:maxlength="50"
/>
</a-form-item>
<a-form-item label="手机号(可选)" class="form-item">
<a-input
v-model:value="departmentManagement.form.adminPhone"
placeholder="请输入手机号(可用于登录)"
size="large"
:maxlength="11"
/>
<div v-if="departmentManagement.form.phoneError" class="error-text">
{{ departmentManagement.form.phoneError }}
</div>
</a-form-item>
</template>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { reactive, onMounted, watch } from 'vue'
import { notification, Modal } from 'ant-design-vue'
import { departmentApi, apiSuperAdminGet } from '@/apis'
import { Delete as Trash2, Edit3 as Pencil, Plus, Users } from 'lucide-vue-next'
// 表格列定义
const columns = [
{
title: '部门名称',
dataIndex: 'name',
key: 'name',
width: 200
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true
},
{
title: '用户数量',
dataIndex: 'user_count',
key: 'userCount',
width: 100,
align: 'center'
},
{
title: '操作',
key: 'action',
width: 120,
align: 'center'
}
]
// 部门管理状态
const departmentManagement = reactive({
loading: false,
departments: [],
error: null,
modalVisible: false,
modalTitle: '添加部门',
editMode: false,
editDepartmentId: null,
form: {
name: '',
description: '',
adminUserId: '',
adminPassword: '',
adminConfirmPassword: '',
adminPhone: '',
userIdError: '',
phoneError: ''
}
})
// 获取部门列表
const fetchDepartments = async () => {
try {
departmentManagement.loading = true
departmentManagement.error = null
const departments = await departmentApi.getDepartments()
departmentManagement.departments = departments
} catch (error) {
console.error('获取部门列表失败:', error)
departmentManagement.error = '获取部门列表失败'
} finally {
departmentManagement.loading = false
}
}
// 打开添加部门模态框
const showAddDepartmentModal = () => {
departmentManagement.modalTitle = '添加部门'
departmentManagement.editMode = false
departmentManagement.editDepartmentId = null
departmentManagement.form = {
name: '',
description: '',
adminUserId: '',
adminPassword: '',
adminConfirmPassword: '',
adminPhone: '',
userIdError: '',
phoneError: ''
}
departmentManagement.modalVisible = true
}
// 打开编辑部门模态框
const showEditDepartmentModal = (department) => {
departmentManagement.modalTitle = '编辑部门'
departmentManagement.editMode = true
departmentManagement.editDepartmentId = department.id
departmentManagement.form = {
name: department.name,
description: department.description || '',
adminUserId: '',
adminPassword: '',
adminConfirmPassword: '',
adminPhone: '',
userIdError: '',
phoneError: ''
}
departmentManagement.modalVisible = true
}
// 验证手机号格式
const validatePhoneNumber = (phone) => {
if (!phone) {
return true // 手机号可选
}
const phoneRegex = /^1[3-9]\d{9}$/
return phoneRegex.test(phone)
}
// 监听手机号输入变化
watch(
() => departmentManagement.form.adminPhone,
(newPhone) => {
departmentManagement.form.phoneError = ''
if (newPhone && !validatePhoneNumber(newPhone)) {
departmentManagement.form.phoneError = '请输入正确的手机号格式'
}
}
)
// 检查管理员用户ID是否可用
const checkAdminUserId = async () => {
const userId = departmentManagement.form.adminUserId.trim()
departmentManagement.form.userIdError = ''
if (!userId) {
return
}
// 验证格式
if (!/^[a-zA-Z0-9_]+$/.test(userId)) {
departmentManagement.form.userIdError = '用户ID只能包含字母、数字和下划线'
return
}
if (userId.length < 3 || userId.length > 20) {
departmentManagement.form.userIdError = '用户ID长度必须在3-20个字符之间'
return
}
// 检查是否已存在
try {
const result = await apiSuperAdminGet(`/api/auth/check-user-id/${userId}`)
if (!result.is_available) {
departmentManagement.form.userIdError = '该用户ID已被使用'
}
} catch (error) {
console.error('检查用户ID失败:', error)
}
}
// 处理部门表单提交
const handleDepartmentFormSubmit = async () => {
try {
// 验证部门名称
if (!departmentManagement.form.name.trim()) {
notification.error({ message: '部门名称不能为空' })
return
}
if (departmentManagement.form.name.trim().length < 2) {
notification.error({ message: '部门名称至少2个字符' })
return
}
// 验证管理员用户ID
const adminUserId = departmentManagement.form.adminUserId.trim()
if (!adminUserId) {
notification.error({ message: '请输入管理员用户ID' })
return
}
if (!/^[a-zA-Z0-9_]+$/.test(adminUserId)) {
notification.error({ message: '用户ID只能包含字母、数字和下划线' })
return
}
if (adminUserId.length < 3 || adminUserId.length > 20) {
notification.error({ message: '用户ID长度必须在3-20个字符之间' })
return
}
if (departmentManagement.form.userIdError) {
notification.error({ message: '管理员用户ID已存在或格式错误' })
return
}
// 验证密码
if (!departmentManagement.form.adminPassword) {
notification.error({ message: '请输入管理员密码' })
return
}
if (
departmentManagement.form.adminPassword !== departmentManagement.form.adminConfirmPassword
) {
notification.error({ message: '两次输入的密码不一致' })
return
}
// 验证手机号
if (
departmentManagement.form.adminPhone &&
!validatePhoneNumber(departmentManagement.form.adminPhone)
) {
notification.error({ message: '请输入正确的手机号格式' })
return
}
departmentManagement.loading = true
if (departmentManagement.editMode) {
// 更新部门
await departmentApi.updateDepartment(departmentManagement.editDepartmentId, {
name: departmentManagement.form.name.trim(),
description: departmentManagement.form.description.trim() || undefined
})
notification.success({ message: '部门更新成功' })
} else {
// 创建部门,同时创建管理员
await departmentApi.createDepartment({
name: departmentManagement.form.name.trim(),
description: departmentManagement.form.description.trim() || undefined,
admin_user_id: adminUserId,
admin_password: departmentManagement.form.adminPassword,
admin_phone: departmentManagement.form.adminPhone || undefined
})
notification.success({ message: `部门创建成功,管理员 "${adminUserId}" 已创建` })
}
// 重新获取部门列表
await fetchDepartments()
departmentManagement.modalVisible = false
} catch (error) {
console.error('部门操作失败:', error)
notification.error({
message: '操作失败',
description: error.message || '请稍后重试'
})
} finally {
departmentManagement.loading = false
}
}
// 删除部门
const confirmDeleteDepartment = (department) => {
Modal.confirm({
title: '确认删除部门',
content: `确定要删除部门 "${department.name}" 吗?此操作不可撤销。该部门下的用户会被迁移到默认部门,部门级配置和部门 API Key 会一并清理。`,
okText: '删除',
okType: 'danger',
cancelText: '取消',
async onOk() {
try {
departmentManagement.loading = true
await departmentApi.deleteDepartment(department.id)
notification.success({ message: '部门删除成功' })
// 重新获取部门列表
await fetchDepartments()
} catch (error) {
console.error('删除部门失败:', error)
notification.error({
message: '删除失败',
description: error.message || '请稍后重试'
})
} finally {
departmentManagement.loading = false
}
}
})
}
// 在组件挂载时获取部门列表
onMounted(() => {
fetchDepartments()
})
</script>
<style lang="less" scoped>
.department-management {
.content-section {
overflow: hidden;
.error-message {
padding: 16px 24px;
}
.empty-state {
padding: 60px 20px;
text-align: center;
}
.department-table {
:deep(.ant-table-thead > tr > th) {
background: var(--gray-50);
font-weight: 500;
padding: 8px 12px;
}
:deep(.ant-table-tbody > tr > td) {
padding: 8px 12px;
}
.department-name {
.name-text {
font-weight: 500;
color: var(--gray-900);
}
}
.description-text {
color: var(--gray-600);
}
.action-btn {
padding: 4px 8px;
border-radius: 6px;
transition: all 0.2s ease;
&:hover {
background: var(--gray-25);
}
}
}
}
}
.department-modal {
:deep(.ant-modal-header) {
padding: 20px 24px;
border-bottom: 1px solid var(--gray-150);
.ant-modal-title {
font-size: 16px;
font-weight: 600;
color: var(--gray-900);
}
}
:deep(.ant-modal-body) {
padding: 24px;
}
.department-form {
.form-item {
margin-bottom: 20px;
:deep(.ant-form-item-label) {
padding-bottom: 4px;
label {
font-weight: 500;
color: var(--gray-900);
}
}
}
}
.error-text {
color: var(--color-error-500);
font-size: 12px;
margin-top: 4px;
line-height: 1.3;
}
.help-text {
color: var(--gray-600);
font-size: 12px;
margin-top: 4px;
line-height: 1.3;
}
}
</style>