refactor(projects): 1、新增执行任务,表单优化;2、删除逻辑丰富。3、修改已知问题

This commit is contained in:
2026-05-21 21:42:23 +08:00
parent 28d597d91e
commit ba328e02bb
68 changed files with 3329 additions and 644 deletions

View 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));
}
}

View 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>;
}

View 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
};
}

View 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 };
}

View 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: {}
};
}

View File

@@ -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 };
}

View 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;
}