From 39458386ae35ad7a929f2236050d4ce4b63d457e Mon Sep 17 00:00:00 2001 From: hongawen <83944980@qq.com> Date: Thu, 4 Jun 2026 11:26:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(projects):=20=E5=B7=A5=E4=BD=9C=E5=8F=B0?= =?UTF-8?q?=E9=83=A8=E5=88=86=E7=BB=84=E4=BB=B6=E8=B0=83=E6=88=90=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 49 +++ src/service/api/project-shared.ts | 80 +++++ src/service/api/project.ts | 54 +++ src/typings/api/project.d.ts | 101 ++++++ .../execution/composables/use-task-actions.ts | 30 +- .../composables/use-task-permissions.ts | 7 +- .../execution/modules/task-board-view.vue | 15 +- .../execution/modules/task-table-view.vue | 4 +- .../modules/task-worklog-content.vue | 21 +- .../execution/modules/task-worklog-panel.vue | 4 +- .../composables/use-workbench-layout.ts | 52 ++- .../composables/use-workbench-modules.ts | 53 +-- .../composables/use-workbench-refresh.ts | 30 ++ .../composables/workbench-layout-default.ts | 47 ++- .../composables/workbench-layout-reconcile.ts | 46 ++- .../composables/workbench-layout-types.ts | 23 +- src/views/workbench/homepage.ts | 168 ++++----- src/views/workbench/index.vue | 84 +++-- src/views/workbench/mock.ts | 242 ------------- .../workbench/modules/workbench-column.vue | 61 ---- .../modules/workbench-module-card.vue | 39 +- .../modules/workbench-module-library.vue | 17 +- .../modules/workbench-my-execution.vue | 173 ++++++--- .../modules/workbench-my-week-worklog.vue | 46 ++- .../modules/workbench-notice-notification.vue | 293 --------------- .../modules/workbench-product-snapshot.vue | 12 +- .../modules/workbench-project-grid.vue | 338 ++++++++++-------- .../modules/workbench-project-health.vue | 12 +- .../modules/workbench-shortcut-picker.vue | 1 + .../workbench/modules/workbench-shortcut.vue | 65 +++- .../workbench/modules/workbench-team-load.vue | 17 +- .../modules/workbench-todo-panel.vue | 17 +- 33 files changed, 1033 insertions(+), 1169 deletions(-) create mode 100644 src/views/workbench/composables/use-workbench-refresh.ts delete mode 100644 src/views/workbench/modules/workbench-column.vue delete mode 100644 src/views/workbench/modules/workbench-notice-notification.vue diff --git a/package.json b/package.json index 2a14bf9..dd30ffa 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "dompurify": "3.2.6", "echarts": "6.0.0", "element-plus": "^2.11.1", + "grid-layout-plus": "^1.1.1", "jsbarcode": "3.12.1", "jsencrypt": "^3.5.4", "json5": "2.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e81680..174aa9e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,6 +89,9 @@ importers: element-plus: specifier: ^2.11.1 version: 2.13.6(typescript@5.8.3)(vue@3.5.20(typescript@5.8.3)) + grid-layout-plus: + specifier: ^1.1.1 + version: 1.1.1(vue@3.5.20(typescript@5.8.3)) jsbarcode: specifier: 3.12.1 version: 3.12.1 @@ -882,6 +885,9 @@ packages: peerDependencies: vue: '>=3' + '@interactjs/types@1.10.27': + resolution: {integrity: sha512-BUdv0cvs4H5ODuwft2Xp4eL8Vmi3LcihK42z0Ft/FbVJZoRioBsxH+LlsBdK4tAie7PqlKGy+1oyOncu1nQ6eA==} + '@intlify/core-base@11.1.11': resolution: {integrity: sha512-1Z0N8jTfkcD2Luq9HNZt+GmjpFe4/4PpZF3AOzoO1u5PTtSuXZcfhwBatywbfE2ieB/B5QHIoOFmCXY2jqVKEQ==} engines: {node: '>= 16'} @@ -921,6 +927,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@juggle/resize-observer@3.4.0': + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + '@naoak/workerize-transferable@0.1.0': resolution: {integrity: sha512-fDLfuP71IPNP5+zSfxFb52OHgtjZvauRJWbVnpzQ7G7BjcbLjTny0OW1d3ZO806XKpLWNKmeeW3MhE0sy8iwYQ==} peerDependencies: @@ -1817,6 +1826,14 @@ packages: peerDependencies: '@uppy/core': ^2.3.3 + '@vexip-ui/hooks@2.9.4': + resolution: {integrity: sha512-dGUiBAeHIsnSVigGSPHcuHBVqrSGW8LV+zGohvOpBfXs8Ynn5ZcSmybIWJ3G826NsicPu9rqwcJG8uvSgG4k4Q==} + peerDependencies: + vue: ^3.2.25 + + '@vexip-ui/utils@2.16.4': + resolution: {integrity: sha512-KX+Q4EsuwDp6ZlRJ7OAkiYxu52D5CVM8zpqQz/FXYV+JUtzl9T3dvxgtA8gQ0wm5Sh/xT6jp8Wo4X7tLAzRh/A==} + '@visactor/vchart-theme@1.12.2': resolution: {integrity: sha512-r298TUdK+CKbHGVYWgQnNSEB5uqpFvF2/aMNZ/2POQnd2CovAPJOx2nTE6hAcOn8rra2FwJ2xF8AyP1O5OhrTw==} peerDependencies: @@ -3493,6 +3510,11 @@ packages: graphlib@2.1.8: resolution: {integrity: sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==} + grid-layout-plus@1.1.1: + resolution: {integrity: sha512-7CWehJubrVC8Ps5QFUlnDsp0kiREvKfi3Pdjp21EyY8BNzSusqI3Utcxvu1Y9UUKe3YExvbhJzIxHK6rorbRaQ==} + peerDependencies: + vue: ^3.0.0 + gzip-size@6.0.0: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} engines: {node: '>=10'} @@ -3629,6 +3651,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + interactjs@1.10.27: + resolution: {integrity: sha512-y/8RcCftGAF24gSp76X2JS3XpHiUvDQyhF8i7ujemBz77hwiHDuJzftHx7thY8cxGogwGiPJ+o97kWB6eAXnsA==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -6226,6 +6251,8 @@ snapshots: '@iconify/types': 2.0.0 vue: 3.5.20(typescript@5.8.3) + '@interactjs/types@1.10.27': {} + '@intlify/core-base@11.1.11': dependencies: '@intlify/message-compiler': 11.1.11 @@ -6273,6 +6300,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@juggle/resize-observer@3.4.0': {} + '@naoak/workerize-transferable@0.1.0(workerize-loader@2.0.2(webpack@5.105.4))': dependencies: workerize-loader: 2.0.2(webpack@5.105.4) @@ -7082,6 +7111,15 @@ snapshots: '@uppy/utils': 4.1.3 nanoid: 3.3.11 + '@vexip-ui/hooks@2.9.4(vue@3.5.20(typescript@5.8.3))': + dependencies: + '@floating-ui/dom': 1.7.6 + '@juggle/resize-observer': 3.4.0 + '@vexip-ui/utils': 2.16.4 + vue: 3.5.20(typescript@5.8.3) + + '@vexip-ui/utils@2.16.4': {} + '@visactor/vchart-theme@1.12.2(@visactor/vchart@2.0.4)': dependencies: '@visactor/vchart': 2.0.4 @@ -9179,6 +9217,13 @@ snapshots: dependencies: lodash: 4.17.23 + grid-layout-plus@1.1.1(vue@3.5.20(typescript@5.8.3)): + dependencies: + '@vexip-ui/hooks': 2.9.4(vue@3.5.20(typescript@5.8.3)) + '@vexip-ui/utils': 2.16.4 + interactjs: 1.10.27 + vue: 3.5.20(typescript@5.8.3) + gzip-size@6.0.0: dependencies: duplexer: 0.1.2 @@ -9295,6 +9340,10 @@ snapshots: inherits@2.0.4: {} + interactjs@1.10.27: + dependencies: + '@interactjs/types': 1.10.27 + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 diff --git a/src/service/api/project-shared.ts b/src/service/api/project-shared.ts index 25cba36..4a71784 100644 --- a/src/service/api/project-shared.ts +++ b/src/service/api/project-shared.ts @@ -40,6 +40,42 @@ export type ProjectExecutionResponse = Omit< priorityName?: string | null; }; +export type MyExecutionResponse = Omit< + Api.Project.MyExecutionItem, + | 'id' + | 'projectId' + | 'projectRequirementId' + | 'priority' + | 'progressRate' + | 'plannedStartDate' + | 'plannedEndDate' + | 'actualStartDate' + | 'actualEndDate' +> & { + id: StringIdResponse; + projectId: StringIdResponse; + projectRequirementId?: StringIdResponse | null; + priority?: string | number | null; + progressRate?: number | null; + plannedStartDate?: ProjectLocalDateValue; + plannedEndDate?: ProjectLocalDateValue; + actualStartDate?: ProjectLocalDateValue; + actualEndDate?: ProjectLocalDateValue; +}; + +export type MyParticipatedProjectResponse = Omit & { + id: StringIdResponse; +}; + +export type MyOwnedProjectMemberResponse = Omit & { + userId: StringIdResponse; +}; + +export type MyOwnedProjectResponse = Omit & { + id: StringIdResponse; + members?: MyOwnedProjectMemberResponse[] | null; +}; + export type ExecutionAssigneeResponse = Omit & { id: StringIdResponse; executionId: StringIdResponse; @@ -286,6 +322,50 @@ export function normalizeProjectExecution(response: ProjectExecutionResponse): A }; } +export function normalizeMyExecution(response: MyExecutionResponse): Api.Project.MyExecutionItem { + return { + ...response, + id: normalizeStringId(response.id), + projectId: normalizeStringId(response.projectId), + statusName: response.statusName ?? null, + priority: normalizePriority(response.priority), + progressRate: typeof response.progressRate === 'number' ? response.progressRate : 0, + plannedStartDate: normalizeProjectLocalDate(response.plannedStartDate), + plannedEndDate: normalizeProjectLocalDate(response.plannedEndDate), + actualStartDate: normalizeProjectLocalDate(response.actualStartDate), + actualEndDate: normalizeProjectLocalDate(response.actualEndDate), + projectRequirementId: normalizeNullableStringId(response.projectRequirementId), + projectRequirementName: response.projectRequirementName ?? null + }; +} + +export function normalizeMyParticipatedProject( + response: MyParticipatedProjectResponse +): Api.Project.MyParticipatedProjectItem { + return { + ...response, + id: normalizeStringId(response.id), + code: response.code ?? null, + statusName: response.statusName ?? null, + myRole: response.myRole ?? null + }; +} + +export function normalizeMyOwnedProject(response: MyOwnedProjectResponse): Api.Project.MyOwnedProjectItem { + return { + ...response, + id: normalizeStringId(response.id), + code: response.code ?? null, + myRole: response.myRole ?? null, + plannedEndDate: response.plannedEndDate ?? null, + members: (response.members ?? []).map(member => ({ + ...member, + userId: normalizeStringId(member.userId), + userName: member.userName ?? null + })) + }; +} + export function normalizeExecutionAssignee(response: ExecutionAssigneeResponse): Api.Project.ExecutionAssignee { return { ...response, diff --git a/src/service/api/project.ts b/src/service/api/project.ts index a06a73d..b2a5ae8 100644 --- a/src/service/api/project.ts +++ b/src/service/api/project.ts @@ -10,6 +10,9 @@ import { import { type ExecutionAssigneeLogResponse, type ExecutionAssigneeResponse, + type MyExecutionResponse, + type MyOwnedProjectResponse, + type MyParticipatedProjectResponse, type ProjectExecutionResponse, type ProjectLocalDateValue, type ProjectMemberResponse, @@ -20,6 +23,9 @@ import { getProjectLifecycleActions, normalizeExecutionAssignee, normalizeExecutionAssigneeLog, + normalizeMyExecution, + normalizeMyOwnedProject, + normalizeMyParticipatedProject, normalizeProjectExecution, normalizeProjectLocalDate, normalizeProjectMember, @@ -365,6 +371,54 @@ export async function fetchGetProjectExecutionPage( })); } +/** 获取工作台「我负责的执行」(跨项目聚合,owner 隐式取当前登录用户) */ +export async function fetchGetMyExecutionPage(params?: Api.Project.MyExecutionSearchParams) { + type MyExecutionPageResponse = Api.Project.PageResult; + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_PREFIX}/me/executions/page`, + method: 'get', + params + }); + + return mapServiceResult(result as ServiceRequestResult, data => ({ + ...data, + list: data.list.map(normalizeMyExecution) + })); +} + +/** 获取工作台「我参与的项目」(成员视角,附我的角色与任务量;隐式取当前登录用户) */ +export async function fetchGetMyParticipatedProjectPage(params?: Api.Project.MyProjectSearchParams) { + type MyParticipatedProjectPageResponse = Api.Project.PageResult; + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_PREFIX}/me/participated/page`, + method: 'get', + params + }); + + return mapServiceResult(result as ServiceRequestResult, data => ({ + ...data, + list: data.list.map(normalizeMyParticipatedProject) + })); +} + +/** 获取工作台「我负责的项目」(项目负责人视角,附聚合统计与成员负载;隐式取当前登录用户) */ +export async function fetchGetMyOwnedProjectPage(params?: Api.Project.MyProjectSearchParams) { + type MyOwnedProjectPageResponse = Api.Project.PageResult; + const result = await request({ + ...safeJsonRequestConfig, + url: `${PROJECT_PREFIX}/me/owned/page`, + method: 'get', + params + }); + + return mapServiceResult(result as ServiceRequestResult, data => ({ + ...data, + list: data.list.map(normalizeMyOwnedProject) + })); +} + /** 获取项目执行状态看板 */ export function fetchGetProjectExecutionStatusBoard( projectId: string, diff --git a/src/typings/api/project.d.ts b/src/typings/api/project.d.ts index a7a26b5..46fa174 100644 --- a/src/typings/api/project.d.ts +++ b/src/typings/api/project.d.ts @@ -304,6 +304,107 @@ declare namespace Api { updateTime: string[]; }>; + /** 工作台「我负责的执行」(跨项目)查询入参 */ + type MyExecutionSearchParams = CommonType.RecordNullable< + Pick & { + /** 预留:单状态精确过滤,不传走后端默认口径 */ + statusCode: string; + /** 预留:执行名称模糊匹配 */ + keyword: string; + } + >; + + /** 工作台「我负责的执行」单项(跨项目聚合,owner 恒为当前登录用户) */ + interface MyExecutionItem { + /** 执行 ID(雪花 ID,字符串) */ + id: string; + executionName: string; + /** 所属项目 */ + projectId: string; + projectName: string; + /** 执行状态编码:pending / active / paused */ + statusCode: string; + /** 执行状态名称 */ + statusName: string | null; + /** 优先级字典 value(rdms_req_priority,"0"~"3") */ + priority: string; + /** 计划起止(YYYY-MM-DD) */ + plannedStartDate: string | null; + plannedEndDate: string | null; + /** 实际起止(YYYY-MM-DD) */ + actualStartDate: string | null; + actualEndDate: string | null; + /** 进度(0-100 整数) */ + progressRate: number; + /** 关联项目需求 */ + projectRequirementId: string | null; + projectRequirementName: string | null; + } + + /** 工作台「我的项目」查询入参(我参与的 / 我负责的 共用) */ + type MyProjectSearchParams = CommonType.RecordNullable< + Pick & { + /** 预留:项目名称/编码模糊关键字,后端本期不过滤 */ + keyword: string; + } + >; + + /** 工作台「我参与的项目」单项(成员视角,附带我的角色与任务量) */ + interface MyParticipatedProjectItem { + /** 项目 ID(字符串) */ + id: string; + name: string; + /** 项目编码,可空 */ + code: string | null; + /** 项目状态编码(如 active) */ + statusCode: string; + /** 项目状态名称,可空 */ + statusName: string | null; + /** 项目整体进度 0-100 */ + progress: number; + /** 我在该项目中的角色名(多角色拼接),可空 */ + myRole: string | null; + /** 我负责的任务总数(按负责人,含已完成) */ + myTaskCount: number; + /** 我负责的未完成任务数 */ + myPendingTaskCount: number; + } + + /** 工作台「我负责的项目」成员负载子项 */ + interface MyOwnedProjectMember { + /** 成员用户 ID(字符串) */ + userId: string; + /** 成员姓名/昵称,可空 */ + userName: string | null; + /** 该成员在本项目下进行中任务数(按负责人) */ + activeTaskCount: number; + } + + /** 工作台「我负责的项目」单项(项目负责人视角,附聚合统计与成员负载) */ + interface MyOwnedProjectItem { + /** 项目 ID(字符串) */ + id: string; + name: string; + /** 项目编码,可空 */ + code: string | null; + /** 项目整体进度 0-100 */ + progress: number; + /** 我在该项目中的角色名,可空 */ + myRole: string | null; + /** 项目计划结束日期 YYYY-MM-DD,可空 */ + plannedEndDate: string | null; + /** 项目下进行中执行数 */ + executionCount: number; + /** 项目下进行中任务数 */ + taskCount: number; + /** 项目下逾期任务数 */ + overdueCount: number; + /** 项目当前有效成员数(多角色去重) */ + memberCount: number; + /** 成员负载列表(无成员为 []) */ + members: MyOwnedProjectMember[]; + } + /** 创建执行入参(含 ownerId + assigneeUserIds) */ interface CreateProjectExecutionParams { executionName: string; diff --git a/src/views/project/project/execution/composables/use-task-actions.ts b/src/views/project/project/execution/composables/use-task-actions.ts index e5965da..25fd68c 100644 --- a/src/views/project/project/execution/composables/use-task-actions.ts +++ b/src/views/project/project/execution/composables/use-task-actions.ts @@ -1,6 +1,4 @@ -import { type Ref, computed, markRaw } from 'vue'; -import { useAuthStore } from '@/store/modules/auth'; -import { canReportTaskWorklog } from '../shared'; +import { markRaw } from 'vue'; import { useTaskPermissions } from './use-task-permissions'; import IconMdiCheckCircleOutline from '~icons/mdi/check-circle-outline'; import IconMdiClipboardEditOutline from '~icons/mdi/clipboard-edit-outline'; @@ -58,27 +56,21 @@ const STATUS_ACTION_ORDER: Record = { * * 表格操作列与看板卡片操作区共用同一份语义:填报 / 编辑 / 删除 / 状态推进按钮, * 含 auto_start 过滤、complete 进度 100% 兜底、按钮排序与 icon/type 映射。 - * - * dataRef 用于填报按钮的"叶子"判定(canReportTaskWorklog 需要全量行集合)。 */ -export function useTaskActions(dataRef: Ref, emits: TaskActionEmits) { - const authStore = useAuthStore(); - const currentUserId = computed(() => authStore.userInfo.userId || ''); - const { canEditTask, canDeleteTask, canReportTaskWorklog: hasReportWorklogPermission } = useTaskPermissions(); +export function useTaskActions(emits: TaskActionEmits) { + const { canEditTask, canDeleteTask } = useTaskPermissions(); function createActions(row: Api.Project.ProjectTask): TaskAction[] { const actions: TaskAction[] = []; - // 填报:权限码门槛 AND 业务规则(叶子/身份/状态)双重判定 - if (hasReportWorklogPermission() && canReportTaskWorklog(row, dataRef.value, currentUserId.value)) { - actions.push({ - key: 'report', - tooltip: '填报', - icon: markRaw(IconMdiClipboardEditOutline), - type: 'primary', - onClick: () => emits.report(row) - }); - } + // 工作日志:行操作入口始终显示——查看人人可看;新增/编辑由弹层内 canSubmit 按身份与状态控制 + actions.push({ + key: 'report', + tooltip: '工作日志', + icon: markRaw(IconMdiClipboardEditOutline), + type: 'primary', + onClick: () => emits.report(row) + }); if (canEditTask(row)) { actions.push({ diff --git a/src/views/project/project/execution/composables/use-task-permissions.ts b/src/views/project/project/execution/composables/use-task-permissions.ts index 7d5b99e..d66a9ed 100644 --- a/src/views/project/project/execution/composables/use-task-permissions.ts +++ b/src/views/project/project/execution/composables/use-task-permissions.ts @@ -118,10 +118,6 @@ export function useTaskPermissions() { return isTopLevelTask(task) && currentUserId.value === task.executionOwnerId; } - function canReportTaskWorklog(): boolean { - return hasPermission('project:task:worklog'); - } - return { // execution canEditExecution, @@ -134,7 +130,6 @@ export function useTaskPermissions() { canDeleteTask, canCreateTopLevelTask, canCreateSubTask, - canManageTaskAssignee, - canReportTaskWorklog + canManageTaskAssignee }; } diff --git a/src/views/project/project/execution/modules/task-board-view.vue b/src/views/project/project/execution/modules/task-board-view.vue index ab304c2..a619427 100644 --- a/src/views/project/project/execution/modules/task-board-view.vue +++ b/src/views/project/project/execution/modules/task-board-view.vue @@ -1,5 +1,5 @@ - - - - diff --git a/src/views/workbench/modules/workbench-module-card.vue b/src/views/workbench/modules/workbench-module-card.vue index 75908e6..a317c22 100644 --- a/src/views/workbench/modules/workbench-module-card.vue +++ b/src/views/workbench/modules/workbench-module-card.vue @@ -1,5 +1,5 @@