feat(projects): 工作台部分组件调成真实数据

This commit is contained in:
2026-06-04 11:26:51 +08:00
parent acef4418d8
commit 39458386ae
33 changed files with 1033 additions and 1169 deletions

View File

@@ -1,11 +1,11 @@
import { computed, ref } from 'vue';
import { useDebounceFn } from '@vueuse/core';
import { type WorkbenchColumnId, type WorkbenchModuleKey, useWorkbenchModules } from './use-workbench-modules';
import { type WorkbenchModuleKey, useWorkbenchModules } from './use-workbench-modules';
import { buildDefaultLayout } from './workbench-layout-default';
import type { LayoutStorage } from './layout-storage';
import { LocalStorageAdapter } from './layout-storage-local';
import { reconcileLayout } from './workbench-layout-reconcile';
import { WORKBENCH_LAYOUT_VERSION, type WorkbenchLayout } from './workbench-layout-types';
import { WORKBENCH_LAYOUT_VERSION, type WorkbenchGridItem, type WorkbenchLayout } from './workbench-layout-types';
export type WorkbenchMode = 'normal' | 'editing';
@@ -56,7 +56,7 @@ export function useWorkbenchLayout(options: UseWorkbenchLayoutOptions) {
if (mode.value === 'editing') {
dirty.value = true;
} else {
// 非编辑态写(如折叠)直接落盘
// 非编辑态写直接落盘
persist();
}
}
@@ -91,32 +91,31 @@ export function useWorkbenchLayout(options: UseWorkbenchLayoutOptions) {
}
function hideModule(key: WorkbenchModuleKey) {
for (const col of layout.value.columns) {
col.modules = col.modules.filter(k => k !== key);
}
layout.value.grid = layout.value.grid.filter(item => item.i !== key);
if (!layout.value.hidden.includes(key)) layout.value.hidden.push(key);
markDirty();
}
function showModule(key: WorkbenchModuleKey, columnId: WorkbenchColumnId = 'left') {
function showModule(key: WorkbenchModuleKey) {
if (layout.value.grid.some(item => item.i === key)) return;
layout.value.hidden = layout.value.hidden.filter(k => k !== key);
const target = layout.value.columns.find(c => c.id === columnId);
if (target && !target.modules.includes(key)) target.modules.push(key);
const meta = getAllModules().find(m => m.key === key);
if (!meta) return;
const nextY = layout.value.grid.reduce((max, g) => Math.max(max, g.y + g.h), 0);
layout.value.grid.push({
i: key,
x: meta.defaultGrid.x,
y: nextY,
w: meta.defaultGrid.w,
h: meta.defaultGrid.h,
minW: meta.defaultGrid.minW,
minH: meta.defaultGrid.minH
});
markDirty();
}
function setColumnModules(columnId: WorkbenchColumnId, modules: WorkbenchModuleKey[]) {
const target = layout.value.columns.find(c => c.id === columnId);
if (target) target.modules = modules;
markDirty();
}
function toggleCollapse(key: WorkbenchModuleKey) {
if (layout.value.collapsed.includes(key)) {
layout.value.collapsed = layout.value.collapsed.filter(k => k !== key);
} else {
layout.value.collapsed.push(key);
}
function updateGrid(grid: WorkbenchGridItem[]) {
layout.value.grid = grid;
markDirty();
}
@@ -129,15 +128,16 @@ export function useWorkbenchLayout(options: UseWorkbenchLayoutOptions) {
}
async function resetToDefault() {
layout.value = buildDefaultLayout(getAllModules());
const fresh = buildDefaultLayout(getAllModules());
// 重置只针对布局(位置/尺寸/显隐);用户偏好(如 shortcut.menuKeys原样保留
fresh.settings = { ...layout.value.settings };
layout.value = fresh;
mode.value = 'normal';
dirty.value = false;
snapshotBeforeEdit = null;
await storage.save(options.userId, layout.value);
}
const isCollapsed = (key: WorkbenchModuleKey) => layout.value.collapsed.includes(key);
const hiddenMetas = computed(() => {
const allMeta = getAllModules();
return layout.value.hidden
@@ -152,15 +152,13 @@ export function useWorkbenchLayout(options: UseWorkbenchLayoutOptions) {
saving,
error,
hiddenMetas,
isCollapsed,
load,
enterEditing,
saveEditing,
cancelEditing,
hideModule,
showModule,
setColumnModules,
toggleCollapse,
updateGrid,
updateModuleSettings,
resetToDefault
};

View File

@@ -11,12 +11,10 @@ export type WorkbenchModuleKey =
| 'myExecution' // B8 · 我负责的执行
| 'productSnapshot' // F24 · 产品深度快照(对象快照 / 当前对象切换)
| 'teamLoad' // C13 · 团队负载(管理者)
| 'myWeekWorklog' // D16 · 工时(含「我的工时 / 团队工时」两 tab原 C12 teamWorklog 已并入)
| 'noticeNotification'; // E22 · 公告 + 通知摘要
| 'myWeekWorklog'; // D16 · 工时(含「我的工时 / 团队工时」两 tab原 C12 teamWorklog 已并入)
// 扩展action动作型 widget、snapshot对象快照型 widget需指定一个对象
export type WorkbenchModuleCategory = 'personal' | 'manager' | 'tool' | 'action' | 'snapshot';
export type WorkbenchColumnId = 'left' | 'right';
export interface WorkbenchModuleMeta {
key: WorkbenchModuleKey;
@@ -25,17 +23,17 @@ export interface WorkbenchModuleMeta {
icon: string;
category: WorkbenchModuleCategory;
defaultVisible: boolean;
defaultColumn: WorkbenchColumnId;
defaultOrder: number;
/** 默认网格位置与尺寸12 栅格。hidden 项的 x/y 仅作占位show 时动态找空位。 */
defaultGrid: { x: number; y: number; w: number; h: number; minW: number; minH: number };
}
const placeholder = markRaw({ render: () => null });
// 默认布局2026-05-27 调整,对应 WORKBENCH_LAYOUT_VERSION=3
// left: myTodo(1) → myExecution(2)
// right: shortcut(1) → myProject(2) → myWeekWorklog(3) → teamLoad(4)
// hidden: projectHealth, noticeNotification, productSnapshot
// noticeNotification 隐藏原因:公告搬到 banner、通知归全局头部铃铛
// 默认布局2026-06-01 固化用户实拍布局,对应 WORKBENCH_LAYOUT_VERSION=5
// 左列x=0 w=7myTodo(y=0 h=25) → myWeekWorklog(y=25 h=22)
// 右列x=7 w=5shortcut(y=0 h=11) → myProject(y=11 h=17) → myExecution(y=28 h=19)
// 底部满宽x=0 w=12teamLoad(y=47 h=16)
// hiddenx/y 为占位show 时动态落到网格底部projectHealth、productSnapshot
const registry: WorkbenchModuleMeta[] = [
{
key: 'myTodo',
@@ -44,8 +42,8 @@ const registry: WorkbenchModuleMeta[] = [
icon: 'mdi:clipboard-text-clock-outline',
category: 'personal',
defaultVisible: true,
defaultColumn: 'left',
defaultOrder: 1
// minH 24 ≈ 608px保证至少完整展示 5 条待办(头部 124 + 5×71 列表 + 余量)
defaultGrid: { x: 0, y: 0, w: 7, h: 25, minW: 5, minH: 24 }
},
{
key: 'myExecution',
@@ -54,8 +52,7 @@ const registry: WorkbenchModuleMeta[] = [
icon: 'mdi:flag-checkered',
category: 'personal',
defaultVisible: true,
defaultColumn: 'left',
defaultOrder: 2
defaultGrid: { x: 7, y: 28, w: 5, h: 19, minW: 4, minH: 15 }
},
{
key: 'shortcut',
@@ -64,8 +61,7 @@ const registry: WorkbenchModuleMeta[] = [
icon: 'mdi:rocket-launch-outline',
category: 'tool',
defaultVisible: true,
defaultColumn: 'right',
defaultOrder: 1
defaultGrid: { x: 7, y: 0, w: 5, h: 11, minW: 3, minH: 10 }
},
{
key: 'myProject',
@@ -74,8 +70,7 @@ const registry: WorkbenchModuleMeta[] = [
icon: 'mdi:briefcase-outline',
category: 'personal',
defaultVisible: true,
defaultColumn: 'right',
defaultOrder: 2
defaultGrid: { x: 7, y: 11, w: 5, h: 17, minW: 5, minH: 17 }
},
{
key: 'myWeekWorklog',
@@ -84,8 +79,7 @@ const registry: WorkbenchModuleMeta[] = [
icon: 'mdi:timer-outline',
category: 'personal',
defaultVisible: true,
defaultColumn: 'right',
defaultOrder: 3
defaultGrid: { x: 0, y: 25, w: 7, h: 22, minW: 6, minH: 18 }
},
{
key: 'teamLoad',
@@ -94,8 +88,7 @@ const registry: WorkbenchModuleMeta[] = [
icon: 'mdi:scale-balance',
category: 'manager',
defaultVisible: true,
defaultColumn: 'right',
defaultOrder: 4
defaultGrid: { x: 0, y: 47, w: 12, h: 16, minW: 4, minH: 15 }
},
// === 默认隐藏(用户可从 widget 库拖回) ===
{
@@ -105,18 +98,7 @@ const registry: WorkbenchModuleMeta[] = [
icon: 'mdi:heart-pulse',
category: 'manager',
defaultVisible: false,
defaultColumn: 'right',
defaultOrder: 10
},
{
key: 'noticeNotification',
component: placeholder,
displayName: '公告 + 通知',
icon: 'mdi:bullhorn-outline',
category: 'tool',
defaultVisible: false,
defaultColumn: 'right',
defaultOrder: 11
defaultGrid: { x: 0, y: 0, w: 5, h: 12, minW: 4, minH: 9 }
},
{
key: 'productSnapshot',
@@ -125,8 +107,7 @@ const registry: WorkbenchModuleMeta[] = [
icon: 'mdi:image-area-close',
category: 'snapshot',
defaultVisible: false,
defaultColumn: 'left',
defaultOrder: 41
defaultGrid: { x: 0, y: 0, w: 6, h: 14, minW: 4, minH: 10 }
}
];

View File

@@ -0,0 +1,30 @@
import { ref } from 'vue';
/**
* 工作台 widget 统一刷新:卡片右上角刷新按钮触发,转 loading + 执行加载动作,并发期内忽略重复点击。
*
* - 已接真实接口的 widget传入 loader内部 await 拉取并回填数据)。
* - 尚未接接口的 mock widget不传 loader转一拍 loading 给出可感知反馈;接口接通后补 loader 即自动生效。
*/
export function useWorkbenchRefresh(loader?: () => Promise<void> | void) {
const loading = ref(false);
async function refresh() {
if (loading.value) return;
loading.value = true;
try {
if (loader) {
await loader();
} else {
// 占位mock widget 无真实数据源,转一拍 loading接口接通后传入 loader 替代
await new Promise<void>(resolve => {
setTimeout(resolve, 400);
});
}
} finally {
loading.value = false;
}
}
return { loading, refresh };
}

View File

@@ -2,29 +2,38 @@ import type { WorkbenchModuleMeta } from './use-workbench-modules';
import { WORKBENCH_LAYOUT_VERSION, type WorkbenchLayout } from './workbench-layout-types';
export function buildDefaultLayout(modules: WorkbenchModuleMeta[]): WorkbenchLayout {
const left = modules
.filter(m => m.defaultVisible && m.defaultColumn === 'left')
.sort((a, b) => a.defaultOrder - b.defaultOrder)
.map(m => m.key);
const grid = modules
.filter(m => m.defaultVisible)
.map(m => ({
i: m.key,
x: m.defaultGrid.x,
y: m.defaultGrid.y,
w: m.defaultGrid.w,
h: m.defaultGrid.h,
minW: m.defaultGrid.minW,
minH: m.defaultGrid.minH
}));
const right = modules
.filter(m => m.defaultVisible && m.defaultColumn === 'right')
.sort((a, b) => a.defaultOrder - b.defaultOrder)
.map(m => m.key);
const hidden = modules
.filter(m => !m.defaultVisible)
.sort((a, b) => a.defaultOrder - b.defaultOrder)
.map(m => m.key);
const hidden = modules.filter(m => !m.defaultVisible).map(m => m.key);
return {
version: WORKBENCH_LAYOUT_VERSION,
columns: [
{ id: 'left', modules: left },
{ id: 'right', modules: right }
],
grid,
hidden,
collapsed: [],
settings: {}
// 默认快捷入口(固化用户实拍选择);已有用户的旧 settings 在 load 时优先迁移,此默认仅作用于全新用户
settings: {
shortcut: {
menuKeys: [
'product_list',
'project_list',
'ticket_my-submitted',
'personal-center_my-weekly',
'personal-center_my-monthly',
'personal-center_my-performance',
'personal-center_my-application',
'infra_rd-code'
]
}
}
};
}

View File

@@ -1,31 +1,45 @@
import type { WorkbenchModuleKey, WorkbenchModuleMeta } from './use-workbench-modules';
import type { WorkbenchLayout } from './workbench-layout-types';
import type { WorkbenchGridItem, WorkbenchLayout } from './workbench-layout-types';
/**
* 把存量布局与当前模块注册中心对齐。
* - 注册中心存在但布局未含的 key按 defaultVisible 进 columns 或 hidden
* - 布局含但注册中心已删除的 key丢弃
* - 注册中心存在但布局未含的 key按 defaultVisible 落入网格底部或 hidden
*/
export function reconcileLayout(layout: WorkbenchLayout, modules: WorkbenchModuleMeta[]): WorkbenchLayout {
const knownKeys = new Set<WorkbenchModuleKey>(modules.map(m => m.key));
const filterKnown = (list: WorkbenchModuleKey[]) => list.filter(k => knownKeys.has(k));
const metaByKey = new Map<WorkbenchModuleKey, WorkbenchModuleMeta>(modules.map(m => [m.key, m]));
const columns = layout.columns.map(c => ({ id: c.id, modules: filterKnown(c.modules) }));
const hidden = filterKnown(layout.hidden);
const collapsed = filterKnown(layout.collapsed);
// 最小宽高是组件固有能力下限,始终以 meta 为准刷新(不被旧存储固化),并把 w/h clamp 到不低于下限
const grid: WorkbenchGridItem[] = layout.grid
.filter(item => knownKeys.has(item.i))
.map(item => {
const { minW, minH } = metaByKey.get(item.i)!.defaultGrid;
return { ...item, minW, minH, w: Math.max(item.w, minW), h: Math.max(item.h, minH) };
});
const hidden = layout.hidden.filter(k => knownKeys.has(k));
const appearKeys = new Set<WorkbenchModuleKey>([...columns.flatMap(c => c.modules), ...hidden]);
const appearKeys = new Set<WorkbenchModuleKey>([...grid.map(g => g.i), ...hidden]);
for (const m of modules) {
if (!appearKeys.has(m.key)) {
if (m.defaultVisible) {
const target = columns.find(c => c.id === m.defaultColumn) ?? columns[0];
target.modules.push(m.key);
} else {
hidden.push(m.key);
}
let nextY = grid.reduce((max, g) => Math.max(max, g.y + g.h), 0);
// 注册中心存在但布局未含的 key可见的落网格底部其余进 hidden
for (const m of modules.filter(item => !appearKeys.has(item.key))) {
if (m.defaultVisible) {
grid.push({
i: m.key,
x: m.defaultGrid.x,
y: nextY,
w: m.defaultGrid.w,
h: m.defaultGrid.h,
minW: m.defaultGrid.minW,
minH: m.defaultGrid.minH
});
nextY += m.defaultGrid.h;
} else {
hidden.push(m.key);
}
}
return { ...layout, columns, hidden, collapsed };
return { ...layout, grid, hidden };
}

View File

@@ -1,8 +1,9 @@
import type { WorkbenchColumnId, WorkbenchModuleKey } from './use-workbench-modules';
import type { WorkbenchModuleKey } from './use-workbench-modules';
// v3 (2026-05-27): myProject 移到右列、myExecution 顶替到 left 第 2 位、noticeNotification 默认隐藏(让位给 banner 公告 + 全局铃铛)
// 版本不匹配时 LocalStorageAdapter.load 直接丢弃存量布局走新默认
export const WORKBENCH_LAYOUT_VERSION = 3;
// v4 (2026-06-01): 两列排序 → 12 栅格自由网格。columns→grid移除 collapsed
// v5 (2026-06-01): 固化用户实拍布局为默认(坐标/尺寸 + 默认快捷入口 menuKeys删除 noticeNotification widget
// 版本不匹配时丢弃旧布局走新默认settings 原样迁移。
export const WORKBENCH_LAYOUT_VERSION = 5;
export interface WorkbenchShortcutSettings {
/** 用户在快捷入口里选了哪些菜单 key */
@@ -15,10 +16,20 @@ export interface WorkbenchModuleSettings {
[key: string]: unknown;
}
/** 单个 widget 在 12 栅格中的位置与尺寸。i 即 widget key同时作为 grid-layout-plus 标识)。 */
export interface WorkbenchGridItem {
i: WorkbenchModuleKey;
x: number; // 列起点 0-11
y: number; // 行起点
w: number; // 占列数
h: number; // 占行数
minW?: number;
minH?: number;
}
export interface WorkbenchLayout {
version: typeof WORKBENCH_LAYOUT_VERSION;
columns: Array<{ id: WorkbenchColumnId; modules: WorkbenchModuleKey[] }>;
grid: WorkbenchGridItem[];
hidden: WorkbenchModuleKey[];
collapsed: WorkbenchModuleKey[];
settings: WorkbenchModuleSettings;
}