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 @@ + + + + + + {{ item.roleName }} + + + {{ emptyText }} + + + 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 @@ - + + + + + + + + + + {{ getDirectionLabel(row.group.directionCode, row.group.directionCode || '--') }} + + {{ row.group.products.length }} 个产品 + + + + + + + + + {{ row.product.name }} + + + {{ truncateDesc(row.product.description) }} + 详情 + + + + + + + + + + 收起 + 还有 {{ row.group.products.length - DEFAULT_TOP_N }} 个,展开查看 + + + 默认显示前 {{ DEFAULT_TOP_N }} 个,按最近更新排序 + + + + + changePage(row.group.directionCode, page)" + /> + + + + + + + {{ row.product.code || '--' }} + + + + + + {{ getManagerName(row.product.managerUserId) }} + + + + + + + + + + + + + {{ getProductStatusLabel(row.product.statusCode) }} + + + + + + + {{ row.product.lastStatusReason?.trim() || '--' }} + + + + + + {{ formatDate(row.product.updateTime) }} + + + + + + + + + + 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) { - + {{ getTypeLabel(row.project.projectType, '--') }} - + {{ projectManagerLabel(row.project) }} + + + + + + @@ -660,13 +676,44 @@ function overdueDays(project: Api.Project.Project) { padding: 0; } -// === 提示行 / 项目行 / 收纳行:统一缩进到产品名起始位 === +// === 提示行 / 收纳行:统一缩进到产品名起始位 === .pg-hint, -.pg-more-line, -.pg-proj-name { +.pg-more-line { margin-left: 55px; } +// 项目行:文件夹图标 + 文本块;图标占缩进位(margin 25 + icon22 + gap8 = 55),项目名仍与产品名对齐 +.pg-proj-name { + display: flex; + align-items: center; + gap: 8px; + margin-left: 25px; + min-width: 0; +} + +// 项目文件夹图标:浅蓝底 + 同色系深字,与产品库存箱图标同一视觉语言(父=箱子 / 子=文件夹) +.pg-proj-icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex: none; + width: 22px; + height: 22px; + border-radius: 6px; + background: rgb(240 249 255 / 96%); + color: rgb(2 132 199 / 92%); + font-size: 14px; +} + +.pg-proj-main { + display: flex; + flex-direction: column; + // 贴左:避免 ElButton(link) 被 stretch 拉满后文字居中 + align-items: flex-start; + gap: 2px; + min-width: 0; +} + .pg-hint { display: flex; align-items: center; diff --git a/src/views/project/project/execution/composables/use-task-view-context.ts b/src/views/project/project/execution/composables/use-task-view-context.ts index 1aeb207..3418c4c 100644 --- a/src/views/project/project/execution/composables/use-task-view-context.ts +++ b/src/views/project/project/execution/composables/use-task-view-context.ts @@ -16,7 +16,7 @@ export interface ViewContext { } export function useTaskViewContext() { - const state = reactive({ type: 'my' }); + const state = reactive({ type: 'all' }); function switchToMine() { state.type = 'my'; diff --git a/src/views/project/project/execution/index.vue b/src/views/project/project/execution/index.vue index fb09d79..6e33d8e 100644 --- a/src/views/project/project/execution/index.vue +++ b/src/views/project/project/execution/index.vue @@ -64,7 +64,7 @@ const { context: viewContext, switchToMine, switchToAll } = useTaskViewContext() // 执行域独立视角:跟任务域 viewContext 完全独立,切换互不影响。 // 用 inline reactive 即可,不抽 composable(只在本页用,YAGNI)。 -const executionViewContext = reactive<{ type: 'my' | 'all' }>({ type: 'my' }); +const executionViewContext = reactive<{ type: 'my' | 'all' }>({ type: 'all' }); function switchExecutionToMine() { executionViewContext.type = 'my'; @@ -406,8 +406,8 @@ async function handleExecutionStatusFilter(status: ExecutionStatusFilter) { selectedStatus.value = status; // 状态 chip 是"按状态范围浏览":意味着不再锚定具体执行,清掉 selectedExecution if (selectedExecution.value) selectedExecution.value = null; - // 左侧范围切换默认回「我参与的」视角,符合"在该范围内看自己的"主用法 - switchToMine(); + // 左侧范围切换默认回「所有任务」视角,符合"在该范围内看全部"的主用法 + switchToAll(); await reloadExecutionData(1); } @@ -437,9 +437,9 @@ async function getExecutionDetail(row: Api.Project.ProjectExecution) { } function handleSelectExecution(row: Api.Project.ProjectExecution) { - // 锚定具体执行,默认回「我参与的」视角 + // 锚定具体执行,默认回「所有任务」视角 selectedExecution.value = row; - switchToMine(); + switchToAll(); } function handleSelectPerspective(type: 'my' | 'all') { diff --git a/src/views/system/user/index.vue b/src/views/system/user/index.vue index cdd2bcb..686a6bf 100644 --- a/src/views/system/user/index.vue +++ b/src/views/system/user/index.vue @@ -377,11 +377,11 @@ async function loadFormOptions() { } if (!companyResult.error) { - companyOptions.value = companyResult.data; + companyOptions.value = companyResult.data.list; } if (!orgCodeResult.error) { - orgCodeOptions.value = orgCodeResult.data; + orgCodeOptions.value = orgCodeResult.data.list; } } diff --git a/src/views/workbench/modules/workbench-my-week-worklog.vue b/src/views/workbench/modules/workbench-my-week-worklog.vue index 0177434..7f71d71 100644 --- a/src/views/workbench/modules/workbench-my-week-worklog.vue +++ b/src/views/workbench/modules/workbench-my-week-worklog.vue @@ -359,7 +359,9 @@ watch(activeTab, async tab => { 每日工时 - + + + @@ -535,11 +537,16 @@ watch(activeTab, async tab => { color: var(--el-text-color-placeholder); flex-shrink: 0; } +.ww-section-info-wrap { + display: inline-flex; + align-items: center; + flex-shrink: 0; + cursor: help; +} + .ww-section-info { font-size: 13px; color: var(--el-text-color-placeholder); - flex-shrink: 0; - cursor: help; } .ww-pie-wrap {