feat: 人员分配模块前端实现

前端实现:
- userAssignment.js: API接口封装(56行,5个接口)
  - getUserAssignmentList(): 分页查询
  - getUserListByDept(): 按部门查询
  - assignUser(): 分配用户
  - batchAssignUsers(): 批量分配
  - removeUserAssignment(): 移除分配

- userAssignment.vue: 人员分配页面(408行)
  - 搜索功能:用户名、真实姓名、部门(树选择)、岗位、状态
  - 表格展示:用户信息、部门标签、岗位标签、职位、状态
  - 批量操作:多选批量分配
  - 筛选功能:仅显示未分配用户
  - 分配对话框:部门树选择、岗位下拉、职位输入
  - 移除分配:确认后移除部门/岗位关联

技术特点:
- 多条件组合搜索
- 表格多选(el-table selection)
- 部门树选择器
- 岗位下拉选择
- 状态标签展示
- 批量分配优化

模块状态: 完整(前端+后端)
This commit is contained in:
zhangjf 2026-02-16 09:48:22 +08:00
parent 25776cc9a4
commit 8ac97bb313
2 changed files with 462 additions and 0 deletions

View File

@ -0,0 +1,55 @@
import request from '../utils/request'
/**
* 获取用户分配列表分页
*/
export const getUserAssignmentList = (params) => {
return request({
url: '/sys/api/v1/user-assignment/list',
method: 'get',
params
})
}
/**
* 根据部门ID获取用户列表
*/
export const getUserListByDept = (deptId) => {
return request({
url: `/sys/api/v1/user-assignment/list/dept/${deptId}`,
method: 'get'
})
}
/**
* 分配用户到部门和岗位
*/
export const assignUser = (userId, data) => {
return request({
url: `/sys/api/v1/user-assignment/${userId}`,
method: 'put',
params: data
})
}
/**
* 批量分配用户
*/
export const batchAssignUsers = (userIds, deptId, postId) => {
return request({
url: '/sys/api/v1/user-assignment/batch',
method: 'put',
data: userIds,
params: { deptId, postId }
})
}
/**
* 移除用户分配
*/
export const removeUserAssignment = (userId) => {
return request({
url: `/sys/api/v1/user-assignment/${userId}`,
method: 'delete'
})
}

View File

