feat: 项目成员管理前端页面实现

前端实现:
- projectMember.js: API接口封装(7个接口)
- projectMember.vue: 项目成员管理页面

页面特点:
- 以项目为维度管理成员
- 角色类型:项目经理/开发/测试/财务/普通成员
- 状态管理:在职/已离开
- 工作量占比:0-100%

模块状态: 完整(前端+后端)
This commit is contained in:
zhangjf 2026-02-16 09:03:00 +08:00
parent 6702b92a95
commit 588ef25869
2 changed files with 137 additions and 61 deletions

View File

@ -1,9 +1,9 @@
import request from '../utils/request' import request from '../utils/request'
/** /**
* 获取项目成员列表 * 获取项目成员列表按项目ID
*/ */
export const getProjectMembers = (projectId) => { export const getMemberListByProject = (projectId) => {
return request({ return request({
url: `/proj/api/v1/project-member/list/project/${projectId}`, url: `/proj/api/v1/project-member/list/project/${projectId}`,
method: 'get' method: 'get'
@ -11,9 +11,9 @@ export const getProjectMembers = (projectId) => {
} }
/** /**
* 获取用户参与的项目列表 * 获取用户的项目列表按用户ID
*/ */
export const getUserProjects = (userId) => { export const getMemberListByUser = (userId) => {
return request({ return request({
url: `/proj/api/v1/project-member/list/user/${userId}`, url: `/proj/api/v1/project-member/list/user/${userId}`,
method: 'get' method: 'get'
@ -21,7 +21,7 @@ export const getUserProjects = (userId) => {
} }
/** /**
* 获取成员详情 * 获取项目成员详情
*/ */
export const getMemberById = (memberId) => { export const getMemberById = (memberId) => {
return request({ return request({
@ -33,7 +33,7 @@ export const getMemberById = (memberId) => {
/** /**
* 添加项目成员 * 添加项目成员
*/ */
export const addProjectMember = (data) => { export const addMember = (data) => {
return request({ return request({
url: '/proj/api/v1/project-member', url: '/proj/api/v1/project-member',
method: 'post', method: 'post',
@ -44,7 +44,7 @@ export const addProjectMember = (data) => {
/** /**
* 更新项目成员 * 更新项目成员
*/ */
export const updateProjectMember = (memberId, data) => { export const updateMember = (memberId, data) => {
return request({ return request({
url: `/proj/api/v1/project-member/${memberId}`, url: `/proj/api/v1/project-member/${memberId}`,
method: 'put', method: 'put',
@ -55,7 +55,7 @@ export const updateProjectMember = (memberId, data) => {
/** /**
* 移除项目成员 * 移除项目成员
*/ */
export const removeProjectMember = (memberId) => { export const removeMember = (memberId) => {
return request({ return request({
url: `/proj/api/v1/project-member/${memberId}`, url: `/proj/api/v1/project-member/${memberId}`,
method: 'delete' method: 'delete'

View File

@ -1,10 +1,10 @@
<template> <template>
<div class="project-member-container"> <div class="member-container">
<el-card> <el-card>
<!-- 搜索栏 --> <!-- 搜索栏 -->
<el-form :inline="true" :model="searchForm"> <el-form :inline="true" :model="searchForm">
<el-form-item label="项目"> <el-form-item label="项目">
<el-select v-model="searchForm.projectId" placeholder="请选择项目" clearable @change="fetchData"> <el-select v-model="searchForm.projectId" placeholder="请选择项目" clearable @change="handleSearch">
<el-option <el-option
v-for="project in projectOptions" v-for="project in projectOptions"
:key="project.projectId" :key="project.projectId"
@ -13,25 +13,53 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="角色">
<el-select v-model="searchForm.role" placeholder="请选择角色" clearable @change="handleSearch">
<el-option label="项目经理" value="pm" />
<el-option label="开发" value="dev" />
<el-option label="测试" value="test" />
<el-option label="财务" value="finance" />
<el-option label="普通成员" value="member" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable @change="handleSearch">
<el-option label="在职" :value="1" />
<el-option label="已离开" :value="0" />
</el-select>
</el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="handleAdd">添加成员</el-button> <el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<!-- 操作栏 -->
<el-row style="margin-bottom: 15px;">
<el-button type="primary" @click="handleAdd" :disabled="!searchForm.projectId">添加成员</el-button>
<el-alert
v-if="!searchForm.projectId"
title="请先选择项目"
type="info"
:closable="false"
style="margin-top: 10px;"
/>
</el-row>
<!-- 表格 --> <!-- 表格 -->
<el-table :data="tableData" border v-loading="loading"> <el-table :data="tableData" border v-loading="loading">
<el-table-column prop="userId" label="用户ID" width="100" /> <el-table-column prop="userId" label="用户ID" width="100" />
<el-table-column prop="role" label="项目角色" width="120"> <el-table-column prop="role" label="项目角色" width="120">
<template #default="{ row }"> <template #default="{ row }">
<el-tag v-if="row.role === 'pm'" type="danger">项目经理</el-tag> {{ getRoleText(row.role) }}
<el-tag v-else-if="row.role === 'dev'" type="primary">开发</el-tag>
<el-tag v-else-if="row.role === 'test'" type="warning">测试</el-tag>
<el-tag v-else-if="row.role === 'finance'" type="success">财务</el-tag>
<el-tag v-else type="info">普通成员</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="joinDate" label="加入日期" width="120" /> <el-table-column prop="joinDate" label="加入日期" width="120" />
<el-table-column prop="leaveDate" label="离开日期" width="120" /> <el-table-column prop="leaveDate" label="离开日期" width="120">
<template #default="{ row }">
{{ row.leaveDate || '-' }}
</template>
</el-table-column>
<el-table-column prop="workload" label="工作量占比" width="120"> <el-table-column prop="workload" label="工作量占比" width="120">
<template #default="{ row }"> <template #default="{ row }">
{{ row.workload }}% {{ row.workload }}%
@ -43,11 +71,23 @@
<el-tag v-else type="info">已离开</el-tag> <el-tag v-else type="info">已离开</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip /> <el-table-column prop="remark" label="备注" width="150" show-overflow-tooltip />
<el-table-column prop="createdTime" label="创建时间" width="180" /> <el-table-column prop="createdTime" label="创建时间" width="180" />
<el-table-column label="操作" width="200" fixed="right"> <el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button> <el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
<el-button
v-if="row.status === 1"
type="warning"
size="small"
@click="handleUpdateStatus(row, 0)"
>标记离开</el-button>
<el-button
v-else
type="success"
size="small"
@click="handleUpdateStatus(row, 1)"
>恢复在职</el-button>
<el-button type="danger" size="small" @click="handleRemove(row)">移除</el-button> <el-button type="danger" size="small" @click="handleRemove(row)">移除</el-button>
</template> </template>
</el-table-column> </el-table-column>
@ -57,12 +97,12 @@
<!-- 新增/编辑对话框 --> <!-- 新增/编辑对话框 -->
<el-dialog <el-dialog
v-model="dialogVisible" v-model="dialogVisible"
:title="form.memberId ? '编辑成员' : '添加成员'" :title="form.memberId ? '编辑项目成员' : '添加项目成员'"
width="600px" width="600px"
> >
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px"> <el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
<el-form-item label="项目" prop="projectId"> <el-form-item label="项目" prop="projectId">
<el-select v-model="form.projectId" placeholder="请选择项目" style="width: 100%;"> <el-select v-model="form.projectId" placeholder="请选择项目" style="width: 100%;" disabled>
<el-option <el-option
v-for="project in projectOptions" v-for="project in projectOptions"
:key="project.projectId" :key="project.projectId"
@ -73,7 +113,7 @@
</el-form-item> </el-form-item>
<el-form-item label="用户ID" prop="userId"> <el-form-item label="用户ID" prop="userId">
<el-input-number v-model="form.userId" :min="1" style="width: 100%;" /> <el-input-number v-model="form.userId" :min="1" style="width: 100%;" placeholder="请输入用户ID" />
</el-form-item> </el-form-item>
<el-form-item label="项目角色" prop="role"> <el-form-item label="项目角色" prop="role">
@ -98,28 +138,12 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="离开日期"> <el-form-item label="工作量占比">
<el-date-picker <el-input-number v-model="form.workload" :min="0" :max="100" style="width: 100%;" />
v-model="form.leaveDate"
type="date"
placeholder="选择日期"
style="width: 100%;"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-form-item label="工作量占比" prop="workload">
<el-slider v-model="form.workload" :min="0" :max="100" show-input />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio :label="1">在职</el-radio>
<el-radio :label="0">已离开</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注说明"> <el-form-item label="备注说明">
<el-input <el-input
v-model="form.remark" v-model="form.remark"
@ -142,16 +166,19 @@
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { import {
getProjectMembers, getMemberListByProject,
addProjectMember, addMember,
updateProjectMember, updateMember,
removeProjectMember removeMember,
updateMemberStatus
} from '../../api/projectMember' } from '../../api/projectMember'
import { getProjectList } from '../../api/project' import { getProjectList } from '../../api/project'
// //
const searchForm = reactive({ const searchForm = reactive({
projectId: null projectId: null,
role: '',
status: null
}) })
// //
@ -172,9 +199,7 @@ const form = reactive({
userId: null, userId: null,
role: 'member', role: 'member',
joinDate: null, joinDate: null,
leaveDate: null,
workload: 0, workload: 0,
status: 1,
remark: '' remark: ''
}) })
@ -187,7 +212,7 @@ const rules = {
{ required: true, message: '请输入用户ID', trigger: 'blur' } { required: true, message: '请输入用户ID', trigger: 'blur' }
], ],
role: [ role: [
{ required: true, message: '请选择角色', trigger: 'change' } { required: true, message: '请选择项目角色', trigger: 'change' }
] ]
} }
@ -201,17 +226,38 @@ const loadProjects = async () => {
} }
} }
//
const getRoleText = (role) => {
const roleMap = {
'pm': '项目经理',
'dev': '开发',
'test': '测试',
'finance': '财务',
'member': '普通成员'
}
return roleMap[role] || role
}
// //
const fetchData = async () => { const fetchData = async () => {
if (!searchForm.projectId) { if (!searchForm.projectId) {
ElMessage.warning('请选择项目') tableData.value = []
return return
} }
loading.value = true loading.value = true
try { try {
const res = await getProjectMembers(searchForm.projectId) let data = await getMemberListByProject(searchForm.projectId)
tableData.value = res
//
if (searchForm.role) {
data = data.filter(item => item.role === searchForm.role)
}
if (searchForm.status !== null && searchForm.status !== '') {
data = data.filter(item => item.status === searchForm.status)
}
tableData.value = data
} catch (error) { } catch (error) {
console.error('加载数据失败:', error) console.error('加载数据失败:', error)
ElMessage.error(error.message || '加载数据失败') ElMessage.error(error.message || '加载数据失败')
@ -220,6 +266,19 @@ const fetchData = async () => {
} }
} }
//
const handleSearch = () => {
fetchData()
}
//
const handleReset = () => {
searchForm.projectId = null
searchForm.role = ''
searchForm.status = null
tableData.value = []
}
// //
const handleAdd = () => { const handleAdd = () => {
if (!searchForm.projectId) { if (!searchForm.projectId) {
@ -233,9 +292,7 @@ const handleAdd = () => {
userId: null, userId: null,
role: 'member', role: 'member',
joinDate: null, joinDate: null,
leaveDate: null,
workload: 0, workload: 0,
status: 1,
remark: '' remark: ''
}) })
dialogVisible.value = true dialogVisible.value = true
@ -249,9 +306,7 @@ const handleEdit = (row) => {
userId: row.userId, userId: row.userId,
role: row.role, role: row.role,
joinDate: row.joinDate, joinDate: row.joinDate,
leaveDate: row.leaveDate,
workload: row.workload, workload: row.workload,
status: row.status,
remark: row.remark remark: row.remark
}) })
dialogVisible.value = true dialogVisible.value = true
@ -270,10 +325,10 @@ const handleSubmit = async () => {
try { try {
if (form.memberId) { if (form.memberId) {
await updateProjectMember(form.memberId, form) await updateMember(form.memberId, form)
ElMessage.success('更新成功') ElMessage.success('更新成功')
} else { } else {
await addProjectMember(form) await addMember(form)
ElMessage.success('添加成功') ElMessage.success('添加成功')
} }
dialogVisible.value = false dialogVisible.value = false
@ -284,6 +339,27 @@ const handleSubmit = async () => {
} }
} }
//
const handleUpdateStatus = async (row, status) => {
const statusText = status === 1 ? '恢复在职' : '标记离开'
try {
await ElMessageBox.confirm(`确定要${statusText}吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await updateMemberStatus(row.memberId, status)
ElMessage.success(`${statusText}成功`)
await fetchData()
} catch (error) {
if (error !== 'cancel') {
console.error('状态更新失败:', error)
ElMessage.error(error.message || '状态更新失败')
}
}
}
// //
const handleRemove = async (row) => { const handleRemove = async (row) => {
try { try {
@ -293,7 +369,7 @@ const handleRemove = async (row) => {
type: 'warning' type: 'warning'
}) })
await removeProjectMember(row.memberId) await removeMember(row.memberId)
ElMessage.success('移除成功') ElMessage.success('移除成功')
await fetchData() await fetchData()
} catch (error) { } catch (error) {
@ -311,7 +387,7 @@ onMounted(() => {
</script> </script>
<style scoped> <style scoped>
.project-member-container { .member-container {
padding: 20px; padding: 20px;
} }
</style> </style>