This commit is contained in:
crushing1111 2025-08-15 10:03:26 +08:00
commit 66d4a1bbfe
9 changed files with 3828 additions and 425 deletions

View File

@ -0,0 +1,54 @@
import type * as T from './type'
import http from '@/utils/http'
const BASE_URL = '/project/personnel-organization'
/** @desc 获取项目列表 */
export function getProjectList(query?: T.ProjectQuery) {
return http.get<PageRes<T.ProjectResp[]>>('/project/list', query)
}
/** @desc 获取项目人员组织信息 */
export function getPersonnelOrganization(projectId: string | number) {
return http.get<T.PersonnelOrganizationResp>(`${BASE_URL}/${projectId}`)
}
/** @desc 获取工种列表 */
export function getWorkTypeList() {
return http.get<T.WorkTypeResp[]>('/work-type/list')
}
/** @desc 获取施工人员列表 */
export function getConstructorList(query: T.ConstructorQuery) {
return http.get<PageRes<T.ConstructorResp[]>>('/constructor/list', query)
}
/** @desc 获取施工人员技能证书 */
export function getConstructorCertifications(constructorId: string | number) {
return http.get<T.CertificationResp[]>(`/constructor/${constructorId}/certifications`)
}
/** @desc 分配施工人员到项目 */
export function assignConstructorToProject(data: T.AssignConstructorReq) {
return http.post(`${BASE_URL}/assign`, data)
}
/** @desc 移除项目施工人员 */
export function removeConstructorFromProject(data: T.RemoveConstructorReq) {
return http.delete(`${BASE_URL}/remove`, data)
}
/** @desc 更新施工人员分配信息 */
export function updateConstructorAssignment(data: T.UpdateAssignmentReq) {
return http.put(`${BASE_URL}/update`, data)
}
/** @desc 获取项目人员统计 */
export function getPersonnelStats(projectId: string | number) {
return http.get<T.PersonnelStatsResp>(`${BASE_URL}/${projectId}/stats`)
}
/** @desc 导出项目人员组织 */
export function exportPersonnelOrganization(projectId: string | number) {
return http.download(`${BASE_URL}/${projectId}/export`)
}

View File

