refactor(projects): 1、新增执行任务,表单优化;2、删除逻辑丰富。3、修改已知问题
This commit is contained in:
28
src/views/workbench/composables/layout-storage-local.ts
Normal file
28
src/views/workbench/composables/layout-storage-local.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { LayoutStorage } from './layout-storage';
|
||||
import { WORKBENCH_LAYOUT_VERSION, type WorkbenchLayout } from './workbench-layout-types';
|
||||
|
||||
const KEY_PREFIX = 'rdms-workbench-layout';
|
||||
|
||||
function buildKey(userId: string) {
|
||||
return `${KEY_PREFIX}-${userId}`;
|
||||
}
|
||||
|
||||
export class LocalStorageAdapter implements LayoutStorage {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async load(userId: string): Promise<WorkbenchLayout | null> {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(buildKey(userId));
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw) as WorkbenchLayout;
|
||||
if (parsed?.version !== WORKBENCH_LAYOUT_VERSION) return null;
|
||||
return parsed;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async save(userId: string, layout: WorkbenchLayout): Promise<void> {
|
||||
window.localStorage.setItem(buildKey(userId), JSON.stringify(layout));
|
||||
}
|
||||
}
|
||||
6
src/views/workbench/composables/layout-storage.ts
Normal file
6
src/views/workbench/composables/layout-storage.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { WorkbenchLayout } from './workbench-layout-types';
|
||||
|
||||
export interface LayoutStorage {
|
||||
load(userId: string): Promise<WorkbenchLayout | null>;
|
||||
save(userId: string, layout: WorkbenchLayout): Promise<void>;
|
||||
}
|
||||
158
src/views/workbench/composables/use-workbench-layout.ts
Normal file
158
src/views/workbench/composables/use-workbench-layout.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { type WorkbenchColumnId, 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 type { WorkbenchLayout } from './workbench-layout-types';
|
||||
|
||||
export type WorkbenchMode = 'normal' | 'editing';
|
||||
|
||||
interface UseWorkbenchLayoutOptions {
|
||||
userId: string;
|
||||
storage?: LayoutStorage;
|
||||
}
|
||||
|
||||
export function useWorkbenchLayout(options: UseWorkbenchLayoutOptions) {
|
||||
const { getAllModules } = useWorkbenchModules();
|
||||
const storage = options.storage ?? new LocalStorageAdapter();
|
||||
|
||||
const layout = ref<WorkbenchLayout>(buildDefaultLayout(getAllModules()));
|
||||
const mode = ref<WorkbenchMode>('normal');
|
||||
const dirty = ref(false);
|
||||
const saving = ref(false);
|
||||
const error = ref<Error | null>(null);
|
||||
|
||||
let snapshotBeforeEdit: WorkbenchLayout | null = null;
|
||||
|
||||
async function load() {
|
||||
const fromStorage = await storage.load(options.userId);
|
||||
layout.value = reconcileLayout(fromStorage ?? buildDefaultLayout(getAllModules()), getAllModules());
|
||||
}
|
||||
|
||||
const persist = useDebounceFn(async () => {
|
||||
saving.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
await storage.save(options.userId, layout.value);
|
||||
} catch (err) {
|
||||
error.value = err as Error;
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}, 500);
|
||||
|
||||
function markDirty() {
|
||||
if (mode.value === 'editing') {
|
||||
dirty.value = true;
|
||||
} else {
|
||||
// 非编辑态写(如折叠)直接落盘
|
||||
persist();
|
||||
}
|
||||
}
|
||||
|
||||
function enterEditing() {
|
||||
snapshotBeforeEdit = JSON.parse(JSON.stringify(layout.value));
|
||||
mode.value = 'editing';
|
||||
dirty.value = false;
|
||||
}
|
||||
|
||||
async function saveEditing() {
|
||||
saving.value = true;
|
||||
try {
|
||||
await storage.save(options.userId, layout.value);
|
||||
mode.value = 'normal';
|
||||
dirty.value = false;
|
||||
snapshotBeforeEdit = null;
|
||||
} catch (err) {
|
||||
error.value = err as Error;
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function cancelEditing() {
|
||||
if (snapshotBeforeEdit) {
|
||||
layout.value = snapshotBeforeEdit;
|
||||
}
|
||||
mode.value = 'normal';
|
||||
dirty.value = false;
|
||||
snapshotBeforeEdit = null;
|
||||
}
|
||||
|
||||
function hideModule(key: WorkbenchModuleKey) {
|
||||
for (const col of layout.value.columns) {
|
||||
col.modules = col.modules.filter(k => k !== key);
|
||||
}
|
||||
if (!layout.value.hidden.includes(key)) layout.value.hidden.push(key);
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function showModule(key: WorkbenchModuleKey, columnId: WorkbenchColumnId = 'left') {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function updateModuleSettings<K extends keyof WorkbenchLayout['settings']>(
|
||||
key: K,
|
||||
value: WorkbenchLayout['settings'][K]
|
||||
) {
|
||||
layout.value.settings = { ...layout.value.settings, [key]: value };
|
||||
markDirty();
|
||||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
layout.value = buildDefaultLayout(getAllModules());
|
||||
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
|
||||
.map(k => allMeta.find(m => m.key === k))
|
||||
.filter((m): m is NonNullable<typeof m> => Boolean(m));
|
||||
});
|
||||
|
||||
return {
|
||||
layout,
|
||||
mode,
|
||||
dirty,
|
||||
saving,
|
||||
error,
|
||||
hiddenMetas,
|
||||
isCollapsed,
|
||||
load,
|
||||
enterEditing,
|
||||
saveEditing,
|
||||
cancelEditing,
|
||||
hideModule,
|
||||
showModule,
|
||||
setColumnModules,
|
||||
toggleCollapse,
|
||||
updateModuleSettings,
|
||||
resetToDefault
|
||||
};
|
||||
}
|
||||
160
src/views/workbench/composables/use-workbench-modules.ts
Normal file
160
src/views/workbench/composables/use-workbench-modules.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import type { Component } from 'vue';
|
||||
import { markRaw, shallowRef } from 'vue';
|
||||
|
||||
export type WorkbenchModuleKey =
|
||||
| 'kpi'
|
||||
| 'myTodo'
|
||||
| 'myTask'
|
||||
| 'myRequirement'
|
||||
| 'myProject'
|
||||
| 'activity'
|
||||
| 'shortcut'
|
||||
| 'teamTodo'
|
||||
| 'projectHealth'
|
||||
| 'progressChart'
|
||||
| 'favorite';
|
||||
|
||||
export type WorkbenchModuleCategory = 'personal' | 'manager' | 'tool';
|
||||
export type WorkbenchColumnId = 'left' | 'right';
|
||||
|
||||
export interface WorkbenchModuleMeta {
|
||||
key: WorkbenchModuleKey;
|
||||
component: Component;
|
||||
displayName: string;
|
||||
icon: string;
|
||||
category: WorkbenchModuleCategory;
|
||||
defaultVisible: boolean;
|
||||
defaultColumn: WorkbenchColumnId;
|
||||
defaultOrder: number;
|
||||
}
|
||||
|
||||
const placeholder = markRaw({ render: () => null });
|
||||
|
||||
const registry: WorkbenchModuleMeta[] = [
|
||||
{
|
||||
key: 'kpi',
|
||||
component: placeholder,
|
||||
displayName: 'KPI 速览',
|
||||
icon: 'mdi:view-dashboard-outline',
|
||||
category: 'personal',
|
||||
defaultVisible: true,
|
||||
defaultColumn: 'left',
|
||||
defaultOrder: 1
|
||||
},
|
||||
{
|
||||
key: 'myTodo',
|
||||
component: placeholder,
|
||||
displayName: '我的待办',
|
||||
icon: 'mdi:clipboard-text-clock-outline',
|
||||
category: 'personal',
|
||||
defaultVisible: true,
|
||||
defaultColumn: 'left',
|
||||
defaultOrder: 2
|
||||
},
|
||||
{
|
||||
key: 'myTask',
|
||||
component: placeholder,
|
||||
displayName: '我的任务',
|
||||
icon: 'mdi:checkbox-marked-circle-outline',
|
||||
category: 'personal',
|
||||
defaultVisible: true,
|
||||
defaultColumn: 'left',
|
||||
defaultOrder: 3
|
||||
},
|
||||
{
|
||||
key: 'myRequirement',
|
||||
component: placeholder,
|
||||
displayName: '我的需求',
|
||||
icon: 'mdi:file-document-multiple-outline',
|
||||
category: 'personal',
|
||||
defaultVisible: true,
|
||||
defaultColumn: 'left',
|
||||
defaultOrder: 4
|
||||
},
|
||||
{
|
||||
key: 'myProject',
|
||||
component: placeholder,
|
||||
displayName: '我参与的项目',
|
||||
icon: 'mdi:briefcase-outline',
|
||||
category: 'personal',
|
||||
defaultVisible: true,
|
||||
defaultColumn: 'right',
|
||||
defaultOrder: 1
|
||||
},
|
||||
{
|
||||
key: 'activity',
|
||||
component: placeholder,
|
||||
displayName: '最近动态',
|
||||
icon: 'mdi:timeline-outline',
|
||||
category: 'personal',
|
||||
defaultVisible: true,
|
||||
defaultColumn: 'right',
|
||||
defaultOrder: 2
|
||||
},
|
||||
{
|
||||
key: 'shortcut',
|
||||
component: placeholder,
|
||||
displayName: '快捷入口',
|
||||
icon: 'mdi:rocket-launch-outline',
|
||||
category: 'tool',
|
||||
defaultVisible: true,
|
||||
defaultColumn: 'right',
|
||||
defaultOrder: 3
|
||||
},
|
||||
{
|
||||
key: 'teamTodo',
|
||||
component: placeholder,
|
||||
displayName: '团队待办汇总',
|
||||
icon: 'mdi:account-group-outline',
|
||||
category: 'manager',
|
||||
defaultVisible: false,
|
||||
defaultColumn: 'right',
|
||||
defaultOrder: 4
|
||||
},
|
||||
{
|
||||
key: 'projectHealth',
|
||||
component: placeholder,
|
||||
displayName: '项目健康度',
|
||||
icon: 'mdi:heart-pulse',
|
||||
category: 'manager',
|
||||
defaultVisible: false,
|
||||
defaultColumn: 'right',
|
||||
defaultOrder: 5
|
||||
},
|
||||
{
|
||||
key: 'progressChart',
|
||||
component: placeholder,
|
||||
displayName: '跨项目进度图',
|
||||
icon: 'mdi:chart-bar',
|
||||
category: 'manager',
|
||||
defaultVisible: false,
|
||||
defaultColumn: 'right',
|
||||
defaultOrder: 6
|
||||
},
|
||||
{
|
||||
key: 'favorite',
|
||||
component: placeholder,
|
||||
displayName: '我的收藏',
|
||||
icon: 'mdi:star-outline',
|
||||
category: 'tool',
|
||||
defaultVisible: false,
|
||||
defaultColumn: 'right',
|
||||
defaultOrder: 7
|
||||
}
|
||||
];
|
||||
|
||||
const registryRef = shallowRef(registry);
|
||||
|
||||
export function useWorkbenchModules() {
|
||||
function getAllModules() {
|
||||
return registryRef.value;
|
||||
}
|
||||
function getModuleMeta(key: WorkbenchModuleKey) {
|
||||
return registryRef.value.find(m => m.key === key);
|
||||
}
|
||||
function registerModuleComponent(key: WorkbenchModuleKey, component: Component) {
|
||||
const target = registryRef.value.find(m => m.key === key);
|
||||
if (target) target.component = markRaw(component);
|
||||
}
|
||||
return { getAllModules, getModuleMeta, registerModuleComponent };
|
||||
}
|
||||
30
src/views/workbench/composables/workbench-layout-default.ts
Normal file
30
src/views/workbench/composables/workbench-layout-default.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
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 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);
|
||||
|
||||
return {
|
||||
version: WORKBENCH_LAYOUT_VERSION,
|
||||
columns: [
|
||||
{ id: 'left', modules: left },
|
||||
{ id: 'right', modules: right }
|
||||
],
|
||||
hidden,
|
||||
collapsed: [],
|
||||
settings: {}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { WorkbenchModuleKey, WorkbenchModuleMeta } from './use-workbench-modules';
|
||||
import type { WorkbenchLayout } from './workbench-layout-types';
|
||||
|
||||
/**
|
||||
* 把存量布局与当前模块注册中心对齐。
|
||||
* - 注册中心存在但布局未含的 key:按 defaultVisible 进 columns 或 hidden
|
||||
* - 布局含但注册中心已删除的 key:丢弃
|
||||
*/
|
||||
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 columns = layout.columns.map(c => ({ id: c.id, modules: filterKnown(c.modules) }));
|
||||
const hidden = filterKnown(layout.hidden);
|
||||
const collapsed = filterKnown(layout.collapsed);
|
||||
|
||||
const appearKeys = new Set<WorkbenchModuleKey>([...columns.flatMap(c => c.modules), ...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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { ...layout, columns, hidden, collapsed };
|
||||
}
|
||||
22
src/views/workbench/composables/workbench-layout-types.ts
Normal file
22
src/views/workbench/composables/workbench-layout-types.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { WorkbenchColumnId, WorkbenchModuleKey } from './use-workbench-modules';
|
||||
|
||||
export const WORKBENCH_LAYOUT_VERSION = 1;
|
||||
|
||||
export interface WorkbenchShortcutSettings {
|
||||
/** 用户在快捷入口里选了哪些菜单 key */
|
||||
menuKeys: string[];
|
||||
}
|
||||
|
||||
export interface WorkbenchModuleSettings {
|
||||
shortcut?: WorkbenchShortcutSettings;
|
||||
/** 后续每模块可加自定义设置 */
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface WorkbenchLayout {
|
||||
version: typeof WORKBENCH_LAYOUT_VERSION;
|
||||
columns: Array<{ id: WorkbenchColumnId; modules: WorkbenchModuleKey[] }>;
|
||||
hidden: WorkbenchModuleKey[];
|
||||
collapsed: WorkbenchModuleKey[];
|
||||
settings: WorkbenchModuleSettings;
|
||||
}
|
||||
Reference in New Issue
Block a user