From ee5534d0520cab84a171eddae8a135f399d5c497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E8=AF=97=E6=95=8F?= <3130004661@qq.com> Date: Thu, 14 Aug 2025 20:23:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E5=AE=8C=E6=88=90=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E7=AE=A1=E7=90=86=E5=92=8C=E4=BA=BA=E5=91=98=E7=BB=84?= =?UTF-8?q?=E7=BB=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/project/personnel-organization.ts | 54 + src/apis/project/type.ts | 130 ++ src/router/route.ts | 12 +- src/types/components.d.ts | 63 + .../projects/personnel-organization/index.vue | 1385 +++++++++++++++++ .../projects/progress/index.vue | 86 +- 6 files changed, 1686 insertions(+), 44 deletions(-) create mode 100644 src/apis/project/personnel-organization.ts create mode 100644 src/views/project-management/projects/personnel-organization/index.vue diff --git a/src/apis/project/personnel-organization.ts b/src/apis/project/personnel-organization.ts new file mode 100644 index 0000000..0d07776 --- /dev/null +++ b/src/apis/project/personnel-organization.ts @@ -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>('/project/list', query) +} + +/** @desc 获取项目人员组织信息 */ +export function getPersonnelOrganization(projectId: string | number) { + return http.get(`${BASE_URL}/${projectId}`) +} + +/** @desc 获取工种列表 */ +export function getWorkTypeList() { + return http.get('/work-type/list') +} + +/** @desc 获取施工人员列表 */ +export function getConstructorList(query: T.ConstructorQuery) { + return http.get>('/constructor/list', query) +} + +/** @desc 获取施工人员技能证书 */ +export function getConstructorCertifications(constructorId: string | number) { + return http.get(`/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(`${BASE_URL}/${projectId}/stats`) +} + +/** @desc 导出项目人员组织 */ +export function exportPersonnelOrganization(projectId: string | number) { + return http.download(`${BASE_URL}/${projectId}/export`) +} diff --git a/src/apis/project/type.ts b/src/apis/project/type.ts index 5d5f527..ea6c210 100644 --- a/src/apis/project/type.ts +++ b/src/apis/project/type.ts @@ -327,7 +327,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 { diff --git a/src/router/route.ts b/src/router/route.ts index 05536b6..1156b79 100644 --- a/src/router/route.ts +++ b/src/router/route.ts @@ -804,7 +804,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', diff --git a/src/types/components.d.ts b/src/types/components.d.ts index 7fa6b1b..a427ece 100644 --- a/src/types/components.d.ts +++ b/src/types/components.d.ts @@ -7,7 +7,70 @@ export {} declare module 'vue' { export interface GlobalComponents { + ApprovalAssistant: typeof import('./../components/ApprovalAssistant/index.vue')['default'] + ApprovalMessageItem: typeof import('./../components/NotificationCenter/ApprovalMessageItem.vue')['default'] + Avatar: typeof import('./../components/Avatar/index.vue')['default'] + Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default'] + CellCopy: typeof import('./../components/CellCopy/index.vue')['default'] + Chart: typeof import('./../components/Chart/index.vue')['default'] + CircularProgress: typeof import('./../components/CircularProgress/index.vue')['default'] + ColumnSetting: typeof import('./../components/GiTable/src/components/ColumnSetting.vue')['default'] + CronForm: typeof import('./../components/GenCron/CronForm/index.vue')['default'] + CronModal: typeof import('./../components/GenCron/CronModal/index.vue')['default'] + DateRangePicker: typeof import('./../components/DateRangePicker/index.vue')['default'] + DayForm: typeof import('./../components/GenCron/CronForm/component/day-form.vue')['default'] + FilePreview: typeof import('./../components/FilePreview/index.vue')['default'] + GiCellAvatar: typeof import('./../components/GiCell/GiCellAvatar.vue')['default'] + GiCellGender: typeof import('./../components/GiCell/GiCellGender.vue')['default'] + GiCellStatus: typeof import('./../components/GiCell/GiCellStatus.vue')['default'] + GiCellTag: typeof import('./../components/GiCell/GiCellTag.vue')['default'] + GiCellTags: typeof import('./../components/GiCell/GiCellTags.vue')['default'] + GiCodeView: typeof import('./../components/GiCodeView/index.vue')['default'] + GiDot: typeof import('./../components/GiDot/index.tsx')['default'] + GiEditTable: typeof import('./../components/GiEditTable/GiEditTable.vue')['default'] + GiFooter: typeof import('./../components/GiFooter/index.vue')['default'] + GiForm: typeof import('./../components/GiForm/src/GiForm.vue')['default'] + GiIconBox: typeof import('./../components/GiIconBox/index.vue')['default'] + GiIconSelector: typeof import('./../components/GiIconSelector/index.vue')['default'] + GiIframe: typeof import('./../components/GiIframe/index.vue')['default'] + GiOption: typeof import('./../components/GiOption/index.vue')['default'] + GiOptionItem: typeof import('./../components/GiOptionItem/index.vue')['default'] + GiPageLayout: typeof import('./../components/GiPageLayout/index.vue')['default'] + GiSpace: typeof import('./../components/GiSpace/index.vue')['default'] + GiSplitButton: typeof import('./../components/GiSplitButton/index.vue')['default'] + GiSplitPane: typeof import('./../components/GiSplitPane/index.vue')['default'] + GiSplitPaneFlexibleBox: typeof import('./../components/GiSplitPane/components/GiSplitPaneFlexibleBox.vue')['default'] + GiSvgIcon: typeof import('./../components/GiSvgIcon/index.vue')['default'] + GiTable: typeof import('./../components/GiTable/src/GiTable.vue')['default'] + GiTag: typeof import('./../components/GiTag/index.tsx')['default'] + GiThemeBtn: typeof import('./../components/GiThemeBtn/index.vue')['default'] + HourForm: typeof import('./../components/GenCron/CronForm/component/hour-form.vue')['default'] + Icon403: typeof import('./../components/icons/Icon403.vue')['default'] + Icon404: typeof import('./../components/icons/Icon404.vue')['default'] + Icon500: typeof import('./../components/icons/Icon500.vue')['default'] + IconBorders: typeof import('./../components/icons/IconBorders.vue')['default'] + IconTableSize: typeof import('./../components/icons/IconTableSize.vue')['default'] + IconTreeAdd: typeof import('./../components/icons/IconTreeAdd.vue')['default'] + IconTreeReduce: typeof import('./../components/icons/IconTreeReduce.vue')['default'] + ImageImport: typeof import('./../components/ImageImport/index.vue')['default'] + ImageImportWizard: typeof import('./../components/ImageImportWizard/index.vue')['default'] + IndustrialImageList: typeof import('./../components/IndustrialImageList/index.vue')['default'] + JsonPretty: typeof import('./../components/JsonPretty/index.vue')['default'] + MinuteForm: typeof import('./../components/GenCron/CronForm/component/minute-form.vue')['default'] + MonthForm: typeof import('./../components/GenCron/CronForm/component/month-form.vue')['default'] + NotificationCenter: typeof import('./../components/NotificationCenter/index.vue')['default'] + ParentView: typeof import('./../components/ParentView/index.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] + SecondForm: typeof import('./../components/GenCron/CronForm/component/second-form.vue')['default'] + SplitPanel: typeof import('./../components/SplitPanel/index.vue')['default'] + TextCopy: typeof import('./../components/TextCopy/index.vue')['default'] + TurbineGrid: typeof import('./../components/TurbineGrid/index.vue')['default'] + UserSelect: typeof import('./../components/UserSelect/index.vue')['default'] + Verify: typeof import('./../components/Verify/index.vue')['default'] + VerifyPoints: typeof import('./../components/Verify/Verify/VerifyPoints.vue')['default'] + VerifySlide: typeof import('./../components/Verify/Verify/VerifySlide.vue')['default'] + WeekForm: typeof import('./../components/GenCron/CronForm/component/week-form.vue')['default'] + YearForm: typeof import('./../components/GenCron/CronForm/component/year-form.vue')['default'] } } diff --git a/src/views/project-management/projects/personnel-organization/index.vue b/src/views/project-management/projects/personnel-organization/index.vue new file mode 100644 index 0000000..ae2284d --- /dev/null +++ b/src/views/project-management/projects/personnel-organization/index.vue @@ -0,0 +1,1385 @@ + + + + + diff --git a/src/views/project-management/projects/progress/index.vue b/src/views/project-management/projects/progress/index.vue index ae5a12d..153b22b 100644 --- a/src/views/project-management/projects/progress/index.vue +++ b/src/views/project-management/projects/progress/index.vue @@ -49,9 +49,9 @@ 📅 {{ formatShortDate(project.endDate) }} - +
@@ -68,9 +68,9 @@

{{ selectedProject.projectName }}

- - {{ getStatusText(selectedProject.status) }} - + + {{ getStatusText(selectedProject.status) }} + {{ selectedProject.projectCode }}
@@ -626,37 +626,37 @@ const availableWeeks = ref([ // 时间轴周数 - 简化版本 const timelineWeeks = computed(() => { try { - const weeks = [] + const weeks = [] const startDate = new Date('2025-01-06') - - for (let i = 0; i < 4; i++) { + + for (let i = 0; i < 4; i++) { const weekStart = dayjs.add(startDate, i * 7, 'day') - const days = [] - - for (let j = 0; j < 7; j++) { + const days = [] + + for (let j = 0; j < 7; j++) { const currentDate = dayjs.add(weekStart, j, 'day') const dayNumber = currentDate.getDate() const weekday = ['日', '一', '二', '三', '四', '五', '六'][currentDate.getDay()] const isWeekend = currentDate.getDay() === 0 || currentDate.getDay() === 6 const isToday = dayjs.isSame(currentDate, new Date(), 'day') - - days.push({ - date: dayjs.format(currentDate, 'YYYY-MM-DD'), - dayNumber, - weekday, - isWeekend, - isToday - }) - } - weeks.push({ - value: `week${i + 1}`, - label: `第${i + 1}周`, - days + days.push({ + date: dayjs.format(currentDate, 'YYYY-MM-DD'), + dayNumber, + weekday, + isWeekend, + isToday }) } - return weeks + weeks.push({ + value: `week${i + 1}`, + label: `第${i + 1}周`, + days + }) + } + + return weeks } catch (error) { console.error('计算时间轴出错:', error) return [] @@ -666,7 +666,7 @@ const timelineWeeks = computed(() => { // 计算属性 const getProjectCountByStatus = (status: string) => { try { - return projects.value.filter(p => getStatusKey(p.status) === status).length + return projects.value.filter(p => getStatusKey(p.status) === status).length } catch (error) { console.error('计算项目数量出错:', error) return 0 @@ -675,7 +675,7 @@ const getProjectCountByStatus = (status: string) => { const getProjectsByStatus = (status: string) => { try { - return projects.value.filter(p => getStatusKey(p.status) === status) + return projects.value.filter(p => getStatusKey(p.status) === status) } catch (error) { console.error('筛选项目出错:', error) return [] @@ -684,10 +684,10 @@ const getProjectsByStatus = (status: string) => { const getTotalWorkload = () => { try { - if (!selectedProject.value) return 0 - return projectTasks.value - .filter(task => !task.isSubTask) - .reduce((total, task) => total + (task.workDays || 0), 0) + if (!selectedProject.value) return 0 + return projectTasks.value + .filter(task => !task.isSubTask) + .reduce((total, task) => total + (task.workDays || 0), 0) } catch (error) { console.error('计算工作量出错:', error) return 0 @@ -796,8 +796,8 @@ const isTaskActiveOnDay = (task: any, date: string) => { const isTaskCompletedOnDay = (task: any, date: string) => { try { - if (task.progress >= 100) return true - + if (task.progress >= 100) return true + const taskStart = new Date(task.startDate) const taskEnd = new Date(task.endDate) const currentDate = new Date(date) @@ -805,10 +805,10 @@ const isTaskCompletedOnDay = (task: any, date: string) => { if (!dayjs.isBetween(currentDate, taskStart, taskEnd, 'day', '[]')) return false const totalDays = dayjs.diff(taskEnd, taskStart, 'day') + 1 - const completedDays = Math.floor((task.progress / 100) * totalDays) + const completedDays = Math.floor((task.progress / 100) * totalDays) const daysFromStart = dayjs.diff(currentDate, taskStart, 'day') + 1 - - return daysFromStart <= completedDays + + return daysFromStart <= completedDays } catch (error) { console.error('检查任务完成状态出错:', error) return false @@ -820,10 +820,10 @@ const getTaskBarStyle = (task: any, date: string) => { const taskStart = new Date(task.startDate) const currentDate = new Date(date) const daysFromStart = dayjs.diff(currentDate, taskStart, 'day') - - return { - left: `${daysFromStart * 40}px`, - width: '40px' + + return { + left: `${daysFromStart * 40}px`, + width: '40px' } } catch (error) { console.error('计算任务条样式出错:', error) @@ -840,9 +840,9 @@ onMounted(() => { console.log('项目数据:', projects.value) console.log('任务数据:', projectTasks.value) - // 默认选择第一个项目 - if (projects.value.length > 0) { - selectProject(projects.value[0]) + // 默认选择第一个项目 + if (projects.value.length > 0) { + selectProject(projects.value[0]) console.log('已选择默认项目:', projects.value[0]) } else { console.warn('没有可用的项目数据') @@ -963,7 +963,7 @@ onMounted(() => { margin-bottom: 4px; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; } - + .project-manager { font-size: 12px; color: #86909c;