@ -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[]
@ -327,7 +407,137 @@ export interface UpdateRequirementStatusForm {
status: 'pending' | 'recruiting' | 'completed'
}
// ==================== 人员组织相关类型 ====================
/** 工种响应 */
export interface WorkTypeResp {
id: string | number
name: string
code: string
description?: string
requiredCertifications?: string[]
sort?: number
}
/** 施工人员响应 */
export interface ConstructorResp {
id: string | number
name: string
phone: string
email?: string
avatar?: string
workTypes: string[] // 工种列表
certifications: CertificationResp[]
experience: number // 工作经验(年)
status: 'ACTIVE' | 'INACTIVE' | 'BUSY'
rating: number // 评分
joinDate: string
remark?: string
}
/** 证书响应 */
export interface CertificationResp {
id: string | number
name: string
type: string
code: string
issueDate: string
expiryDate: string
issuingAuthority: string
status: 'VALID' | 'EXPIRED' | 'EXPIRING_SOON'
imageUrl?: string
}
/** 施工人员查询参数 */
export interface ConstructorQuery extends PageQuery {
name?: string
workType?: string
status?: string
hasCertification?: boolean
experienceMin?: number
experienceMax?: number
}
/** 人员组织响应 */
export interface PersonnelOrganizationResp {
projectId: string | number
projectName: string
workTypeAssignments: WorkTypeAssignmentResp[]
totalPersonnel: number
assignedPersonnel: number
availablePersonnel: number
}
/** 工种分配响应 */
export interface WorkTypeAssignmentResp {
workTypeId: string | number
workTypeName: string
workTypeCode: string
requiredCount: number
assignedCount: number
constructors: ConstructorAssignmentResp[]
}
/** 施工人员分配响应 */
export interface ConstructorAssignmentResp {
constructorId: string | number
constructorName: string
phone: string
avatar?: string
workTypes: string[]
certifications: CertificationResp[]
experience: number
rating: number
assignmentDate: string
status: 'ASSIGNED' | 'WORKING' | 'COMPLETED'
remark?: string
}
/** 分配施工人员请求 */
export interface AssignConstructorReq {
projectId: string | number
workTypeId: string | number
constructorIds: (string | number)[]
assignmentDate?: string
remark?: string
}
/** 移除施工人员请求 */
export interface RemoveConstructorReq {
projectId: string | number
constructorIds: (string | number)[]
reason?: string
}
/** 更新分配信息请求 */
export interface UpdateAssignmentReq {
projectId: string | number
constructorId: string | number
workTypeId?: string | number
status?: string
remark?: string
}
/** 人员统计响应 */
export interface PersonnelStatsResp {
totalPersonnel: number
assignedPersonnel: number
availablePersonnel: number
workTypeStats: {
workTypeName: string
requiredCount: number
assignedCount: number
shortageCount: number
}[]
certificationStats: {
certificationType: string
count: number
}[]
experienceStats: {
range: string
count: number
}[]
}
/** 分页查询基础参数 */
export interface PageQuery {
@ -341,4 +551,89 @@ export interface PageRes<T> {
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[]
}

View File

@ -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<PageRes<WorkGroup>>('/api/work-groups', params)
}
/**
* ID获取工作组列表
*/
export function getWorkGroupsByProject(projectId: string) {
return request.get<WorkGroup[]>(`/api/projects/${projectId}/work-groups`)
}
/**
*
*/
export function getWorkGroupDetail(id: string) {
return request.get<WorkGroup>(`/api/work-groups/${id}`)
}
/**
*
*/
export function createWorkGroup(data: CreateWorkGroupReq) {
return request.post<WorkGroup>('/api/work-groups', data)
}
/**
*
*/
export function updateWorkGroup(id: string, data: UpdateWorkGroupReq) {
return request.put<WorkGroup>(`/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<WorkGroup>(`/api/work-groups/${id}/status`, { status })
}
/**
*
*/
export function getWorkGroupStats(projectId?: string) {
return request.get('/api/work-groups/stats', { projectId })
}

View File

@ -63,3 +63,8 @@ export function resetUserPwd(data: any, id: string) {
export function updateUserRole(data: { roleIds: string[] }, id: string) {
return http.patch(`${BASE_URL}/${id}/role`, data)
}
/** @desc 按姓名模糊搜索用户 */
export function searchUserByName(name: string) {
return http.get<T.UserNewResp[]>(`${BASE_URL}/searchByName`, { name })
}

View File

@ -816,7 +816,17 @@ export const systemRoutes: RouteRecordRaw[] = [
hidden: false,
},
},
{
path: '/project-management/projects/personnel-organization',
name: 'PersonnelOrganization',
component: () => import('@/views/project-management/projects/personnel-organization/index.vue'),
meta: {
title: '人员组织',
icon: 'user-group',
hidden: false,
},
},
{
path: '/project-management/projects/device',
name: 'DeviceManagement',

View File

@ -180,7 +180,17 @@
<div class="form-row">
<div class="form-item">
<label>姓名 <span class="required">*</span></label>
<a-input v-model="memberForm.name" placeholder="请输入姓名" />
<a-auto-complete
v-model="memberForm.name"
placeholder="请输入姓名进行搜索"
:data="userSearchOptions"
:loading="userSearchLoading"
@search="handleUserSearch"
@select="handleUserSelect"
allow-clear
:filter-option="false"
:trigger-on-focus="false"
/>
</div>
<div class="form-item">
<label>联系电话 <span class="required">*</span></label>
@ -304,7 +314,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import { useRoute } from 'vue-router'
import type { TeamMemberResp, TeamMemberQuery, TeamMemberExportQuery, CreateTeamMemberForm, UpdateTeamMemberForm, BackendTeamMemberResp } from '@/apis/project/type'
@ -317,6 +327,8 @@ import {
exportTeamMembers,
downloadImportTemplate
} from '@/apis/project/personnel-dispatch'
import { searchUserByName } from '@/apis/system/user'
import type { UserNewResp } from '@/apis/system/type'
//
const route = useRoute()
@ -411,6 +423,12 @@ const statusForm = reactive<{
const fileList = ref<any[]>([])
//
const userSearchResults = ref<UserNewResp[]>([])
const userSearchOptions = ref<Array<{ label: string; value: string; user: UserNewResp }>>([])
const userSearchLoading = ref(false)
const searchTimeout = ref<NodeJS.Timeout | null>(null)
//
const loadData = async () => {
if (!projectId) {
@ -505,6 +523,70 @@ const openAddModal = () => {
memberModalVisible.value = true
}
//
const handleUserSearch = async (value: string) => {
console.log('开始搜索用户,输入值:', value)
//
if (searchTimeout.value) {
clearTimeout(searchTimeout.value)
}
//
if (!value || value.trim().length < 1) {
userSearchResults.value = []
console.log('输入为空,清空搜索结果')
return
}
// 300ms
searchTimeout.value = setTimeout(async () => {
console.log('执行搜索,搜索值:', value.trim())
userSearchLoading.value = true
try {
const response = await searchUserByName(value.trim())
console.log('API响应完整数据:', response)
// 使 response.rows response.data
const users = response.rows || []
userSearchResults.value = users
// a-auto-complete
userSearchOptions.value = users.map(user => ({
label: `${user.name} | ${user.deptName || '未分配部门'}`,
value: user.name,
user: user
}))
console.log('设置搜索结果:', userSearchResults.value)
console.log('设置搜索选项:', userSearchOptions.value)
} catch (error) {
console.error('搜索用户失败:', error)
userSearchResults.value = []
userSearchOptions.value = []
Message.error('搜索用户失败')
} finally {
userSearchLoading.value = false
}
}, 300)
}
//
const handleUserSelect = (value: string, option: any) => {
const selectedOption = userSearchOptions.value.find(option => option.value === value)
if (selectedOption) {
const selectedUser = selectedOption.user
//
memberForm.name = selectedUser.name
memberForm.phone = selectedUser.mobile || ''
// email
// memberForm.email = selectedUser.email || ''
//
userSearchResults.value = []
userSearchOptions.value = []
console.log('选择的用户:', selectedUser)
}
}
const openEditModal = (record: TeamMemberResp) => {
isEdit.value = true
Object.assign(memberForm, {
@ -568,6 +650,9 @@ const saveMember = async () => {
const cancelMember = () => {
memberModalVisible.value = false
resetMemberForm()
//
userSearchResults.value = []
userSearchOptions.value = []
}
const resetMemberForm = () => {
@ -582,6 +667,9 @@ const resetMemberForm = () => {
joinDate: '',
remark: ''
})
//
userSearchResults.value = []
userSearchOptions.value = []
}
const openStatusModal = (record: TeamMemberResp) => {
@ -715,11 +803,11 @@ const getRoleTypeText = (roleType: string) => {
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
ACTIVE: 'success',
SUSPENDEN: 'warning',
INACTIVE: 'danger'
ACTIVE: '#52c41a', // 绿 -
SUSPENDEN: '#faad14', // -
INACTIVE: '#ff4d4f' // - 线
}
return colorMap[status] || 'danger'
return colorMap[status] || '#ff4d4f'
}
const getStatusText = (status: string) => {
@ -741,6 +829,13 @@ onMounted(() => {
Message.warning('未获取到项目信息,请从项目详情页面进入')
}
})
onUnmounted(() => {
//
if (searchTimeout.value) {
clearTimeout(searchTimeout.value)
}
})
</script>
<style lang="scss" scoped>
@ -1307,4 +1402,24 @@ onMounted(() => {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
//
.user-option {
.user-name {
font-weight: 500;
color: var(--color-text-1);
}
}
//
:deep(.arco-auto-complete) {
.arco-input {
transition: all 0.3s ease;
&:focus {
border-color: var(--color-primary-6);
box-shadow: 0 0 0 2px rgba(var(--color-primary-6), 0.1);
}
}
}
</style>

View File

@ -79,7 +79,7 @@
<icon-user class="detail-icon" />
<span>负责人: {{ project.manager || '未指定' }}</span>
</div>
<div class="detail-item">
<div class="detail-item clickable" @click.stop="openPersonnelManagement(project)">
<icon-user-group class="detail-icon" />
<span>团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}</span>
</div>
@ -215,7 +215,7 @@
<icon-user class="detail-icon" />
<span>负责人: {{ project.manager || '未指定' }}</span>
</div>
<div class="detail-item">
<div class="detail-item clickable" @click.stop="openPersonnelManagement(project)">
<icon-user-group class="detail-icon" />
<span>团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}</span>
</div>
@ -351,7 +351,7 @@
<icon-user class="detail-icon" />
<span>负责人: {{ project.manager || '未指定' }}</span>
</div>
<div class="detail-item">
<div class="detail-item clickable" @click.stop="openPersonnelManagement(project)">
<icon-user-group class="detail-icon" />
<span>团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}</span>
</div>
@ -487,7 +487,7 @@
<icon-user class="detail-icon" />
<span>负责人: {{ project.manager || '未指定' }}</span>
</div>
<div class="detail-item">
<div class="detail-item clickable" @click.stop="openPersonnelManagement(project)">
<icon-user-group class="detail-icon" />
<span>团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}</span>
</div>
@ -623,7 +623,7 @@
<icon-user class="detail-icon" />
<span>负责人: {{ project.manager || '未指定' }}</span>
</div>
<div class="detail-item">
<div class="detail-item clickable" @click.stop="openPersonnelManagement(project)">
<icon-user-group class="detail-icon" />
<span>团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}</span>
</div>
@ -759,7 +759,7 @@
<icon-user class="detail-icon" />
<span>负责人: {{ project.manager || '未指定' }}</span>
</div>
<div class="detail-item">
<div class="detail-item clickable" @click.stop="openPersonnelManagement(project)">
<icon-user-group class="detail-icon" />
<span>团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}</span>
</div>
@ -895,7 +895,7 @@
<icon-user class="detail-icon" />
<span>负责人: {{ project.manager || '未指定' }}</span>
</div>
<div class="detail-item">
<div class="detail-item clickable" @click.stop="openPersonnelManagement(project)">
<icon-user-group class="detail-icon" />
<span>团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}</span>
</div>
@ -1031,7 +1031,7 @@
<icon-user class="detail-icon" />
<span>负责人: {{ project.manager || '未指定' }}</span>
</div>
<div class="detail-item">
<div class="detail-item clickable" @click.stop="openPersonnelManagement(project)">
<icon-user-group class="detail-icon" />
<span>团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}</span>
</div>
@ -1180,10 +1180,10 @@
<icon-user class="detail-icon" />
<span>负责人: {{ project.manager || '未指定' }}</span>
</div>
<div class="detail-item">
<icon-user-group class="detail-icon" />
<span>团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}</span>
</div>
<div class="detail-item clickable" @click.stop="openPersonnelManagement(project)">
<icon-user-group class="detail-icon" />
<span>团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}</span>
</div>
<div class="progress-section">
<div class="progress-info">
<span>项目进度</span>
@ -1327,7 +1327,7 @@
<icon-user class="detail-icon" />
<span>负责人: {{ project.manager || '未指定' }}</span>
</div>
<div class="detail-item">
<div class="detail-item clickable" @click.stop="openPersonnelManagement(project)">
<icon-user-group class="detail-icon" />
<span>团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}</span>
</div>
@ -1474,7 +1474,7 @@
<icon-user class="detail-icon" />
<span>负责人: {{ project.manager || '未指定' }}</span>
</div>
<div class="detail-item">
<div class="detail-item clickable" @click.stop="openPersonnelManagement(project)">
<icon-user-group class="detail-icon" />
<span>团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}</span>
</div>
@ -1621,7 +1621,7 @@
<icon-user class="detail-icon" />
<span>负责人: {{ project.manager || '未指定' }}</span>
</div>
<div class="detail-item">
<div class="detail-item clickable" @click.stop="openPersonnelManagement(project)">
<icon-user-group class="detail-icon" />
<span>团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}</span>
</div>
@ -1915,7 +1915,7 @@
<icon-user class="detail-icon" />
<span>负责人: {{ project.manager || '未指定' }}</span>
</div>
<div class="detail-item">
<div class="detail-item clickable" @click.stop="openPersonnelManagement(project)">
<icon-user-group class="detail-icon" />
<span>团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}</span>
</div>
@ -2062,7 +2062,7 @@
<icon-user class="detail-icon" />
<span>负责人: {{ project.manager || '未指定' }}</span>
</div>
<div class="detail-item">
<div class="detail-item clickable" @click.stop="openPersonnelManagement(project)">
<icon-user-group class="detail-icon" />
<span>团队: {{ project.teamSize > 0 ? project.teamSize + '人' : '未设置' }}</span>
</div>
@ -3041,11 +3041,11 @@ const openSiteManagement = (project: any) => {
}
//
const openPersonnelManagement = () => {
if (currentProject.value && currentProject.value.id) {
const openPersonnelManagement = (project: any) => {
if (project && project.id) {
router.push({
path: '/project-management/personnel-dispatch/construction-personnel',
query: { projectId: currentProject.value.id }
query: { projectId: project.id }
})
} else {
Message.error('项目信息不完整,无法进入团队成员管理')

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff