feat(projects): 工作台部分组件调成真实数据
This commit is contained in:
@@ -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
|
||||
};
|
||||
|
||||
@@ -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=7):myTodo(y=0 h=25) → myWeekWorklog(y=25 h=22)
|
||||
// 右列(x=7 w=5):shortcut(y=0 h=11) → myProject(y=11 h=17) → myExecution(y=28 h=19)
|
||||
// 底部满宽(x=0 w=12):teamLoad(y=47 h=16)
|
||||
// hidden(x/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 }
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
30
src/views/workbench/composables/use-workbench-refresh.ts
Normal file
30
src/views/workbench/composables/use-workbench-refresh.ts
Normal 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 };
|
||||
}
|
||||
@@ -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'
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user