看板
This commit is contained in:
@@ -0,0 +1,421 @@
|
||||
import {
|
||||
ref,
|
||||
reactive,
|
||||
getCurrentInstance,
|
||||
onUnmounted,
|
||||
nextTick,
|
||||
computed,
|
||||
onMounted,
|
||||
type ComponentInternalInstance
|
||||
} from 'vue';
|
||||
import { logger } from '../util/logger';
|
||||
|
||||
// 类型定义
|
||||
interface ParentContext {
|
||||
name: string;
|
||||
addChild: (child: ChildContext) => void;
|
||||
removeChild: (childId: string) => void;
|
||||
broadcast: (event: string, data?: any, childIds?: string | string[]) => void;
|
||||
broadcastToChildren: (componentName: string, event: string, data?: any) => void;
|
||||
getChildren: () => ChildContext[];
|
||||
getExposed: () => Record<string, any>;
|
||||
getChildExposed: (childId: string) => Record<string, any>;
|
||||
getChildrenExposed: () => Array<{ id: string; name: string; exposed: Record<string, any> }>;
|
||||
getInstance: () => any;
|
||||
}
|
||||
|
||||
interface ChildContext {
|
||||
id: string;
|
||||
name: string;
|
||||
getChildIndex: () => number;
|
||||
emitToParent: (event: string, data?: any) => void;
|
||||
getParentExposed: () => Record<string, any>;
|
||||
getInstance: () => any;
|
||||
getExposed: () => Record<string, any>;
|
||||
}
|
||||
|
||||
// 符号定义
|
||||
const PARENT_CONTEXT_SYMBOL = Symbol('parent_context');
|
||||
|
||||
/**
|
||||
* 生成实例唯一ID
|
||||
*/
|
||||
function generateInstanceId(componentName: string): string {
|
||||
return `${componentName}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找父组件实例
|
||||
*/
|
||||
function findParentInstance(name: string, instance: any): any {
|
||||
if (!instance) return null;
|
||||
|
||||
let parent = instance.parent;
|
||||
while (parent) {
|
||||
const parentName = parent.type?.name || parent.type?.__name;
|
||||
if (parentName === name) {
|
||||
return parent;
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取父组件上下文
|
||||
*/
|
||||
function getParentContext(name: string, instance: any): ParentContext | null {
|
||||
const parentInstance = findParentInstance(name, instance);
|
||||
return parentInstance?.proxy?.[PARENT_CONTEXT_SYMBOL] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归查找所有指定名称的子组件
|
||||
*/
|
||||
function findAllChildComponents(componentName: string, instance: any): any[] {
|
||||
const components: any[] = [];
|
||||
|
||||
function traverse(currentInstance: any) {
|
||||
if (!currentInstance?.subTree) return;
|
||||
|
||||
const subTree = currentInstance.subTree?.children || [];
|
||||
const children = Array.isArray(subTree) ? subTree : [subTree];
|
||||
|
||||
children.forEach((vnode: any) => {
|
||||
const child = vnode.component;
|
||||
if (!child) return;
|
||||
|
||||
const name = child.type?.name || child.type?.__name;
|
||||
if (name === componentName) {
|
||||
components.push(child);
|
||||
}
|
||||
traverse(child);
|
||||
});
|
||||
}
|
||||
|
||||
traverse(instance);
|
||||
logger.log(`Found ${components.length} ${componentName} components`);
|
||||
return components;
|
||||
}
|
||||
|
||||
/**
|
||||
* 父组件 Hook
|
||||
*/
|
||||
export function useParent(componentName?: string) {
|
||||
const instance = getCurrentInstance();
|
||||
if (!instance) {
|
||||
throw new Error('useParent must be called within setup function');
|
||||
}
|
||||
|
||||
const name = componentName || instance.type.name || instance.type.__name;
|
||||
if (!name) {
|
||||
throw new Error('Component name is required for useParent');
|
||||
}
|
||||
|
||||
const children = reactive<ChildContext[]>([]);
|
||||
const childrenMap = new Map<string, ChildContext>();
|
||||
|
||||
const broadcast = (event: string, data?: any, childIds?: string | string[]) => {
|
||||
const targetChildren = childIds
|
||||
? ((Array.isArray(childIds) ? childIds : [childIds])
|
||||
.map(id => childrenMap.get(id))
|
||||
.filter(Boolean) as ChildContext[])
|
||||
: Array.from(childrenMap.values());
|
||||
|
||||
logger.log(`Parent ${name} broadcasting event: ${event} to ${targetChildren.length} children`);
|
||||
|
||||
targetChildren.forEach(child => {
|
||||
const exposed = child.getExposed();
|
||||
if (exposed && typeof exposed[event] === 'function') {
|
||||
try {
|
||||
exposed[event](data);
|
||||
} catch (error) {
|
||||
logger.warn(`Error calling child method ${event}:`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const broadcastToChildren = (componentName: string, event: string, data?: any) => {
|
||||
logger.log(`Parent ${name} broadcasting event: ${event} to all ${componentName} components`);
|
||||
|
||||
const childComponents = findAllChildComponents(componentName, instance);
|
||||
let successCount = 0;
|
||||
|
||||
childComponents.forEach(childComponent => {
|
||||
const exposed = childComponent.exposed || childComponent.proxy;
|
||||
if (exposed && typeof exposed[event] === 'function') {
|
||||
try {
|
||||
exposed[event](data);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
logger.warn(`Error calling ${componentName} method ${event}:`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logger.log(
|
||||
`Parent ${name} successfully called ${successCount} of ${childComponents.length} ${componentName} components`
|
||||
);
|
||||
};
|
||||
|
||||
const parentContext: ParentContext = {
|
||||
name,
|
||||
addChild(child: ChildContext) {
|
||||
if (!childrenMap.has(child.id)) {
|
||||
childrenMap.set(child.id, child);
|
||||
children.push(child);
|
||||
logger.log(`Parent ${name} added child: ${child.name}`);
|
||||
}
|
||||
},
|
||||
removeChild(childId: string) {
|
||||
if (childrenMap.has(childId)) {
|
||||
const child = childrenMap.get(childId)!;
|
||||
childrenMap.delete(childId);
|
||||
const index = children.findIndex(c => c.id === childId);
|
||||
if (index > -1) children.splice(index, 1);
|
||||
logger.log(`Parent ${name} removed child: ${childId}`);
|
||||
}
|
||||
},
|
||||
broadcast,
|
||||
broadcastToChildren,
|
||||
getChildren: () => Array.from(childrenMap.values()),
|
||||
getExposed: () => instance.exposed || {},
|
||||
getChildExposed(childId: string) {
|
||||
const child = childrenMap.get(childId);
|
||||
return child?.getExposed?.() || {};
|
||||
},
|
||||
getChildrenExposed() {
|
||||
return Array.from(childrenMap.values())
|
||||
.filter(child => child.getExposed)
|
||||
.map(child => ({
|
||||
id: child.id,
|
||||
name: child.name,
|
||||
exposed: child.getExposed()
|
||||
}))
|
||||
.filter(item => Object.keys(item.exposed).length > 0);
|
||||
},
|
||||
getInstance: () => instance
|
||||
};
|
||||
|
||||
if (instance.proxy) {
|
||||
(instance.proxy as any)[PARENT_CONTEXT_SYMBOL] = parentContext;
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
childrenMap.forEach((_, childId) => parentContext.removeChild(childId));
|
||||
if (instance.proxy) {
|
||||
delete (instance.proxy as any)[PARENT_CONTEXT_SYMBOL];
|
||||
}
|
||||
logger.log(`Parent ${name} unmounted and cleaned up`);
|
||||
});
|
||||
|
||||
return {
|
||||
parentName: name,
|
||||
children,
|
||||
broadcast,
|
||||
broadcastToChildren,
|
||||
getChildren: parentContext.getChildren,
|
||||
getChildExposed: parentContext.getChildExposed,
|
||||
getChildrenExposed: parentContext.getChildrenExposed,
|
||||
getExposed: parentContext.getExposed,
|
||||
getInstance: parentContext.getInstance
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 子组件 Hook
|
||||
*/
|
||||
export function useChildren(componentName?: string, parentName?: string) {
|
||||
const instance = getCurrentInstance();
|
||||
if (!instance) {
|
||||
throw new Error('useChildren must be called within setup function');
|
||||
}
|
||||
|
||||
const name = componentName || instance.type.name || instance.type.__name;
|
||||
const instanceId = generateInstanceId(name || 'anonymous');
|
||||
const parentRef = ref<any | null>(null);
|
||||
const parentExposed = ref<Record<string, any>>({});
|
||||
|
||||
const createSimulatedParentContext = (parentInstance: any): ParentContext => ({
|
||||
name: parentInstance?.type?.name || parentInstance?.type?.__name || 'unknown',
|
||||
addChild: () => logger.log('Simulated parent added child'),
|
||||
removeChild: () => logger.log('Simulated parent removed child'),
|
||||
broadcast: () => logger.log('Simulated parent broadcasting'),
|
||||
broadcastToChildren: () => logger.log('Simulated parent broadcasting to children'),
|
||||
getChildren: () => [],
|
||||
getExposed: () => parentInstance?.exposed || {},
|
||||
getChildExposed: () => ({}),
|
||||
getChildrenExposed: () => [],
|
||||
getInstance: () => parentInstance
|
||||
});
|
||||
|
||||
const getParentExposed = (): Record<string, any> => {
|
||||
if (parentRef.value) {
|
||||
const exposed = parentRef.value.getExposed();
|
||||
parentExposed.value = exposed;
|
||||
return exposed;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const getExposed = (): Record<string, any> => instance.exposed || {};
|
||||
|
||||
const findParent = (): ParentContext | null => {
|
||||
if (parentName) {
|
||||
const parentContext = getParentContext(parentName, instance);
|
||||
if (parentContext) {
|
||||
if (!parentContext.getInstance) {
|
||||
parentContext.getInstance = () => findParentInstance(parentName, instance);
|
||||
}
|
||||
return parentContext;
|
||||
}
|
||||
|
||||
const parentInstance = findParentInstance(parentName, instance);
|
||||
if (parentInstance) {
|
||||
return createSimulatedParentContext(parentInstance);
|
||||
}
|
||||
}
|
||||
|
||||
let current = instance.parent;
|
||||
while (current) {
|
||||
const context = (current.proxy as any)?.[PARENT_CONTEXT_SYMBOL];
|
||||
if (context) {
|
||||
if (!context.getInstance) {
|
||||
context.getInstance = () => current;
|
||||
}
|
||||
return context;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return instance.parent ? createSimulatedParentContext(instance.parent) : null;
|
||||
};
|
||||
|
||||
const linkParent = (): boolean => {
|
||||
const parent = findParent();
|
||||
if (parent) {
|
||||
parentRef.value = parent;
|
||||
if (parent.addChild && childContext) {
|
||||
parent.addChild(childContext);
|
||||
}
|
||||
getParentExposed();
|
||||
logger.log(`Child ${name || 'anonymous'} linked to parent ${parent.name}`);
|
||||
return true;
|
||||
}
|
||||
logger.log(`Child ${name || 'anonymous'} no parent found, working in standalone mode`);
|
||||
return false;
|
||||
};
|
||||
|
||||
const emitToParent = (event: string, data?: any) => {
|
||||
if (parentRef.value) {
|
||||
const exposed = getParentExposed();
|
||||
if (exposed && typeof exposed[event] === 'function') {
|
||||
try {
|
||||
exposed[event](data, instanceId, name);
|
||||
} catch (error) {
|
||||
logger.warn(`Error calling parent method ${event}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getChildIndex = () => {
|
||||
if (!parentRef.value) return -1;
|
||||
try {
|
||||
const children = parentRef.value.getChildren();
|
||||
return children.findIndex((child: ChildContext) => child.id === instanceId);
|
||||
} catch (error) {
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
const childContext: ChildContext = {
|
||||
id: instanceId,
|
||||
name: name || 'anonymous',
|
||||
getChildIndex,
|
||||
emitToParent,
|
||||
getParentExposed,
|
||||
getInstance: () => instance,
|
||||
getExposed
|
||||
};
|
||||
|
||||
logger.log(`Child ${name || 'anonymous'} registered, looking for parent`);
|
||||
|
||||
onMounted(() => {
|
||||
let connected = linkParent();
|
||||
nextTick(() => {
|
||||
connected = linkParent();
|
||||
if (!connected) {
|
||||
setTimeout(linkParent, 500);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (parentRef.value?.removeChild) {
|
||||
parentRef.value.removeChild(instanceId);
|
||||
}
|
||||
logger.log(`Child ${name || 'anonymous'} unmounted`);
|
||||
});
|
||||
|
||||
return {
|
||||
childId: instanceId,
|
||||
childName: name || 'anonymous',
|
||||
childIndex: computed(getChildIndex),
|
||||
parent: parentRef,
|
||||
emitToParent,
|
||||
getParentExposed,
|
||||
parentExposed: computed(() => parentExposed.value),
|
||||
getExposed
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查父组件是否存在
|
||||
*/
|
||||
export function hasParent(parentName?: string): boolean {
|
||||
const instance = getCurrentInstance();
|
||||
if (!instance) return false;
|
||||
|
||||
if (parentName) {
|
||||
return getParentContext(parentName, instance) !== null;
|
||||
}
|
||||
|
||||
let current = instance.parent;
|
||||
while (current) {
|
||||
if ((current.proxy as any)?.[PARENT_CONTEXT_SYMBOL]) return true;
|
||||
current = current.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取父组件上下文
|
||||
*/
|
||||
export function getParentContextByName(parentName: string): ParentContext | null {
|
||||
const instance = getCurrentInstance();
|
||||
return instance ? getParentContext(parentName, instance) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 热更新清理函数
|
||||
*/
|
||||
export function cleanupComponentRelations(): void {
|
||||
logger.log('Cleaning up component relations for hot reload');
|
||||
}
|
||||
|
||||
// 热更新处理
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(() => {
|
||||
logger.log('Hot reload detected, relations will be automatically reconnected');
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
useParent,
|
||||
useChildren,
|
||||
hasParent,
|
||||
getParentContextByName,
|
||||
cleanupComponentRelations
|
||||
};
|
||||
Reference in New Issue
Block a user