From 4ed4b537adf9a7f895b8fac8b12a137338436e7b Mon Sep 17 00:00:00 2001 From: hongawen <83944980@qq.com> Date: Thu, 28 May 2026 08:20:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(projects):=20=E5=B7=A5=E4=BD=9C=E5=8F=B0?= =?UTF-8?q?=E5=B0=8F=E7=BB=84=E4=BB=B6=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../custom/business-rich-text-view.vue | 1 + .../custom/business-user-picker.vue | 920 ++++++++++++++++++ .../components/user-picker-trigger.vue | 127 +++ .../composables/use-chain-source.ts | 90 ++ .../composables/use-dept-source.ts | 99 ++ .../composables/use-picker-selection.ts | 89 ++ .../components/notification-bell.vue | 457 +++++++++ src/layouts/modules/global-header/index.vue | 2 + src/service/api/route.ts | 1 - src/typings/components.d.ts | 2 + .../modules/personal-item-detail-dialog.vue | 1 + .../list/modules/product-create-base-form.vue | 7 +- .../requirement/modules/module-tree-node.vue | 9 +- .../modules/requirement-split-dialog.vue | 1 - .../list/modules/project-create-base-form.vue | 7 +- .../execution/modules/task-worklog-panel.vue | 8 +- .../requirement/modules/module-tree-node.vue | 9 +- .../modules/requirement-split-dialog.vue | 1 - .../composables/layout-storage-local.ts | 8 +- .../composables/use-workbench-colors.ts | 34 + .../composables/use-workbench-layout.ts | 13 +- .../composables/use-workbench-modules.ts | 246 +---- .../composables/workbench-layout-types.ts | 4 +- src/views/workbench/homepage.ts | 536 +++++++--- src/views/workbench/index.vue | 59 +- src/views/workbench/mock.ts | 557 ++++++++--- .../workbench/modules/workbench-approval.vue | 94 -- .../workbench/modules/workbench-banner.vue | 383 ++++---- .../modules/workbench-edit-overlay.vue | 8 + .../workbench/modules/workbench-favorite.vue | 72 -- .../workbench/modules/workbench-mentions.vue | 121 --- .../modules/workbench-module-card.vue | 11 +- .../modules/workbench-module-library.vue | 84 +- .../modules/workbench-my-completion-rate.vue | 122 --- .../modules/workbench-my-execution.vue | 301 ++++-- .../modules/workbench-my-requirement.vue | 101 -- .../workbench/modules/workbench-my-task.vue | 107 -- .../modules/workbench-my-week-worklog.vue | 654 ++++++++++++- .../modules/workbench-notice-notification.vue | 261 ++++- .../modules/workbench-personal-item.vue | 73 -- .../modules/workbench-product-snapshot.vue | 2 +- .../modules/workbench-project-grid.vue | 329 ++++++- .../modules/workbench-project-health.vue | 42 + .../modules/workbench-project-snapshot.vue | 275 ------ .../modules/workbench-recent-visit.vue | 81 -- .../modules/workbench-risk-alert.vue | 124 --- .../modules/workbench-shortcut-picker.vue | 56 +- .../workbench/modules/workbench-shortcut.vue | 52 +- .../workbench/modules/workbench-team-load.vue | 304 ++++-- .../workbench/modules/workbench-team-todo.vue | 182 ---- .../modules/workbench-team-worklog.vue | 112 --- .../modules/workbench-ticket-sla.vue | 86 -- .../modules/workbench-todo-panel.vue | 10 +- .../modules/workbench-worklog-reminder.vue | 111 --- 54 files changed, 4726 insertions(+), 2720 deletions(-) create mode 100644 src/components/custom/business-user-picker.vue create mode 100644 src/components/custom/business-user-picker/components/user-picker-trigger.vue create mode 100644 src/components/custom/business-user-picker/composables/use-chain-source.ts create mode 100644 src/components/custom/business-user-picker/composables/use-dept-source.ts create mode 100644 src/components/custom/business-user-picker/composables/use-picker-selection.ts create mode 100644 src/layouts/modules/global-header/components/notification-bell.vue create mode 100644 src/views/workbench/composables/use-workbench-colors.ts delete mode 100644 src/views/workbench/modules/workbench-approval.vue delete mode 100644 src/views/workbench/modules/workbench-favorite.vue delete mode 100644 src/views/workbench/modules/workbench-mentions.vue delete mode 100644 src/views/workbench/modules/workbench-my-completion-rate.vue delete mode 100644 src/views/workbench/modules/workbench-my-requirement.vue delete mode 100644 src/views/workbench/modules/workbench-my-task.vue delete mode 100644 src/views/workbench/modules/workbench-personal-item.vue delete mode 100644 src/views/workbench/modules/workbench-project-snapshot.vue delete mode 100644 src/views/workbench/modules/workbench-recent-visit.vue delete mode 100644 src/views/workbench/modules/workbench-risk-alert.vue delete mode 100644 src/views/workbench/modules/workbench-team-todo.vue delete mode 100644 src/views/workbench/modules/workbench-team-worklog.vue delete mode 100644 src/views/workbench/modules/workbench-ticket-sla.vue delete mode 100644 src/views/workbench/modules/workbench-worklog-reminder.vue diff --git a/src/components/custom/business-rich-text-view.vue b/src/components/custom/business-rich-text-view.vue index 5a7f9ff..4d4ab37 100644 --- a/src/components/custom/business-rich-text-view.vue +++ b/src/components/custom/business-rich-text-view.vue @@ -21,6 +21,7 @@ const isEmpty = computed(() => !safeHtml.value || safeHtml.value.replace(/<[^>]+ {{ props.emptyText }} + diff --git a/src/components/custom/business-user-picker.vue b/src/components/custom/business-user-picker.vue new file mode 100644 index 0000000..3faea0e --- /dev/null +++ b/src/components/custom/business-user-picker.vue @@ -0,0 +1,920 @@ + + + + + + + + + + + + + {{ tab === 'dept' ? '部门' : tab === 'chain' ? '团队' : '全部用户' }} + + + + + + {{ source === 'dept' ? '部门' : '团队' }} + + + + + + + + + + {{ data.name }} + + {{ deptSource.getMetaText(data) }} + + + + + + + + + + {{ data.userNickname }} + + {{ chainSource.getMetaText(data) }} + + + + + + + + + + + 候选用户( + {{ filteredUserIds.length }} + 人) + + + 隐藏已添加 + + + + + + + + 该节点下没有匹配用户 + + 清除筛选条件 + + + + + {{ (getUserById(uid)?.nickname ?? '?').slice(0, 1) }} + + {{ getUserById(uid)?.nickname }} + + + {{ disabledLabel }} + + + + + + + + + + 已选 + {{ selection.size.value }} + 人 + + + 清空 + + + 从左侧勾选用户后会出现在这里 + + + + {{ getUserById(uid)?.nickname }} + + ·{{ disabledLabel }} + + + + × + + + + + + +{{ overflowSelectedCount }} 更多 + + + + + 另外 + {{ overflowSelectedCount }} + 人 + + + + + + {{ getUserById(uid)?.nickname }} + + ·{{ disabledLabel }} + + + + × + + + + + + + + + + + + diff --git a/src/components/custom/business-user-picker/components/user-picker-trigger.vue b/src/components/custom/business-user-picker/components/user-picker-trigger.vue new file mode 100644 index 0000000..9e023e4 --- /dev/null +++ b/src/components/custom/business-user-picker/components/user-picker-trigger.vue @@ -0,0 +1,127 @@ + + + + + {{ displayText }} + {{ placeholder }} + + + + + + + diff --git a/src/components/custom/business-user-picker/composables/use-chain-source.ts b/src/components/custom/business-user-picker/composables/use-chain-source.ts new file mode 100644 index 0000000..635e79b --- /dev/null +++ b/src/components/custom/business-user-picker/composables/use-chain-source.ts @@ -0,0 +1,90 @@ +import { computed, ref } from 'vue'; +import { fetchGetUserManagementRelationTree } from '@/service/api'; +import type { TreeCheckState } from './use-dept-source'; + +type ChainNode = Api.SystemManage.UserManagementRelationTreeRespVO; + +export function useChainSource(selectedIds: () => Set, disabledUserIdSet: () => Set) { + const tree = ref([]); + const loading = ref(false); + let loaded = false; + + async function ensureLoaded() { + if (loaded) return; + loading.value = true; + try { + const { data } = await fetchGetUserManagementRelationTree({ fromUserIndex: false }); + tree.value = data ?? []; + loaded = true; + } finally { + loading.value = false; + } + } + + function nodeKey(node: ChainNode): string { + return node.id ?? `chain_${node.userId}`; + } + + function getNodeUserIds(node: ChainNode): string[] { + const ids = new Set([String(node.userId)]); + if (node.children) { + for (const c of node.children) { + for (const id of getNodeUserIds(c)) ids.add(id); + } + } + return [...ids]; + } + + function getNodeCheckState(node: ChainNode): TreeCheckState { + const ids = getNodeUserIds(node).filter(id => !disabledUserIdSet().has(id)); + if (!ids.length) return 'none'; + const sel = ids.filter(id => selectedIds().has(id)).length; + if (sel === 0) return 'none'; + if (sel === ids.length) return 'all'; + return 'partial'; + } + + function findNode(list: ChainNode[], key: string): ChainNode | null { + for (const n of list) { + if (nodeKey(n) === key) return n; + if (n.children) { + const r = findNode(n.children, key); + if (r) return r; + } + } + return null; + } + + function matchKeyword(node: ChainNode, kw: string): boolean { + if (!kw) return true; + if (node.userNickname.toLowerCase().includes(kw)) return true; + if (node.children) return node.children.some(c => matchKeyword(c, kw)); + return false; + } + + function filterByKeyword(kw: string) { + const lower = kw.trim().toLowerCase(); + if (!lower) return tree.value; + return tree.value.filter(n => matchKeyword(n, lower)); + } + + function getMetaText(node: ChainNode): string { + const total = getNodeUserIds(node).length; + return total > 1 ? `${total} 人` : ''; + } + + const treeProps = computed(() => ({ children: 'children', label: 'userNickname' }) as const); + + return { + tree, + loading, + treeProps, + ensureLoaded, + getNodeUserIds, + getNodeCheckState, + findNode, + filterByKeyword, + getMetaText, + nodeKey + }; +} diff --git a/src/components/custom/business-user-picker/composables/use-dept-source.ts b/src/components/custom/business-user-picker/composables/use-dept-source.ts new file mode 100644 index 0000000..ae7183e --- /dev/null +++ b/src/components/custom/business-user-picker/composables/use-dept-source.ts @@ -0,0 +1,99 @@ +import { computed, ref } from 'vue'; +import { fetchGetDeptSimpleList } from '@/service/api'; +import { buildMenuTree } from '@/views/system/shared/menu-tree'; + +export type TreeCheckState = 'none' | 'partial' | 'all'; + +export function useDeptSource( + userOptions: () => Api.SystemManage.UserSimple[], + selectedIds: () => Set, + disabledUserIdSet: () => Set +) { + const tree = ref([]); + const loading = ref(false); + let loaded = false; + + async function ensureLoaded() { + if (loaded) return; + loading.value = true; + try { + const { data } = await fetchGetDeptSimpleList(); + tree.value = data ? buildMenuTree(data) : []; + loaded = true; + } finally { + loading.value = false; + } + } + + function collectDeptIds(node: Api.SystemManage.DeptSimple): string[] { + const ids: string[] = [String(node.id)]; + if (node.children) { + for (const c of node.children) ids.push(...collectDeptIds(c)); + } + return ids; + } + + function getNodeUserIds(node: Api.SystemManage.DeptSimple): string[] { + const deptIds = new Set(collectDeptIds(node)); + return userOptions() + .filter(u => u.deptId !== null && u.deptId !== undefined && deptIds.has(String(u.deptId))) + .map(u => String(u.id)); + } + + function getNodeCheckState(node: Api.SystemManage.DeptSimple): TreeCheckState { + const ids = getNodeUserIds(node).filter(id => !disabledUserIdSet().has(id)); + if (!ids.length) return 'none'; + const sel = ids.filter(id => selectedIds().has(id)).length; + if (sel === 0) return 'none'; + if (sel === ids.length) return 'all'; + return 'partial'; + } + + function findNode(list: Api.SystemManage.DeptSimple[], key: string): Api.SystemManage.DeptSimple | null { + for (const n of list) { + if (String(n.id) === key) return n; + if (n.children) { + const r = findNode(n.children, key); + if (r) return r; + } + } + return null; + } + + function matchKeyword(node: Api.SystemManage.DeptSimple, kw: string): boolean { + if (!kw) return true; + if (node.name.toLowerCase().includes(kw)) return true; + if (node.children) return node.children.some(c => matchKeyword(c, kw)); + return false; + } + + function filterByKeyword(kw: string) { + const lower = kw.trim().toLowerCase(); + if (!lower) return tree.value; + return tree.value.filter(n => matchKeyword(n, lower)); + } + + function getMetaText(node: Api.SystemManage.DeptSimple): string { + const total = getNodeUserIds(node).length; + return total > 0 ? `${total} 人` : ''; + } + + function nodeKey(node: Api.SystemManage.DeptSimple): string { + return String(node.id); + } + + const treeProps = computed(() => ({ children: 'children', label: 'name' }) as const); + + return { + tree, + loading, + treeProps, + ensureLoaded, + getNodeUserIds, + getNodeCheckState, + findNode, + filterByKeyword, + getMetaText, + nodeKey + }; +} diff --git a/src/components/custom/business-user-picker/composables/use-picker-selection.ts b/src/components/custom/business-user-picker/composables/use-picker-selection.ts new file mode 100644 index 0000000..6b6c083 --- /dev/null +++ b/src/components/custom/business-user-picker/composables/use-picker-selection.ts @@ -0,0 +1,89 @@ +import { computed, ref } from 'vue'; + +export interface PickerSelectionOptions { + multiple: boolean; +} + +export function usePickerSelection(options: () => PickerSelectionOptions) { + const multiSet = ref>(new Set()); + const singleId = ref(null); + + const multiple = computed(() => options().multiple); + + function has(userId: string): boolean { + if (multiple.value) return multiSet.value.has(userId); + return singleId.value === userId; + } + + function toggle(userId: string) { + if (multiple.value) { + if (multiSet.value.has(userId)) multiSet.value.delete(userId); + else multiSet.value.add(userId); + multiSet.value = new Set(multiSet.value); + } else { + singleId.value = singleId.value === userId ? null : userId; + } + } + + function addMany(userIds: readonly string[]) { + if (!multiple.value) { + singleId.value = userIds[0] ?? singleId.value; + return; + } + for (const id of userIds) multiSet.value.add(id); + multiSet.value = new Set(multiSet.value); + } + + function removeMany(userIds: readonly string[]) { + if (!multiple.value) { + if (singleId.value && userIds.includes(singleId.value)) singleId.value = null; + return; + } + for (const id of userIds) multiSet.value.delete(id); + multiSet.value = new Set(multiSet.value); + } + + function clear(preserveIds?: readonly string[]) { + const keep = new Set((preserveIds ?? []).map(String)); + if (multiple.value) { + const next = new Set(); + for (const id of multiSet.value) { + if (keep.has(id)) next.add(id); + } + multiSet.value = next; + } else if (singleId.value && !keep.has(singleId.value)) singleId.value = null; + } + + function reset(initial: string | string[] | null | undefined) { + if (multiple.value) { + const ids = Array.isArray(initial) ? initial.map(String) : []; + multiSet.value = new Set(ids); + } else { + singleId.value = typeof initial === 'string' ? initial : null; + } + } + + const selectedIds = computed(() => { + if (multiple.value) return [...multiSet.value]; + return singleId.value ? [singleId.value] : []; + }); + + const size = computed(() => selectedIds.value.length); + + function commit(): string | string[] | null { + if (multiple.value) return [...multiSet.value]; + return singleId.value; + } + + return { + selectedIds, + size, + has, + toggle, + addMany, + removeMany, + clear, + reset, + commit + }; +} diff --git a/src/layouts/modules/global-header/components/notification-bell.vue b/src/layouts/modules/global-header/components/notification-bell.vue new file mode 100644 index 0000000..a28fc4a --- /dev/null +++ b/src/layouts/modules/global-header/components/notification-bell.vue @@ -0,0 +1,457 @@ + + + + + + {{ badgeLabel }} + + + + + + + 通知 + 未读 {{ unreadCount }} + + + 全部已读 + + + + + + + + + + + + + + + + + + + 未读 + {{ filteredUnread.length }} + + + + + + + + {{ row.title }} + {{ row.timeLabel }} + + + + + {{ searchKeyword ? '没有匹配的通知' : '暂无未读通知' }} + + + + + + + + + 已读 + {{ filteredRead.length }} + + + + + + + + {{ row.title }} + {{ row.timeLabel }} + + + + + {{ searchKeyword ? '没有匹配的通知' : '暂无已读通知' }} + + + + + + + + + + diff --git a/src/layouts/modules/global-header/index.vue b/src/layouts/modules/global-header/index.vue index 164daa0..071774b 100644 --- a/src/layouts/modules/global-header/index.vue +++ b/src/layouts/modules/global-header/index.vue @@ -7,6 +7,7 @@ import GlobalLogo from '../global-logo/index.vue'; import GlobalBreadcrumb from '../global-breadcrumb/index.vue'; import GlobalSearch from '../global-search/index.vue'; import ThemeButton from './components/theme-button.vue'; +import NotificationBell from './components/notification-bell.vue'; import UserAvatar from './components/user-avatar.vue'; defineOptions({ name: 'GlobalHeader' }); @@ -48,6 +49,7 @@ const { isFullscreen, toggle } = useFullscreen(); + diff --git a/src/service/api/route.ts b/src/service/api/route.ts index d1f7f40..9952760 100644 --- a/src/service/api/route.ts +++ b/src/service/api/route.ts @@ -1,4 +1,3 @@ -import type { RouteMeta } from 'vue-router'; import type { ElegantConstRoute, LastLevelRouteKey } from '@elegant-router/types'; import { objectContextDomainConfigs } from '@/constants/object-context'; import { SYSTEM_SERVICE_PREFIX } from '@/constants/service'; diff --git a/src/typings/components.d.ts b/src/typings/components.d.ts index 093b12a..a9ae0ca 100644 --- a/src/typings/components.d.ts +++ b/src/typings/components.d.ts @@ -19,6 +19,7 @@ declare module 'vue' { BusinessFormSimpleDialog: typeof import('./../components/custom/business-form-simple-dialog.vue')['default'] BusinessRichTextEditor: typeof import('./../components/custom/business-rich-text-editor.vue')['default'] BusinessRichTextView: typeof import('./../components/custom/business-rich-text-view.vue')['default'] + BusinessUserPicker: typeof import('./../components/custom/business-user-picker.vue')['default'] 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'] @@ -181,6 +182,7 @@ declare module 'vue' { TableSearchFields: typeof import('./../components/custom/table-search-fields.vue')['default'] TableSearchPanel: typeof import('./../components/custom/table-search-panel.vue')['default'] ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default'] + UserPickerTrigger: typeof import('./../components/custom/business-user-picker/components/user-picker-trigger.vue')['default'] WaveBg: typeof import('./../components/custom/wave-bg.vue')['default'] WebSiteLink: typeof import('./../components/custom/web-site-link.vue')['default'] } diff --git a/src/views/personal-center/my-item/modules/personal-item-detail-dialog.vue b/src/views/personal-center/my-item/modules/personal-item-detail-dialog.vue index c2761bd..118d085 100644 --- a/src/views/personal-center/my-item/modules/personal-item-detail-dialog.vue +++ b/src/views/personal-center/my-item/modules/personal-item-detail-dialog.vue @@ -30,6 +30,7 @@ interface Props { } const props = withDefaults(defineProps(), { + rowData: null, defaultTab: 'worklog' }); diff --git a/src/views/product/list/modules/product-create-base-form.vue b/src/views/product/list/modules/product-create-base-form.vue index 1f5a340..5bf4c6b 100644 --- a/src/views/product/list/modules/product-create-base-form.vue +++ b/src/views/product/list/modules/product-create-base-form.vue @@ -2,7 +2,7 @@ import { computed } from 'vue'; import { RDMS_OBJECT_DIRECTION_DICT_CODE } from '@/constants/dict'; import { useForm, useFormRules } from '@/hooks/common/form'; -import BusinessUserSelect from '@/components/custom/business-user-select.vue'; +import BusinessUserPicker from '@/components/custom/business-user-picker.vue'; import DictSelect from '@/components/custom/dict-select.vue'; defineOptions({ name: 'ProductCreateBaseForm' }); @@ -72,9 +72,10 @@ defineExpose({ validate: runValidate }); - diff --git a/src/views/product/requirement/modules/module-tree-node.vue b/src/views/product/requirement/modules/module-tree-node.vue index dede9bd..b6381de 100644 --- a/src/views/product/requirement/modules/module-tree-node.vue +++ b/src/views/product/requirement/modules/module-tree-node.vue @@ -17,7 +17,14 @@ interface Props { } const props = withDefaults(defineProps(), { - level: 0 + level: 0, + selectedModuleId: undefined, + editingNodeId: undefined, + editingName: undefined, + addingChildParentId: undefined, + newChildModuleName: undefined, + rootModuleId: undefined, + moduleRequirementCountMap: undefined }); const emit = defineEmits([ diff --git a/src/views/product/requirement/modules/requirement-split-dialog.vue b/src/views/product/requirement/modules/requirement-split-dialog.vue index 968a857..d2582f9 100644 --- a/src/views/product/requirement/modules/requirement-split-dialog.vue +++ b/src/views/product/requirement/modules/requirement-split-dialog.vue @@ -1,7 +1,6 @@ - - - - - - - {{ item.title }} - {{ item.meta }} - - - 批准 - 驳回 - - - - - - - diff --git a/src/views/workbench/modules/workbench-banner.vue b/src/views/workbench/modules/workbench-banner.vue index 1d783a8..ab2c402 100644 --- a/src/views/workbench/modules/workbench-banner.vue +++ b/src/views/workbench/modules/workbench-banner.vue @@ -1,273 +1,302 @@ - - {{ greeting }},{{ displayName }} - - {{ todayLabel }} - - - - 今日待办 - {{ summary.todoCount }} - 项 - - · - - 即将到期 - - {{ summary.upcomingCount }} - - 项 - - + {{ greeting }},{{ displayName }} + + {{ dateContext.date }} {{ dateContext.weekday }} + · + {{ dateContext.week }} + - - - 本周节奏 - 完成率 {{ summary.weekCompletionRate }}% - - - - - - - {{ item.label }} - {{ item.value }} + + + + + 公告 + {{ allNotices.length }} + + + 更多 + + + + + + {{ row.title }} + {{ row.timeLabel }} + + + + + + {{ row.title }} + {{ row.timeLabel }} + + + + + 关闭 + + diff --git a/src/views/workbench/modules/workbench-edit-overlay.vue b/src/views/workbench/modules/workbench-edit-overlay.vue index af7a08a..f2a9e24 100644 --- a/src/views/workbench/modules/workbench-edit-overlay.vue +++ b/src/views/workbench/modules/workbench-edit-overlay.vue @@ -8,6 +8,7 @@ const emit = defineEmits<{ (e: 'save'): void; (e: 'cancel'): void; (e: 'reset'): void; + (e: 'open-library'): void; }>(); @@ -18,6 +19,10 @@ const emit = defineEmits<{ 正在编辑布局——拖动模块换位 / 抽屉里把隐藏模块拖回来 + + + 模块库 + 重置默认布局 取消 @@ -50,4 +55,7 @@ const emit = defineEmits<{ display: inline-flex; gap: 8px; } +.edit-overlay__btn-icon { + margin-right: 4px; +} diff --git a/src/views/workbench/modules/workbench-favorite.vue b/src/views/workbench/modules/workbench-favorite.vue deleted file mode 100644 index 20c6597..0000000 --- a/src/views/workbench/modules/workbench-favorite.vue +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - v1 仅展示 mock;业务页"收藏"入口将在 v2 加入。 - - - - {{ item.kindLabel }} - {{ item.title }} - {{ item.source }} - - - - - - diff --git a/src/views/workbench/modules/workbench-mentions.vue b/src/views/workbench/modules/workbench-mentions.vue deleted file mode 100644 index a926e9f..0000000 --- a/src/views/workbench/modules/workbench-mentions.vue +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - - {{ item.fromAvatar }} - - - {{ item.fromName }} - {{ item.context }} - - {{ item.timeLabel }} - - - - - - - diff --git a/src/views/workbench/modules/workbench-module-card.vue b/src/views/workbench/modules/workbench-module-card.vue index 6b91bda..75908e6 100644 --- a/src/views/workbench/modules/workbench-module-card.vue +++ b/src/views/workbench/modules/workbench-module-card.vue @@ -1,8 +1,12 @@ @@ -42,21 +58,23 @@ const categoryTagType: Record 点击下方模块加入工作台(默认进左栏)。 - - 所有模块都已显示 - - - {{ meta.displayName }} - - {{ categoryLabel[meta.category] }} - - - + 所有模块都已显示 + + + {{ group.label }} + + + + {{ meta.displayName }} + + + + @@ -68,6 +86,18 @@ const categoryTagType: Record -import WorkbenchModuleCard from './workbench-module-card.vue'; - -defineOptions({ name: 'WorkbenchMyCompletionRate' }); - -interface Props { - editing?: boolean; - collapsed?: boolean; -} -withDefaults(defineProps(), { editing: false, collapsed: false }); -defineEmits<{ (e: 'hide'): void; (e: 'toggle-collapse'): void }>(); - -const rate = 72; // % -const teamAvg = 65; -const total = 25; -const completed = 18; -const onTime = 13; -const overdue = 5; -const diff = rate - teamAvg; - - - - - - - - {{ rate }}% - 按时完成 - - - - 团队均值 {{ teamAvg }}% - - 任务完成 - {{ completed }} - / {{ total }} - - - 按时 - {{ onTime }} - · 逾期 - {{ overdue }} - - - {{ diff >= 0 ? `高于团队均值 +${diff}%` : `低于团队均值 ${diff}%` }} - - - - 统计近 30 天 - - - - diff --git a/src/views/workbench/modules/workbench-my-execution.vue b/src/views/workbench/modules/workbench-my-execution.vue index b01044b..1bf0b2a 100644 --- a/src/views/workbench/modules/workbench-my-execution.vue +++ b/src/views/workbench/modules/workbench-my-execution.vue @@ -1,4 +1,12 @@ - - - - {{ row.name }} - {{ row.progress }}% - - - {{ row.project }} - · - {{ row.done }} / {{ row.total }} 任务完成 - · - {{ row.statusLabel }} - - - - - - + + + + + + {{ group.projectName }} + + {{ group.items.length }} + + + + + {{ item.executionName }} + + + {{ item.statusName }} + + + + + + + 计划 {{ formatDateRange(item.plannedStartDate, item.plannedEndDate) }} + + + + + + 实际 {{ formatDateRange(item.actualStartDate, item.actualEndDate) }} + + + + + + {{ item.projectRequirementName }} + + + + + + + + + diff --git a/src/views/workbench/modules/workbench-my-requirement.vue b/src/views/workbench/modules/workbench-my-requirement.vue deleted file mode 100644 index f31b1b6..0000000 --- a/src/views/workbench/modules/workbench-my-requirement.vue +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - {{ group.statusLabel }} - {{ group.count }} - - - - - - diff --git a/src/views/workbench/modules/workbench-my-task.vue b/src/views/workbench/modules/workbench-my-task.vue deleted file mode 100644 index ec99d4a..0000000 --- a/src/views/workbench/modules/workbench-my-task.vue +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - {{ doneCount }} / {{ totalCount }} 已完成 - - - - - {{ item.title }} - - - - 当日累计工时: - {{ todayHours }}h - / {{ targetHours }}h - - - - - diff --git a/src/views/workbench/modules/workbench-my-week-worklog.vue b/src/views/workbench/modules/workbench-my-week-worklog.vue index d0f541d..cc38003 100644 --- a/src/views/workbench/modules/workbench-my-week-worklog.vue +++ b/src/views/workbench/modules/workbench-my-week-worklog.vue @@ -1,4 +1,18 @@ - - - - - - - {{ d }} + + + + + + - - - 累计 - {{ todayProgress }}h - / {{ target }}h - - {{ deltaText }} + + + + + + + + 工时分布 + + + + 每日工时 + + + + + + + + + + + + + + + + 按天填 + + + + 按周均分 + + + + + + + + + + + + + + + + 填报率 + + {{ teamView.fillRate }} + % + + {{ teamView.totalHours }}h / {{ teamView.expectedTotalHours }}h + + + 团队均值 + + {{ teamView.averageHours }} + h + + {{ teamView.members.length }} 人 + + + 偏低 + + {{ teamView.lowCount }} + 人 + + 低于均值 80% + + + 加班 + + {{ teamView.highCount }} + 人 + + 超 45h + + + + + + + + - 本周总和(含今日):{{ total.toFixed(1) }}h diff --git a/src/views/workbench/modules/workbench-notice-notification.vue b/src/views/workbench/modules/workbench-notice-notification.vue index 53e76bc..cd5c30d 100644 --- a/src/views/workbench/modules/workbench-notice-notification.vue +++ b/src/views/workbench/modules/workbench-notice-notification.vue @@ -1,4 +1,5 @@ - - 📢 公告 + + + + + 公告 + {{ notices.length }} + - - {{ row.title }} - {{ row.timeLabel }} + + {{ row.title }} + {{ row.timeLabel }} - - - 🔔 系统通知(未读 {{ notifications.length }}) + + + + + + + 通知 + 未读 {{ unreadCount }} + {{ notifications.length }} + + 全部已读 + + - - {{ row.title }} - {{ row.timeLabel }} + + + {{ row.title }} + {{ row.timeLabel }} + + + + + + + + + + + + - + @@ -65,50 +131,163 @@ const notifications: Row[] = [ diff --git a/src/views/workbench/modules/workbench-personal-item.vue b/src/views/workbench/modules/workbench-personal-item.vue deleted file mode 100644 index 1ce0e39..0000000 --- a/src/views/workbench/modules/workbench-personal-item.vue +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - {{ item.title }} - - - - - - diff --git a/src/views/workbench/modules/workbench-product-snapshot.vue b/src/views/workbench/modules/workbench-product-snapshot.vue index e5f09e1..ec9ae6e 100644 --- a/src/views/workbench/modules/workbench-product-snapshot.vue +++ b/src/views/workbench/modules/workbench-product-snapshot.vue @@ -77,7 +77,7 @@ function onChange(id: string) { @toggle-collapse="$emit('toggle-collapse')" > - pin + 当前产品: diff --git a/src/views/workbench/modules/workbench-project-grid.vue b/src/views/workbench/modules/workbench-project-grid.vue index bc7a92b..1a8bd8f 100644 --- a/src/views/workbench/modules/workbench-project-grid.vue +++ b/src/views/workbench/modules/workbench-project-grid.vue @@ -1,8 +1,8 @@ - - - - - pin - - - - - - - - {{ pinned.progress }}% - - - - {{ pinned.executionCount }} - 执行 - - - {{ pinned.taskCount }} - 任务 - - - {{ pinned.memberCount }} - 成员 - - - {{ pinned.overdueCount }} - 逾期 - - - - 剩 {{ pinned.remainingDays }} 天 · 我的角色:{{ pinned.myRole }} - - 📌 本周关键节点 - - - {{ m.title }} - {{ m.timeLabel }} - - - - 👥 成员负载 - - - {{ m.name }} - - {{ Math.round(m.load / 10) }} - - - - - - diff --git a/src/views/workbench/modules/workbench-recent-visit.vue b/src/views/workbench/modules/workbench-recent-visit.vue deleted file mode 100644 index d64d75e..0000000 --- a/src/views/workbench/modules/workbench-recent-visit.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - {{ row.type }} - {{ row.title }} - {{ row.timeLabel }} - - - - - - diff --git a/src/views/workbench/modules/workbench-risk-alert.vue b/src/views/workbench/modules/workbench-risk-alert.vue deleted file mode 100644 index 5d11f95..0000000 --- a/src/views/workbench/modules/workbench-risk-alert.vue +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - {{ s.n }} - {{ s.label }} - - - - - {{ row.title }} · {{ row.owner }} - {{ row.sub }} - - - - - - diff --git a/src/views/workbench/modules/workbench-shortcut-picker.vue b/src/views/workbench/modules/workbench-shortcut-picker.vue index 1b29e6f..75ff444 100644 --- a/src/views/workbench/modules/workbench-shortcut-picker.vue +++ b/src/views/workbench/modules/workbench-shortcut-picker.vue @@ -1,6 +1,8 @@ @@ -40,69 +38,243 @@ function level(n: number): 'ok' | 'warn' | 'over' { @hide="$emit('hide')" @toggle-collapse="$emit('toggle-collapse')" > - - - {{ row.name }} - - + + + 高负载 + + {{ view.highCount }} + 人 + + + + 中负载 + + {{ view.midCount }} + 人 + + + + 临期 + 逾期 + + {{ view.urgentTotal }} + 条 + + + + + + + + {{ m.memberName }} + + + + + + + +{{ m.overflowExtra }} - - {{ row.inProgress }}{{ level(row.inProgress) === 'over' ? ' ⚠' : '' }} + + + {{ m.inProgress }} + 进行 + + + + + {{ m.urgent }} + 临期 + + + + - 阈值 ≥6 高负载 · ≥4 中负载 + + 高 = 进行中 ≥ 6 或 临期+逾期 ≥ 2 · 中 = 进行中 ≥ 4 或 临期+逾期 ≥ 1 diff --git a/src/views/workbench/modules/workbench-team-todo.vue b/src/views/workbench/modules/workbench-team-todo.vue deleted file mode 100644 index 5f07af9..0000000 --- a/src/views/workbench/modules/workbench-team-todo.vue +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - - - {{ col.name }} - {{ col.total }}{{ col.warn ? ' ⚠' : '' }} - - - {{ task.title }} - - {{ task.priority }} - {{ task.deadline }} - - - +{{ col.more }} 个 - - - - - - diff --git a/src/views/workbench/modules/workbench-team-worklog.vue b/src/views/workbench/modules/workbench-team-worklog.vue deleted file mode 100644 index e19294c..0000000 --- a/src/views/workbench/modules/workbench-team-worklog.vue +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - {{ m.hours }}h - - - - - {{ m.name }} - - - 平均 {{ avg }}h / 人 · - {{ lowest.name }} - 工时偏低 · - {{ highest.name }} - 超 40h - - - - - diff --git a/src/views/workbench/modules/workbench-ticket-sla.vue b/src/views/workbench/modules/workbench-ticket-sla.vue deleted file mode 100644 index 70673a1..0000000 --- a/src/views/workbench/modules/workbench-ticket-sla.vue +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - 工单业务暂未上线,当前为 mock 数据;正式接口落地后接通。 - - - - {{ unclosed }} - 未关闭 - - - {{ overtime }} - 超时 - - - {{ willOvertime }} - 将超时 - - - 按优先级:高 {{ byPriority.high }} · 中 {{ byPriority.mid }} · 低 {{ byPriority.low }} - - - - diff --git a/src/views/workbench/modules/workbench-todo-panel.vue b/src/views/workbench/modules/workbench-todo-panel.vue index 641af6e..85ae25c 100644 --- a/src/views/workbench/modules/workbench-todo-panel.vue +++ b/src/views/workbench/modules/workbench-todo-panel.vue @@ -57,7 +57,7 @@ const mainTabs: Array<{ key: WorkbenchTodoMainTab; label: string }> = [ { key: 'task', label: '任务' }, { key: 'ticket', label: '工单' }, { key: 'personal', label: '个人事项' }, - { key: 'review', label: '待评审' } + { key: 'approval', label: '待审批' } ]; const deadlineFilters: Array<{ key: Exclude; label: string }> = [ @@ -85,7 +85,7 @@ const tabCounts = computed(() => { task: 0, ticket: 0, personal: 0, - review: 0 + approval: 0 }; allItems.value.forEach(item => { counts[item.category] += 1; @@ -99,7 +99,7 @@ const tabOverdueCount = computed(() => { task: 0, ticket: 0, personal: 0, - review: 0 + approval: 0 }; allItems.value.forEach(item => { if (!isWorkbenchTodoOverdue(item)) return; @@ -144,6 +144,7 @@ watch([activeTab, activeDeadlineFilter, activeSort], () => { function handleSelectTab(key: WorkbenchTodoMainTab) { if (activeTab.value === key) return; activeTab.value = key; + if (key === 'approval') activeDeadlineFilter.value = null; activeDeadlineFilter.value = null; if (key !== 'task' && activeSort.value === 'priority') { activeSort.value = 'deadline'; @@ -209,7 +210,7 @@ function getDeadlineToneClass(item: WorkbenchTodoItem) { - + + diff --git a/src/views/workbench/modules/workbench-worklog-reminder.vue b/src/views/workbench/modules/workbench-worklog-reminder.vue deleted file mode 100644 index 9bb4897..0000000 --- a/src/views/workbench/modules/workbench-worklog-reminder.vue +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - {{ today }} - {{ todayHint }} - - 立即填报 - - - - {{ row.dateLabel }} - 已填 {{ row.hours }}h - 补填 - - - - - -
{{ todayLabel }}
+ {{ dateContext.date }} {{ dateContext.weekday }} + · + {{ dateContext.week }} +
点击下方模块加入工作台(默认进左栏)。