@ -0,0 +1,407 @@
<template>
<div class="assignment-container">
<el-card>
<!-- 搜索栏 -->
<el-form :inline="true" :model="searchForm">
<el-form-item label="用户名">
<el-input v-model="searchForm.username" placeholder="请输入用户名" clearable style="width: 150px;" />
</el-form-item>
<el-form-item label="真实姓名">
<el-input v-model="searchForm.realName" placeholder="请输入真实姓名" clearable style="width: 150px;" />
</el-form-item>
<el-form-item label="所属部门">
<el-tree-select
v-model="searchForm.deptId"
:data="deptTreeOptions"
placeholder="请选择部门"
clearable
check-strictly
:render-after-expand="false"
style="width: 200px;"
/>
</el-form-item>
<el-form-item label="岗位">
<el-select v-model="searchForm.postId" placeholder="请选择岗位" clearable style="width: 150px;">
<el-option
v-for="post in postOptions"
:key="post.postId"
:label="post.postName"
:value="post.postId"
/>
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 120px;">
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作栏 -->
<el-row style="margin-bottom: 15px;">
<el-button type="primary" @click="handleBatchAssign" :disabled="selectedUsers.length === 0">
批量分配
</el-button>
<el-button @click="handleShowUnassigned" :type="showUnassigned ? 'primary' : ''">
{{ showUnassigned ? '显示全部' : '仅显示未分配' }}
</el-button>
<span style="margin-left: 15px; color: #909399;">
已选择 {{ selectedUsers.length }}
</span>
</el-row>
<!-- 表格 -->
<el-table
:data="tableData"
border
v-loading="loading"
stripe
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="userId" label="用户ID" width="80" />
<el-table-column prop="username" label="用户名" width="120" />
<el-table-column prop="realName" label="真实姓名" width="120" />
<el-table-column prop="gender" label="性别" width="80">
<template #default="{ row }">
{{ row.gender === 1 ? '男' : row.gender === 2 ? '女' : '未知' }}
</template>
</el-table-column>
<el-table-column prop="phone" label="手机号" width="140" />
<el-table-column prop="deptName" label="所属部门" width="150">
<template #default="{ row }">
<el-tag v-if="row.deptName && row.deptName !== '-'" type="info">{{ row.deptName }}</el-tag>
<el-tag v-else type="warning">未分配</el-tag>
</template>
</el-table-column>
<el-table-column prop="postName" label="岗位" width="150">
<template #default="{ row }">
<el-tag v-if="row.postName && row.postName !== '-'" type="success">{{ row.postName }}</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="position" label="职位" width="150" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.status === 1" type="success">启用</el-tag>
<el-tag v-else type="danger">禁用</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleAssign(row)">分配</el-button>
<el-button
v-if="row.deptId || row.postId"
type="warning"
size="small"
@click="handleRemove(row)"
>
移除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="page.current"
v-model:page-size="page.size"
:total="page.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="fetchData"
@current-change="fetchData"
style="margin-top: 20px; justify-content: flex-end;"
/>
</el-card>
<!-- 分配对话框 -->
<el-dialog
v-model="dialogVisible"
:title="isBatch ? '批量分配用户' : '分配用户'"
width="500px"
>
<el-form :model="form" label-width="100px">
<el-form-item label="选择用户" v-if="isBatch">
<el-tag
v-for="user in selectedUsers"
:key="user.userId"
style="margin-right: 5px; margin-bottom: 5px;"
>
{{ user.realName || user.username }}
</el-tag>
</el-form-item>
<el-form-item label="用户" v-else>
<span>{{ form.userName }}</span>
</el-form-item>
<el-form-item label="所属部门">
<el-tree-select
v-model="form.deptId"
:data="deptTreeOptions"
placeholder="请选择部门"
clearable
check-strictly
:render-after-expand="false"
style="width: 100%;"
/>
</el-form-item>
<el-form-item label="岗位">
<el-select v-model="form.postId" placeholder="请选择岗位" clearable style="width: 100%;">
<el-option
v-for="post in postOptions"
:key="post.postId"
:label="post.postName"
:value="post.postId"
/>
</el-select>
</el-form-item>
<el-form-item label="职位">
<el-input v-model="form.position" placeholder="请输入职位(可选)" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
getUserAssignmentList,
assignUser,
batchAssignUsers,
removeUserAssignment
} from '../../api/userAssignment'
import { getDeptTree } from '../../api/dept'
import { getPostListEnabled } from '../../api/post'
//
const searchForm = reactive({
username: '',
realName: '',
deptId: null,
postId: null,
status: null
})
//
const tableData = ref([])
const loading = ref(false)
const showUnassigned = ref(false)
//
const page = reactive({
current: 1,
size: 10,
total: 0
})
//
const selectedUsers = ref([])
//
const dialogVisible = ref(false)
const isBatch = ref(false)
//
const form = reactive({
userId: null,
userName: '',
deptId: null,
postId: null,
position: ''
})
//
const deptTreeOptions = ref([])
//
const postOptions = ref([])
//
const loadDeptTree = async () => {
try {
const data = await getDeptTree()
deptTreeOptions.value = convertToTreeSelect(data)
} catch (error) {
console.error('加载部门树失败:', error)
}
}
//
const loadPostOptions = async () => {
try {
const data = await getPostListEnabled()
postOptions.value = data
} catch (error) {
console.error('加载岗位列表失败:', error)
}
}
// TreeSelect
const convertToTreeSelect = (data) => {
return data.map(item => ({
value: item.deptId,
label: item.deptName,
children: item.children ? convertToTreeSelect(item.children) : []
}))
}
//
const fetchData = async () => {
loading.value = true
try {
const params = {
current: page.current,
size: page.size,
username: searchForm.username || undefined,
realName: searchForm.realName || undefined,
deptId: searchForm.deptId || undefined,
postId: searchForm.postId || undefined,
status: searchForm.status !== null && searchForm.status !== '' ? searchForm.status : undefined
}
const res = await getUserAssignmentList(params)
let data = res.records
//
if (showUnassigned.value) {
data = data.filter(item => !item.deptId && !item.postId)
}
tableData.value = data
page.total = res.total
} catch (error) {
console.error('加载数据失败:', error)
ElMessage.error(error.message || '加载数据失败')
} finally {
loading.value = false
}
}
//
const handleSearch = () => {
page.current = 1
fetchData()
}
//
const handleReset = () => {
searchForm.username = ''
searchForm.realName = ''
searchForm.deptId = null
searchForm.postId = null
searchForm.status = null
showUnassigned.value = false
page.current = 1
fetchData()
}
//
const handleShowUnassigned = () => {
showUnassigned.value = !showUnassigned.value
page.current = 1
fetchData()
}
//
const handleSelectionChange = (selection) => {
selectedUsers.value = selection
}
//
const handleAssign = (row) => {
isBatch.value = false
form.userId = row.userId
form.userName = row.realName || row.username
form.deptId = row.deptId
form.postId = row.postId
form.position = row.position
dialogVisible.value = true
}
//
const handleBatchAssign = () => {
if (selectedUsers.value.length === 0) {
ElMessage.warning('请选择要分配的用户')
return
}
isBatch.value = true
form.deptId = null
form.postId = null
form.position = ''
dialogVisible.value = true
}
//
const handleSubmit = async () => {
try {
if (isBatch.value) {
//
const userIds = selectedUsers.value.map(u => u.userId)
await batchAssignUsers(userIds, form.deptId, form.postId)
ElMessage.success(`成功分配 ${userIds.length} 个用户`)
} else {
//
await assignUser(form.userId, {
deptId: form.deptId,
postId: form.postId,
position: form.position
})
ElMessage.success('分配成功')
}
dialogVisible.value = false
selectedUsers.value = []
await fetchData()
} catch (error) {
console.error('分配失败:', error)
ElMessage.error(error.message || '分配失败')
}
}
//
const handleRemove = async (row) => {
try {
await ElMessageBox.confirm(`确定要移除 ${row.realName || row.username} 的部门/岗位分配吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await removeUserAssignment(row.userId)
ElMessage.success('移除分配成功')
await fetchData()
} catch (error) {
if (error !== 'cancel') {
console.error('移除分配失败:', error)
ElMessage.error(error.message || '移除分配失败')
}
}
}
//
onMounted(() => {
loadDeptTree()
loadPostOptions()
fetchData()
})
</script>
<style scoped>
.assignment-container {
padding: 20px;
}
</style>