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

View File

@ -1,10 +1,10 @@
<template>
<div class="project-member-container">
<div class="member-container">
<el-card>
<!-- 搜索栏 -->
<el-form :inline="true" :model="searchForm">
<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
v-for="project in projectOptions"
:key="project.projectId"
@ -13,25 +13,53 @@
/>
</el-select>
</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-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>
<!-- 操作栏 -->
<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-column prop="userId" label="用户ID" width="100" />
<el-table-column prop="role" label="项目角色" width="120">
<template #default="{ row }">
<el-tag v-if="row.role === 'pm'" type="danger">项目经理</el-tag>
<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>
{{ getRoleText(row.role) }}
</template>
</el-table-column>
<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">
<template #default="{ row }">
{{ row.workload }}%
@ -43,11 +71,23 @@
<el-tag v-else type="info">已离开</el-tag>
</template>
</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 label="操作" width="200" fixed="right">
<el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }">
<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>
</template>
</el-table-column>
@ -57,12 +97,12 @@
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="form.memberId ? '编辑成员' : '添加成员'"
:title="form.memberId ? '编辑项目成员' : '添加项目成员'"
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-select v-model="form.projectId" placeholder="请选择项目" style="width: 100%;">
<el-select v-model="form.projectId" placeholder="请选择项目" style="width: 100%;" disabled>
<el-option
v-for="project in projectOptions"
:key="project.projectId"
@ -73,7 +113,7 @@
</el-form-item>
<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 label="项目角色" prop="role">
@ -98,28 +138,12 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="离开日期">
<el-date-picker
v-model="form.leaveDate"
type="date"
placeholder="选择日期"
style="width: 100%;"
/>
<el-form-item label="工作量占比">
<el-input-number v-model="form.workload" :min="0" :max="100" style="width: 100%;" />
</el-form-item>
</el-col>
</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-input
v-model="form.remark"
@ -142,16 +166,19 @@
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
getProjectMembers,
addProjectMember,
updateProjectMember,
removeProjectMember
getMemberListByProject,
addMember,
updateMember,
removeMember,
updateMemberStatus
} from '../../api/projectMember'
import { getProjectList } from '../../api/project'
//
const searchForm = reactive({
projectId: null
projectId: null,
role: '',
status: null
})
//
@ -172,9 +199,7 @@ const form = reactive({
userId: null,
role: 'member',
joinDate: null,
leaveDate: null,
workload: 0,
status: 1,
remark: ''
})
@ -187,7 +212,7 @@ const rules = {
{ required: true, message: '请输入用户ID', trigger: 'blur' }
],
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 () => {
if (!searchForm.projectId) {
ElMessage.warning('请选择项目')
tableData.value = []
return
}
loading.value = true
try {
const res = await getProjectMembers(searchForm.projectId)
tableData.value = res
let data = await getMemberListByProject(searchForm.projectId)
//
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) {
console.error('加载数据失败:', error)
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 = () => {
if (!searchForm.projectId) {
@ -233,9 +292,7 @@ const handleAdd = () => {
userId: null,
role: 'member',
joinDate: null,
leaveDate: null,
workload: 0,
status: 1,
remark: ''
})
dialogVisible.value = true
@ -249,9 +306,7 @@ const handleEdit = (row) => {
userId: row.userId,
role: row.role,
joinDate: row.joinDate,
leaveDate: row.leaveDate,
workload: row.workload,
status: row.status,
remark: row.remark
})
dialogVisible.value = true
@ -270,10 +325,10 @@ const handleSubmit = async () => {
try {
if (form.memberId) {
await updateProjectMember(form.memberId, form)
await updateMember(form.memberId, form)
ElMessage.success('更新成功')
} else {
await addProjectMember(form)
await addMember(form)
ElMessage.success('添加成功')
}
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) => {
try {
@ -293,7 +369,7 @@ const handleRemove = async (row) => {
type: 'warning'
})
await removeProjectMember(row.memberId)
await removeMember(row.memberId)
ElMessage.success('移除成功')
await fetchData()
} catch (error) {
@ -311,7 +387,7 @@ onMounted(() => {
</script>
<style scoped>
.project-member-container {
.member-container {
padding: 20px;
}
</style>