diff --git a/src/apis/project/type.ts b/src/apis/project/type.ts index ea6c210..7840bcc 100644 --- a/src/apis/project/type.ts +++ b/src/apis/project/type.ts @@ -117,6 +117,86 @@ export interface ProjectKanbanStats { teamLeaderCount: string } +// ==================== 工作组相关类型 ==================== + +/** 工作组类型 */ +export interface WorkGroup { + id: string + name: string + description: string + projectId: string + status: 'ACTIVE' | 'INACTIVE' | 'COMPLETED' + workTypeGroups: WorkTypeGroup[] + memberCount: number + requiredCount: number + members?: WorkGroupMember[] + createTime: string + updateTime?: string +} + +/** 工种分组类型 */ +export interface WorkTypeGroup { + id?: string + name: string + requiredCount: number + priority: 'HIGH' | 'MEDIUM' | 'LOW' + assignedCount?: number +} + +/** 工作组成员类型 */ +export interface WorkGroupMember { + id: string + name: string + role: string + avatar?: string + phone?: string + email?: string + workTypes?: string[] + joinTime?: string +} + +/** 工作组查询参数 */ +export interface WorkGroupQuery { + projectId?: string + name?: string + status?: string + workType?: string +} + +/** 工作组分页查询参数 */ +export interface WorkGroupPageQuery extends WorkGroupQuery, PageQuery {} + +/** 创建工作组请求参数 */ +export interface CreateWorkGroupReq { + name: string + description?: string + projectId: string + workTypeGroups: WorkTypeGroup[] +} + +/** 更新工作组请求参数 */ +export interface UpdateWorkGroupReq { + id: string + name?: string + description?: string + status?: string + workTypeGroups?: WorkTypeGroup[] +} + +/** 分配工人到工作组请求参数 */ +export interface AssignWorkerReq { + workGroupId: string + workerIds: string[] + workTypeName: string +} + +/** 从工作组移除工人请求参数 */ +export interface RemoveWorkerReq { + workGroupId: string + workerId: string + workTypeName: string +} + /** 项目看板数据 */ export interface ProjectKanbanData { inProgressProjects: ProjectCard[] @@ -471,4 +551,89 @@ export interface PageRes { total: number page: number pageSize: number +} + +// ==================== 工作组相关类型 ==================== + +/** 工作组类型 */ +export interface WorkGroup { + id: string + name: string + description: string + projectId: string + status: 'ACTIVE' | 'INACTIVE' | 'COMPLETED' + workTypeGroups: WorkTypeGroup[] + memberCount: number + requiredCount: number + members?: WorkGroupMember[] + createTime: string + updateTime?: string +} + +/** 工种分组类型 */ +export interface WorkTypeGroup { + id?: string + name: string + requiredCount: number + priority: 'HIGH' | 'MEDIUM' | 'LOW' + assignedCount?: number +} + +/** 工作组成员类型 */ +export interface WorkGroupMember { + id: string + name: string + role: string + avatar?: string + phone?: string + email?: string + workTypes?: string[] + joinTime?: string +} + +/** 工作组查询参数 */ +export interface WorkGroupQuery { + projectId?: string + name?: string + status?: string + workType?: string +} + +/** 工作组分页查询参数 */ +export interface WorkGroupPageQuery extends WorkGroupQuery, PageQuery {} + +/** 创建工作组请求参数 */ +export interface CreateWorkGroupReq { + name: string + description?: string + projectId: string + workTypeGroups: WorkTypeGroup[] +} + +/** 更新工作组请求参数 */ +export interface UpdateWorkGroupReq { + id: string + name?: string + description?: string + status?: string + workTypeGroups?: WorkTypeGroup[] +} + +/** 分配工人到工作组请求参数 */ +export interface AssignWorkerReq { + workGroupId: string + workerIds: string[] + workTypeName: string +} + +/** 从工作组移除工人请求参数 */ +export interface RemoveWorkerReq { + workGroupId: string + workerId: string + workTypeName: string +} + +// 扩展项目类型,包含工作组信息 +export interface ProjectWithWorkGroups extends ProjectResp { + workGroups?: WorkGroup[] } \ No newline at end of file diff --git a/src/apis/project/workGroup.ts b/src/apis/project/workGroup.ts new file mode 100644 index 0000000..3a6985f --- /dev/null +++ b/src/apis/project/workGroup.ts @@ -0,0 +1,81 @@ +import request from '@/utils/http' +import type { + WorkGroup, + WorkTypeGroup, + WorkGroupPageQuery, + CreateWorkGroupReq, + UpdateWorkGroupReq, + AssignWorkerReq, + RemoveWorkerReq, + PageRes +} from './type' + +/** + * 获取工作组列表 + */ +export function getWorkGroupList(params: WorkGroupPageQuery) { + return request.get>('/api/work-groups', params) +} + +/** + * 根据项目ID获取工作组列表 + */ +export function getWorkGroupsByProject(projectId: string) { + return request.get(`/api/projects/${projectId}/work-groups`) +} + +/** + * 获取工作组详情 + */ +export function getWorkGroupDetail(id: string) { + return request.get(`/api/work-groups/${id}`) +} + +/** + * 创建工作组 + */ +export function createWorkGroup(data: CreateWorkGroupReq) { + return request.post('/api/work-groups', data) +} + +/** + * 更新工作组 + */ +export function updateWorkGroup(id: string, data: UpdateWorkGroupReq) { + return request.put(`/api/work-groups/${id}`, data) +} + +/** + * 删除工作组 + */ +export function deleteWorkGroup(id: string) { + return request.del(`/api/work-groups/${id}`) +} + +/** + * 分配工人到工作组 + */ +export function assignWorkerToGroup(data: AssignWorkerReq) { + return request.post('/api/work-groups/assign-worker', data) +} + +/** + * 从工作组移除工人 + */ +export function removeWorkerFromGroup(data: RemoveWorkerReq) { + return request.post('/api/work-groups/remove-worker', data) +} + +/** + * 更新工作组状态 + */ +export function updateWorkGroupStatus(id: string, status: string) { + return request.patch(`/api/work-groups/${id}/status`, { status }) +} + +/** + * 获取工作组统计信息 + */ +export function getWorkGroupStats(projectId?: string) { + return request.get('/api/work-groups/stats', { projectId }) +} diff --git a/src/views/project-management/projects/personnel-organization/index.vue b/src/views/project-management/projects/personnel-organization/index.vue index ae2284d..6a42055 100644 --- a/src/views/project-management/projects/personnel-organization/index.vue +++ b/src/views/project-management/projects/personnel-organization/index.vue @@ -1,5 +1,5 @@ @@ -436,13 +526,57 @@ import { IconCheck, IconFilter, IconClose, - IconSave + IconSave, + IconEye } from '@arco-design/web-vue/es/icon' import type { ProjectResp, ConstructorResp, - CertificationResp + CertificationResp, + WorkGroup, + WorkTypeGroup, + WorkGroupMember, + ProjectWithWorkGroups } from '@/apis/project/type' +import { + getWorkGroupsByProject, + createWorkGroup as createWorkGroupAPI, + assignWorkerToGroup, + removeWorkerFromGroup +} from '@/apis/project/workGroup' + +// 移除重复的类型定义,使用导入的类型 +// interface WorkGroup { +// id: string +// name: string +// description: string +// projectId: string +// status: 'ACTIVE' | 'INACTIVE' | 'COMPLETED' +// workTypeGroups: WorkTypeGroup[] +// memberCount: number +// requiredCount: number +// members?: WorkGroupMember[] +// createTime: string +// } + +// interface WorkTypeGroup { +// name: string +// requiredCount: number +// priority: 'HIGH' | 'MEDIUM' | 'LOW' +// } + +// interface WorkGroupMember { +// id: string +// name: string +// role: string +// avatar?: string +// phone?: string +// } + +// 扩展项目类型,包含工作组信息 +// interface ProjectWithWorkGroups extends ProjectResp { +// workGroups?: WorkGroup[] +// } defineOptions({ name: 'PersonnelOrganization' }) @@ -450,11 +584,13 @@ defineOptions({ name: 'PersonnelOrganization' }) const currentStep = ref(1) // 项目数据 -const projectList = ref([]) -const selectedProject = ref(null) +const projectList = ref([]) +const selectedProject = ref(null) +const selectedProjectForDetail = ref(null) // 工作组表单 const workGroupForm = reactive({ + id: '', // 添加工作组ID字段 name: '', description: '', projectId: '', @@ -483,6 +619,7 @@ const workerAssignments = ref>({}) // 弹窗状态 const certificateModalVisible = ref(false) const selectedWorkerCertificates = ref([]) +const workGroupDetailModalVisible = ref(false) // 拖拽状态 const draggedWorker = ref(null) @@ -499,7 +636,7 @@ const getGroupMembers = (groupName: string) => { // 方法 const loadProjectList = async () => { try { - // 使用模拟数据 + // 使用模拟数据,包含工作组信息 projectList.value = [ { projectId: '1', @@ -507,7 +644,44 @@ const loadProjectList = async () => { projectName: '风电场A区建设项目', farmName: '风电场A区', status: 1, - statusLabel: '进行中' + statusLabel: '进行中', + workGroups: [ + { + id: 'wg1', + name: '高空作业组', + description: '负责塔上登高作业', + projectId: '1', + status: 'ACTIVE', + workTypeGroups: [ + { name: '塔上登高作业', requiredCount: 5, priority: 'HIGH' } + ], + memberCount: 3, + requiredCount: 5, + members: [ + { id: 'm1', name: '张登高', role: '组长', avatar: '' }, + { id: 'm2', name: '李高空', role: '组员', avatar: '' }, + { id: 'm3', name: '王高空', role: '组员', avatar: '' } + ], + createTime: '2024-01-15' + }, + { + id: 'wg2', + name: '地勤保障组', + description: '负责地面保障工作', + projectId: '1', + status: 'ACTIVE', + workTypeGroups: [ + { name: '地勤人员', requiredCount: 3, priority: 'MEDIUM' } + ], + memberCount: 2, + requiredCount: 3, + members: [ + { id: 'm4', name: '赵地勤', role: '组长', avatar: '' }, + { id: 'm5', name: '孙地勤', role: '组员', avatar: '' } + ], + createTime: '2024-01-16' + } + ] }, { projectId: '2', @@ -515,7 +689,29 @@ const loadProjectList = async () => { projectName: '风电场B区维护项目', farmName: '风电场B区', status: 2, - statusLabel: '已完成' + statusLabel: '已完成', + workGroups: [ + { + id: 'wg3', + name: '维护保养组', + description: '负责设备维护保养', + projectId: '2', + status: 'COMPLETED', + workTypeGroups: [ + { name: '机械工程师', requiredCount: 2, priority: 'HIGH' }, + { name: '电气工程师', requiredCount: 2, priority: 'HIGH' } + ], + memberCount: 4, + requiredCount: 4, + members: [ + { id: 'm6', name: '周机械', role: '机械工程师', avatar: '' }, + { id: 'm7', name: '吴电气', role: '电气工程师', avatar: '' }, + { id: 'm8', name: '郑机械', role: '机械工程师', avatar: '' }, + { id: 'm9', name: '王电气', role: '电气工程师', avatar: '' } + ], + createTime: '2024-01-10' + } + ] }, { projectId: '3', @@ -523,9 +719,21 @@ const loadProjectList = async () => { projectName: '风电场C区升级项目', farmName: '风电场C区', status: 0, - statusLabel: '准备中' + statusLabel: '准备中', + workGroups: [] } ] + + // 为每个项目加载工作组信息 + for (const project of projectList.value) { + try { + const workGroups = await getWorkGroupsByProject(project.projectId) + project.workGroups = workGroups + } catch (error) { + console.warn(`加载项目 ${project.projectName} 的工作组失败:`, error) + // 保持模拟数据作为后备 + } + } } catch (error) { Message.error('加载项目列表失败') } @@ -662,11 +870,11 @@ const loadWorkers = async () => { } } -const selectProject = (project: ProjectResp) => { +const selectProject = (project: ProjectWithWorkGroups) => { selectedProject.value = project } -const enterWorkGroupCreation = (project: ProjectResp) => { +const enterWorkGroupCreation = (project: ProjectWithWorkGroups) => { selectedProject.value = project workGroupForm.projectId = project.projectId currentStep.value = 2 @@ -696,7 +904,7 @@ const goBackToStep2 = () => { currentStep.value = 2 } -const createWorkGroup = () => { +const createWorkGroup = async () => { if (!workGroupForm.name || !workGroupForm.projectId) { Message.warning('请填写工作组名称和关联项目') return @@ -708,11 +916,38 @@ const createWorkGroup = () => { return } - currentStep.value = 3 - loadWorkers() + try { + // 调用API创建工作组 + const newWorkGroup = await createWorkGroupAPI({ + name: workGroupForm.name, + description: workGroupForm.description, + projectId: workGroupForm.projectId, + workTypeGroups: workGroupForm.workTypeGroups + }) + + Message.success('工作组创建成功') + + // 保存新创建的工作组ID + workGroupForm.id = newWorkGroup.id + + // 更新项目的工作组列表 + if (selectedProject.value) { + if (!selectedProject.value.workGroups) { + selectedProject.value.workGroups = [] + } + selectedProject.value.workGroups.push(newWorkGroup) + } + + currentStep.value = 3 + loadWorkers() + } catch (error) { + Message.error('创建工作组失败') + console.error('创建工作组错误:', error) + } } const resetWorkGroupForm = () => { + workGroupForm.id = '' // 重置工作组ID workGroupForm.name = '' workGroupForm.description = '' workGroupForm.projectId = '' @@ -797,16 +1032,34 @@ const handleDrop = (event: DragEvent, group: any) => { Message.warning('该工种分组人数已满') return } - + workerAssignments.value[groupName].push(draggedWorker.value) Message.success(`已将 ${draggedWorker.value.name} 分配到 ${groupName}`) } -const removeFromGroup = (member: ConstructorResp, groupName: string) => { - const index = workerAssignments.value[groupName].findIndex(m => m.id === member.id) - if (index > -1) { - workerAssignments.value[groupName].splice(index, 1) +const removeFromGroup = async (member: ConstructorResp, groupName: string) => { + try { + if (!workGroupForm.id) { + Message.error('工作组ID不存在') + return + } + + await removeWorkerFromGroup({ + workGroupId: workGroupForm.id, + workerId: member.id, + workTypeName: groupName + }) + + // 从本地分配数据中移除 + const index = workerAssignments.value[groupName].findIndex(m => m.id === member.id) + if (index > -1) { + workerAssignments.value[groupName].splice(index, 1) + } + Message.success(`已从 ${groupName} 移除 ${member.name}`) + } catch (error) { + Message.error('移除工人失败') + console.error('移除工人错误:', error) } } @@ -831,14 +1084,51 @@ const saveAssignment = async () => { Message.warning('请至少分配一名工人') return } + + if (!workGroupForm.id) { + Message.error('工作组ID不存在') + return + } + + // 调用API保存分配结果 + const assignmentPromises = [] + for (const [workTypeName, workers] of Object.entries(workerAssignments.value)) { + if (workers.length > 0) { + const workerIds = workers.map(w => w.id) + assignmentPromises.push( + assignWorkerToGroup({ + workGroupId: workGroupForm.id, + workerIds, + workTypeName + }) + ) + } + } + + await Promise.all(assignmentPromises) Message.success('分配保存成功') - // 这里可以调用API保存分配结果 + + // 刷新项目的工作组信息 + if (selectedProject.value) { + try { + const workGroups = await getWorkGroupsByProject(selectedProject.value.projectId) + selectedProject.value.workGroups = workGroups + } catch (error) { + console.warn('刷新工作组信息失败:', error) + } + } } catch (error) { Message.error('保存分配失败') + console.error('保存分配错误:', error) } } +const viewProjectWorkGroups = (project: ProjectWithWorkGroups) => { + selectedProjectForDetail.value = project + workGroupDetailModalVisible.value = true +} + // 工具方法 const getStatusColor = (status: number) => { const statusMap: Record = { @@ -885,9 +1175,27 @@ const getCertificateStatusText = (status: string) => { return statusMap[status] || status } +const getWorkGroupStatusColor = (status: string) => { + const statusMap: Record = { + 'ACTIVE': 'green', + 'INACTIVE': 'red', + 'COMPLETED': 'gray' + } + return statusMap[status] || 'gray' +} + +const getWorkGroupStatusText = (status: string) => { + const statusMap: Record = { + 'ACTIVE': '进行中', + 'INACTIVE': '已暂停', + 'COMPLETED': '已完成' + } + return statusMap[status] || status +} + // 生命周期 onMounted(async () => { - await loadProjectList() + await loadProjectList() }) @@ -896,16 +1204,7 @@ onMounted(async () => { padding: 20px; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); min-height: 100vh; - overflow-y: auto; - height: auto; - position: relative; - z-index: 1; - box-sizing: border-box; -} - -// 确保页面可以滚动 -:deep(.gi-page-layout__body) { - overflow: auto !important; + overflow-y: auto; // 添加垂直滚动 } // 流程导航样式 @@ -918,6 +1217,7 @@ onMounted(async () => { background: white; border-radius: 16px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + flex-shrink: 0; // 防止压缩 } .flow-step { @@ -958,8 +1258,7 @@ onMounted(async () => { .step-content { max-width: 1200px; margin: 0 auto; - overflow-y: auto; - height: auto; + margin-bottom: 30px; // 添加底部间距 } // 项目选择卡片 @@ -1023,7 +1322,7 @@ onMounted(async () => { color: white; font-size: 20px; } - + .project-info { margin-bottom: 20px; @@ -1033,13 +1332,13 @@ onMounted(async () => { font-weight: 600; color: #1d2129; } - + .project-code { margin: 0 0 4px 0; font-size: 14px; color: #86909c; } - + .project-farm { margin: 0; font-size: 14px; @@ -1047,8 +1346,49 @@ onMounted(async () => { } } +.project-work-groups { + margin-top: 20px; + padding-top: 16px; + border-top: 1px solid #e9ecef; + + .work-groups-header { + display: flex; + align-items: center; + gap: 8px; + font-size: 16px; + font-weight: 600; + color: #1d2129; + margin-bottom: 12px; + } + + .work-groups-list { + display: flex; + flex-wrap: wrap; + gap: 12px; + } + + .work-group-preview { + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 8px 12px; + font-size: 14px; + color: #1d2129; + display: flex; + align-items: center; + gap: 8px; + } + + .more-work-groups { + font-size: 14px; + color: #86909c; + font-weight: 500; + } +} + .project-actions { text-align: center; + margin-top: 20px; } // 工作组创建表单 @@ -1103,19 +1443,17 @@ onMounted(async () => { display: grid; grid-template-columns: 280px 1fr 320px; gap: 24px; - min-height: calc(100vh - 200px); - height: auto; + min-height: 600px; // 设置最小高度而不是固定高度 } .filter-sidebar, .work-groups-sidebar { .filter-card, .work-groups-card { - min-height: 100%; - height: auto; + height: 100%; border-radius: 16px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); - + .card-title { display: flex; align-items: center; @@ -1134,8 +1472,7 @@ onMounted(async () => { .worker-table-section { .worker-table-card { - min-height: 100%; - height: auto; + height: 100%; border-radius: 16px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); @@ -1145,7 +1482,7 @@ onMounted(async () => { gap: 8px; font-size: 16px; font-weight: 600; - + .worker-count { color: #86909c; font-weight: 400; @@ -1155,9 +1492,8 @@ onMounted(async () => { } .worker-table { - min-height: calc(100vh - 300px); - max-height: calc(100vh - 300px); - overflow-y: auto; + max-height: 500px; // 设置最大高度 + overflow-y: auto; // 添加垂直滚动 padding: 16px; } @@ -1230,7 +1566,7 @@ onMounted(async () => { justify-content: space-between; align-items: center; margin-bottom: 8px; - + .label { font-size: 12px; color: #86909c; @@ -1254,12 +1590,11 @@ onMounted(async () => { // 工种分组 .work-groups-content { - min-height: calc(100vh - 300px); - max-height: calc(100vh - 300px); - overflow-y: auto; + max-height: 400px; // 设置最大高度 + overflow-y: auto; // 添加垂直滚动 padding: 16px; } - + .work-group-drop-zone { background: white; border: 2px dashed #d9d9d9; @@ -1267,26 +1602,26 @@ onMounted(async () => { padding: 16px; margin-bottom: 16px; transition: all 0.3s ease; - + &:hover { border-color: #667eea; background: #f8f9ff; } } - + .work-group-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; - + h4 { margin: 0; font-size: 16px; font-weight: 600; color: #1d2129; } - + .work-group-stats { font-size: 14px; font-weight: 600; @@ -1299,7 +1634,7 @@ onMounted(async () => { margin: 0 4px; color: #86909c; } - + .required { color: #1890ff; } @@ -1318,7 +1653,7 @@ onMounted(async () => { background: #f8f9fa; border-radius: 8px; margin-bottom: 8px; - + .member-name { flex: 1; font-size: 14px; @@ -1382,4 +1717,155 @@ onMounted(async () => { color: #4e5969; } } + +// 工作组详情弹窗 +.work-group-detail { + .project-info-section { + margin-bottom: 20px; + padding-bottom: 16px; + border-bottom: 1px solid #e9ecef; + + h3 { + margin: 0 0 8px 0; + font-size: 18px; + font-weight: 600; + color: #1d2129; + } + + .project-code { + font-size: 14px; + color: #86909c; + } + } + + .work-groups-section { + h4 { + margin: 0 0 16px 0; + font-size: 16px; + font-weight: 600; + color: #1d2129; + } + + .work-groups-detail-list { + .work-group-detail-item { + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 16px; + margin-bottom: 16px; + + .work-group-detail-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + + h5 { + margin: 0; + font-size: 15px; + font-weight: 600; + color: #1d2129; + } + + .work-group-detail-stats { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: #86909c; + + .member-count { + color: #52c41a; + } + } + } + + .work-group-detail-members { + .members-header { + margin-bottom: 12px; + font-size: 14px; + color: #1d2129; + font-weight: 500; + } + + .members-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); + gap: 12px; + } + + .member-detail-item { + display: flex; + align-items: center; + gap: 12px; + + .member-detail-info { + .member-name { + font-size: 14px; + color: #1d2129; + font-weight: 600; + } + .member-role { + font-size: 12px; + color: #86909c; + } + } + } + } + + .work-group-detail-description { + p { + margin: 0; + font-size: 13px; + color: #4e5969; + } + } + } + } + } +} + +// 响应式设计 +@media (max-width: 1200px) { + .worker-assignment-layout { + grid-template-columns: 250px 1fr 280px; + gap: 16px; + } +} + +@media (max-width: 992px) { + .worker-assignment-layout { + grid-template-columns: 1fr; + gap: 16px; + } + + .filter-sidebar, + .work-groups-sidebar { + order: 2; + } + + .worker-table-section { + order: 1; + } +} + +@media (max-width: 768px) { + .personnel-organization-container { + padding: 16px; + } + + .project-grid { + grid-template-columns: 1fr; + gap: 16px; + } + + .process-flow { + flex-direction: column; + gap: 16px; + + .flow-arrow { + transform: rotate(90deg); + } + } +}