From 31344f1d58b33848078a7dde162b5f6e855eba47 Mon Sep 17 00:00:00 2001 From: hongawen <83944980@qq.com> Date: Wed, 17 Jun 2026 19:27:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(projects):=20=E5=BE=AE=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../custom/business-table-action-cell.tsx | 6 +- .../custom/current-user-role-tags.vue | 84 +++ src/service/api/dict.ts | 2 +- src/service/api/product.ts | 7 +- src/service/api/project.ts | 14 +- src/store/modules/auth/index.ts | 2 +- src/typings/api/common.d.ts | 16 + src/typings/api/product.d.ts | 2 + src/typings/api/project.d.ts | 2 + src/typings/components.d.ts | 6 + src/views/_builtin/login/index.vue | 26 +- src/views/product/list/index.vue | 240 +++----- .../list/modules/product-grouped-table.vue | 580 ++++++++++++++++++ .../list/modules/project-grouped-table.vue | 79 ++- .../composables/use-task-view-context.ts | 2 +- src/views/project/project/execution/index.vue | 10 +- src/views/system/user/index.vue | 4 +- .../modules/workbench-my-week-worklog.vue | 13 +- 18 files changed, 876 insertions(+), 219 deletions(-) create mode 100644 src/components/custom/current-user-role-tags.vue create mode 100644 src/views/product/list/modules/product-grouped-table.vue diff --git a/src/components/custom/business-table-action-cell.tsx b/src/components/custom/business-table-action-cell.tsx index fab7b36..9c77f78 100644 --- a/src/components/custom/business-table-action-cell.tsx +++ b/src/components/custom/business-table-action-cell.tsx @@ -2,6 +2,8 @@ import { computed, defineComponent, h, ref } from 'vue'; import type { Component, PropType } from 'vue'; import { ElButton, ElPopover, ElTooltip } from 'element-plus'; import { $t } from '@/locales'; +import IconMdiDotsHorizontal from '~icons/mdi/dots-horizontal'; +import IconMdiChevronDown from '~icons/mdi/chevron-down'; export type BusinessTableAction = { key: string; @@ -162,11 +164,11 @@ export default defineComponent({ onClick={event => event.stopPropagation()} > {props.variant === 'icon' ? ( - + ) : ( {$t('common.more')} - + )} diff --git a/src/components/custom/current-user-role-tags.vue b/src/components/custom/current-user-role-tags.vue new file mode 100644 index 0000000..0989d00 --- /dev/null +++ b/src/components/custom/current-user-role-tags.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/src/service/api/dict.ts b/src/service/api/dict.ts index 5d7b5d0..67f4128 100644 --- a/src/service/api/dict.ts +++ b/src/service/api/dict.ts @@ -191,6 +191,6 @@ export async function fetchGetDictDataByCode(code: string) { return mapServiceResult(result as ServiceRequestResult, data => ({ ...data, - list: data.list.map(normalizeDictData) + list: (data.list ?? []).map(normalizeDictData) })); } diff --git a/src/service/api/product.ts b/src/service/api/product.ts index c6b0a47..f4647e7 100644 --- a/src/service/api/product.ts +++ b/src/service/api/product.ts @@ -11,9 +11,11 @@ import { normalizeProductMember, normalizeProductSettings } from './product-shar const PRODUCT_PREFIX = `${WEB_SERVICE_PREFIX}/project/product`; -type ProductResponse = Omit & { +type ProductResponse = Omit & { id: string | number; managerUserId?: string | number | null; + /** 灰度/兼容期后端可能缺省,适配层兜底为 [] */ + currentUserRoles?: Api.Common.CurrentUserRole[] | null; }; type ProductPageResponse = Api.Product.PageResult; @@ -39,7 +41,8 @@ function normalizeProduct(product: ProductResponse): Api.Product.Product { return { ...product, id: normalizeStringId(product.id), - managerUserId: normalizeNullableStringId(product.managerUserId) ?? '' + managerUserId: normalizeNullableStringId(product.managerUserId) ?? '', + currentUserRoles: product.currentUserRoles ?? [] }; } diff --git a/src/service/api/project.ts b/src/service/api/project.ts index 0e01e02..38d6615 100644 --- a/src/service/api/project.ts +++ b/src/service/api/project.ts @@ -47,7 +47,14 @@ const PROJECT_PREFIX = `${WEB_SERVICE_PREFIX}/project/project`; export type ProjectResponse = Omit< Api.Project.Project, - 'id' | 'managerUserId' | 'productId' | 'plannedStartDate' | 'plannedEndDate' | 'actualStartDate' | 'actualEndDate' + | 'id' + | 'managerUserId' + | 'productId' + | 'plannedStartDate' + | 'plannedEndDate' + | 'actualStartDate' + | 'actualEndDate' + | 'currentUserRoles' > & { id: string | number; managerUserId?: string | number | null; @@ -56,6 +63,8 @@ export type ProjectResponse = Omit< plannedEndDate?: ProjectLocalDateValue; actualStartDate?: ProjectLocalDateValue; actualEndDate?: ProjectLocalDateValue; + /** 灰度/兼容期后端可能缺省,适配层兜底为 [] */ + currentUserRoles?: Api.Common.CurrentUserRole[] | null; }; type ProjectPageResponse = Api.Project.PageResult; @@ -96,7 +105,8 @@ export function normalizeProject(project: ProjectResponse): Api.Project.Project plannedStartDate: normalizeProjectLocalDate(project.plannedStartDate), plannedEndDate: normalizeProjectLocalDate(project.plannedEndDate), actualStartDate: normalizeProjectLocalDate(project.actualStartDate), - actualEndDate: normalizeProjectLocalDate(project.actualEndDate) + actualEndDate: normalizeProjectLocalDate(project.actualEndDate), + currentUserRoles: project.currentUserRoles ?? [] }; } diff --git a/src/store/modules/auth/index.ts b/src/store/modules/auth/index.ts index 0e315b8..aea8f8d 100644 --- a/src/store/modules/auth/index.ts +++ b/src/store/modules/auth/index.ts @@ -141,7 +141,7 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => { window.$notification?.success({ title: $t('page.login.common.loginSuccess'), - message: $t('page.login.common.welcomeBack', { userName: userInfo.userName }), + message: $t('page.login.common.welcomeBack', { userName: userInfo.nickname || userInfo.userName }), duration: 4500 }); } diff --git a/src/typings/api/common.d.ts b/src/typings/api/common.d.ts index 3005226..cd6047a 100644 --- a/src/typings/api/common.d.ts +++ b/src/typings/api/common.d.ts @@ -31,6 +31,22 @@ declare namespace Api { */ type EnableStatus = '1' | '2'; + /** + * 列表项「当前登录用户在该对象的角色」(产品 / 项目列表共用)。 + * + * 后端只读计算字段,随登录身份变化:同一份列表不同账号看到的内容不同;无角色为 []。 + * 提交 / 更新接口不需要回传它。 + */ + interface CurrentUserRole { + /** + * 角色稳定标识(程序判断用,不随中文名变化)。 + * 例:product_manager / project_manager / developer / tester / watcher / creator / implicit_observer。 + */ + roleKey: string; + /** 角色中文名(直接展示) */ + roleName: string; + } + /** common record */ type CommonRecord = { /** record id */ diff --git a/src/typings/api/product.d.ts b/src/typings/api/product.d.ts index 95043d9..4a311e9 100644 --- a/src/typings/api/product.d.ts +++ b/src/typings/api/product.d.ts @@ -67,6 +67,8 @@ declare namespace Api { createTime: string; /** 更新时间 */ updateTime: string; + /** 当前登录用户在该产品的角色(后端只读计算字段,随登录身份变化;无角色为 []) */ + currentUserRoles: Api.Common.CurrentUserRole[]; } interface ProductSettingBaseInfo { diff --git a/src/typings/api/project.d.ts b/src/typings/api/project.d.ts index 27abf34..2af2ab0 100644 --- a/src/typings/api/project.d.ts +++ b/src/typings/api/project.d.ts @@ -851,6 +851,8 @@ declare namespace Api { createTime: string; /** 更新时间 */ updateTime: string; + /** 当前登录用户在该项目的角色(后端只读计算字段,随登录身份变化;无角色为 []) */ + currentUserRoles: Api.Common.CurrentUserRole[]; } interface ProjectContext { diff --git a/src/typings/components.d.ts b/src/typings/components.d.ts index 277cdae..c57a52a 100644 --- a/src/typings/components.d.ts +++ b/src/typings/components.d.ts @@ -23,6 +23,7 @@ declare module 'vue' { BusinessUserSelect: typeof import('./../components/custom/business-user-select.vue')['default'] ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default'] CountTo: typeof import('./../components/custom/count-to.vue')['default'] + CurrentUserRoleTags: typeof import('./../components/custom/current-user-role-tags.vue')['default'] CustomIconSelect: typeof import('./../components/custom/custom-icon-select.vue')['default'] DarkModeContainer: typeof import('./../components/common/dark-mode-container.vue')['default'] DictSelect: typeof import('./../components/custom/dict-select.vue')['default'] @@ -125,9 +126,13 @@ declare module 'vue' { IconIcRoundChevronRight: typeof import('~icons/ic/round-chevron-right')['default'] IconIcRoundDelete: typeof import('~icons/ic/round-delete')['default'] IconIcRoundEdit: typeof import('~icons/ic/round-edit')['default'] + IconIcRoundFolder: typeof import('~icons/ic/round-folder')['default'] + IconIcRoundInventory2: typeof import('~icons/ic/round-inventory2')['default'] + IconIcRoundPackage2: typeof import('~icons/ic/round-package2')['default'] IconIcRoundPlus: typeof import('~icons/ic/round-plus')['default'] IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default'] IconIcRoundRemove: typeof import('~icons/ic/round-remove')['default'] + IconIcRoundRocketLaunch: typeof import('~icons/ic/round-rocket-launch')['default'] IconIcRoundSearch: typeof import('~icons/ic/round-search')['default'] IconIcRoundUnfoldLess: typeof import('~icons/ic/round-unfold-less')['default'] IconIcRoundUnfoldMore: typeof import('~icons/ic/round-unfold-more')['default'] @@ -137,6 +142,7 @@ declare module 'vue' { IconLocalLogo: typeof import('~icons/local/logo')['default'] 'IconMaterialSymbolsLight:rotate90DegreesCcwOutlineRounded': typeof import('~icons/material-symbols-light/rotate90-degrees-ccw-outline-rounded')['default'] IconMaterialSymbolsLightCheckCircleRounded: typeof import('~icons/material-symbols-light/check-circle-rounded')['default'] + IconMaterialSymbolsPackage2: typeof import('~icons/material-symbols/package2')['default'] 'IconMdi:paperclip': typeof import('~icons/mdi/paperclip')['default'] 'IconMdi:printer': typeof import('~icons/mdi/printer')['default'] IconMdiAccountTieOutline: typeof import('~icons/mdi/account-tie-outline')['default'] diff --git a/src/views/_builtin/login/index.vue b/src/views/_builtin/login/index.vue index 399fff3..78c8975 100644 --- a/src/views/_builtin/login/index.vue +++ b/src/views/_builtin/login/index.vue @@ -148,29 +148,29 @@ const waves = [ /** 电力场景剪影:输电铁塔(底部接地,局部坐标基点为塔脚中心) */ const towers = [ - { x: 150, s: 1 }, - { x: 540, s: 0.85 }, - { x: 1280, s: 0.7 } + { x: 150, s: 1.42 }, + { x: 540, s: 1.2 }, + { x: 1280, s: 1.0 } ]; -/** 塔间悬垂导线(悬链线意象),坐标对应各塔最宽横担端点 */ +/** 塔间悬垂导线(悬链线意象),坐标对应各塔最宽横担端点(随铁塔 scale 同步重算) */ const powerLines = [ - { path: 'M -60,762 Q 30,800 92,750' }, - { path: 'M 208,750 Q 350,819 491,768' }, - { path: 'M 589,768 Q 914,867 1239,786' }, - { path: 'M 1321,786 Q 1430,824 1520,804' } + { path: 'M -60,712 Q 4,756 68,700' }, + { path: 'M 232,700 Q 350,777 470,726' }, + { path: 'M 610,726 Q 915,831 1222,750' }, + { path: 'M 1338,750 Q 1429,788 1520,768' } ]; -/** 导线上滑过的电流光点 */ +/** 导线上滑过的电流光点(端点对齐 powerLines 的中间两段) */ const lineSparks = [ - { key: 'spark-1', path: 'M 208,750 Q 350,819 491,768', dur: '5s', begin: '0s' }, - { key: 'spark-2', path: 'M 589,768 Q 914,867 1239,786', dur: '7s', begin: '2s' } + { key: 'spark-1', path: 'M 232,700 Q 350,777 470,726', dur: '5s', begin: '0s' }, + { key: 'spark-2', path: 'M 610,726 Q 915,831 1222,750', dur: '7s', begin: '2s' } ]; /** 风机(新能源应用场景),dur 为叶轮旋转周期 */ const turbines = [ - { key: 'turbine-1', x: 715, s: 0.9, dur: '9s' }, - { key: 'turbine-2', x: 828, s: 0.6, dur: '13s' } + { key: 'turbine-1', x: 715, s: 1.28, dur: '9s' }, + { key: 'turbine-2', x: 828, s: 0.88, dur: '13s' } ]; diff --git a/src/views/product/list/index.vue b/src/views/product/list/index.vue index 03a0bf2..7ba6741 100644 --- a/src/views/product/list/index.vue +++ b/src/views/product/list/index.vue @@ -1,16 +1,13 @@ - + + + + diff --git a/src/views/project/list/modules/project-grouped-table.vue b/src/views/project/list/modules/project-grouped-table.vue index e37c1f9..968e151 100644 --- a/src/views/project/list/modules/project-grouped-table.vue +++ b/src/views/project/list/modules/project-grouped-table.vue @@ -2,9 +2,10 @@ import { computed, ref, watch } from 'vue'; import { ElButton, ElEmpty, ElProgress, ElTag, ElTooltip } from 'element-plus'; import dayjs from 'dayjs'; -import { ArrowDown, Collection, QuestionFilled } from '@element-plus/icons-vue'; +import { ArrowDown, QuestionFilled } from '@element-plus/icons-vue'; import { RDMS_OBJECT_DIRECTION_DICT_CODE, RDMS_PROJECT_TYPE_DICT_CODE } from '@/constants/dict'; import { useDict } from '@/hooks/business/dict'; +import CurrentUserRoleTags from '@/components/custom/current-user-role-tags.vue'; import { getProjectStatusLabel, getProjectStatusTagType } from '../../shared/project-master-data'; defineOptions({ name: 'ProjectGroupedTable' }); @@ -202,7 +203,8 @@ interface FlatRow { project?: Api.Project.Project; } -const COLUMN_COUNT = 7; +// 项目行列数(名称/类型/经理/我的角色/进度/计划周期/状态/更新),非项目行整行合并用 +const COLUMN_COUNT = 8; /** 单个产品组铺平为行:产品行 +(未折叠时)占位/项目/收纳行 */ function buildGroupRows(group: Api.Project.ProjectGroup): FlatRow[] { @@ -218,7 +220,12 @@ function buildGroupRows(group: Api.Project.ProjectGroup): FlatRow[] { } for (const project of visibleProjects(group)) { - rows.push({ rowType: 'project', key: `proj-${project.id}`, group, project }); + rows.push({ + rowType: 'project', + key: `proj-${project.id}`, + group, + project + }); } if (hiddenCount(group) > 0) { @@ -396,10 +403,8 @@ function overdueDays(project: Api.Project.Project) {
- - - - + + {{ row.group.productName }} @@ -429,10 +434,15 @@ function overdueDays(project: Api.Project.Project) {
- - {{ row.project.projectName }} - -
{{ row.project.projectCode }}
+ + + +
+ + {{ row.project.projectName }} + +
{{ row.project.projectCode }}
+
@@ -454,18 +464,24 @@ function overdueDays(project: Api.Project.Project) { - + - + + + + +