uview-pro 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +21 -0
- package/components/u-checkbox/u-checkbox.vue +81 -62
- package/components/u-checkbox-group/types.ts +1 -1
- package/components/u-checkbox-group/u-checkbox-group.vue +77 -26
- package/components/u-collapse/u-collapse.vue +140 -14
- package/components/u-collapse-item/u-collapse-item.vue +159 -61
- package/index.ts +11 -2
- package/libs/hooks/index.ts +1 -1
- package/libs/hooks/useComponent.ts +343 -0
- package/libs/hooks/useParent.ts +22 -20
- package/libs/index.ts +34 -5
- package/libs/util/eventBus.ts +86 -0
- package/libs/util/logger.ts +364 -0
- package/package.json +1 -1
- package/readme.md +3 -1
- package/types/global.d.ts +39 -3
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<view class="u-collapse-item" :style="`${$u.toStyle(itemStyle)}${$u.toStyle(customStyle)}`" :class="customClass">
|
|
3
|
-
<view
|
|
3
|
+
<view
|
|
4
|
+
:hover-stay-time="200"
|
|
5
|
+
class="u-collapse-head"
|
|
6
|
+
@tap.stop="headClick"
|
|
7
|
+
:hover-class="hoverClass"
|
|
8
|
+
:style="headStyle"
|
|
9
|
+
>
|
|
4
10
|
<template v-if="!slots['title-all']">
|
|
5
11
|
<view v-if="!slots['title']" class="u-collapse-title u-line-1" :style="titleStyle">
|
|
6
12
|
{{ title }}
|
|
7
13
|
</view>
|
|
8
14
|
<slot v-else name="title" />
|
|
9
15
|
<view class="u-icon-wrap">
|
|
10
|
-
<u-icon v-if="
|
|
16
|
+
<u-icon v-if="showArrow" :color="arrowColor" :name="isShow ? 'arrow-up' : 'arrow-down'" />
|
|
11
17
|
</view>
|
|
12
18
|
</template>
|
|
13
19
|
<slot v-else name="title-all" />
|
|
@@ -26,17 +32,18 @@ export default {
|
|
|
26
32
|
name: 'u-collapse-item',
|
|
27
33
|
options: {
|
|
28
34
|
addGlobalClass: true,
|
|
35
|
+
// #ifndef MP-TOUTIAO
|
|
29
36
|
virtualHost: true,
|
|
37
|
+
// #endif
|
|
30
38
|
styleIsolation: 'shared'
|
|
31
39
|
}
|
|
32
40
|
};
|
|
33
41
|
</script>
|
|
34
42
|
|
|
35
43
|
<script setup lang="ts">
|
|
36
|
-
import { ref, watch, onMounted, useSlots, getCurrentInstance, nextTick, computed } from 'vue';
|
|
37
|
-
import { $u } from '../..';
|
|
44
|
+
import { ref, watch, onMounted, useSlots, getCurrentInstance, nextTick, computed, onUnmounted } from 'vue';
|
|
45
|
+
import { $u, useChildren, onParentEvent } from '../..';
|
|
38
46
|
import { CollapseItemProps } from './types';
|
|
39
|
-
import { useParent } from '../../libs/hooks/useParent';
|
|
40
47
|
|
|
41
48
|
/**
|
|
42
49
|
* collapseItem 手风琴Item
|
|
@@ -59,50 +66,77 @@ const instance = getCurrentInstance();
|
|
|
59
66
|
|
|
60
67
|
const isShow = ref(false);
|
|
61
68
|
const elId = ref('');
|
|
62
|
-
const height = ref('0');
|
|
63
|
-
const headStyle = ref<Record<string, any>>({});
|
|
64
|
-
const bodyStyle = ref<Record<string, any>>({});
|
|
65
|
-
const itemStyle = ref<Record<string, any>>({});
|
|
66
|
-
const arrowColor = ref('');
|
|
67
|
-
const hoverClass = ref('');
|
|
68
|
-
const accordion = ref(true); // 是否显示右侧箭头
|
|
69
|
-
const arrow = ref(true);
|
|
69
|
+
const height = ref('0');
|
|
70
|
+
const headStyle = ref<Record<string, any>>({});
|
|
71
|
+
const bodyStyle = ref<Record<string, any>>({});
|
|
72
|
+
const itemStyle = ref<Record<string, any>>({});
|
|
73
|
+
const arrowColor = ref('');
|
|
74
|
+
const hoverClass = ref('');
|
|
70
75
|
|
|
71
|
-
|
|
76
|
+
// 使用通信库的子组件Hook
|
|
77
|
+
const { childId, parentExposed } = useChildren('u-collapse-item', 'u-collapse');
|
|
72
78
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
},
|
|
78
|
-
{ immediate: true }
|
|
79
|
-
);
|
|
79
|
+
// 计算属性
|
|
80
|
+
const showArrow = computed(() => {
|
|
81
|
+
return parentExposed.value?.props ? parentExposed.value.props.arrow : true;
|
|
82
|
+
});
|
|
80
83
|
|
|
81
|
-
/**
|
|
82
|
-
* 获取父组件的配置项
|
|
83
|
-
*/
|
|
84
84
|
const titleStyle = computed(() => {
|
|
85
85
|
let style = { textAlign: props.align ? props.align : 'left' };
|
|
86
86
|
|
|
87
|
-
if (isShow.value && props.activeStyle && !
|
|
87
|
+
if (isShow.value && props.activeStyle && !showArrow.value) {
|
|
88
88
|
style = $u.deepMerge(style, props.activeStyle);
|
|
89
89
|
}
|
|
90
90
|
return $u.toStyle(style);
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
+
// 获取唯一标识符
|
|
94
|
+
const itemName = computed(() => {
|
|
95
|
+
// 优先级:name > index > childId
|
|
96
|
+
if (props.name !== undefined && props.name !== '') {
|
|
97
|
+
return props.name;
|
|
98
|
+
} else if (props.index !== undefined && props.index !== '') {
|
|
99
|
+
return props.index;
|
|
100
|
+
} else {
|
|
101
|
+
return childId;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 设置显示状态
|
|
107
|
+
*/
|
|
108
|
+
function setShowState(show: boolean) {
|
|
109
|
+
if (isShow.value !== show) {
|
|
110
|
+
isShow.value = show;
|
|
111
|
+
|
|
112
|
+
// 如果展开,需要重新计算高度
|
|
113
|
+
if (show) {
|
|
114
|
+
nextTick(() => {
|
|
115
|
+
queryRect();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 本地触发change事件
|
|
120
|
+
emit('change', {
|
|
121
|
+
index: props.index,
|
|
122
|
+
name: itemName.value,
|
|
123
|
+
show: isShow.value
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
93
128
|
/**
|
|
94
129
|
* 异步获取内容,或者动态修改了内容时,需要重新初始化
|
|
95
130
|
*/
|
|
96
131
|
function init() {
|
|
97
|
-
if (
|
|
98
|
-
headStyle.value = parentExposed.props.headStyle;
|
|
99
|
-
bodyStyle.value = parentExposed.props.bodyStyle;
|
|
100
|
-
arrowColor.value = parentExposed.props.arrowColor;
|
|
101
|
-
hoverClass.value = parentExposed.props.hoverClass;
|
|
102
|
-
|
|
103
|
-
itemStyle.value = parentExposed.props.itemStyle;
|
|
104
|
-
accordion.value = parentExposed.props.accordion;
|
|
132
|
+
if (parentExposed.value?.props) {
|
|
133
|
+
headStyle.value = parentExposed.value.props.headStyle || {};
|
|
134
|
+
bodyStyle.value = parentExposed.value.props.bodyStyle || {};
|
|
135
|
+
arrowColor.value = parentExposed.value.props.arrowColor || '#909399';
|
|
136
|
+
hoverClass.value = parentExposed.value.props.hoverClass || 'u-hover-class';
|
|
137
|
+
itemStyle.value = parentExposed.value.props.itemStyle || {};
|
|
105
138
|
}
|
|
139
|
+
|
|
106
140
|
elId.value = $u.guid();
|
|
107
141
|
nextTick(() => {
|
|
108
142
|
queryRect();
|
|
@@ -114,46 +148,108 @@ function init() {
|
|
|
114
148
|
*/
|
|
115
149
|
function headClick() {
|
|
116
150
|
if (props.disabled) return;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
} else {
|
|
122
|
-
vm.exposed.isShow.value = !vm.exposed.isShow.value;
|
|
123
|
-
emit('change', {
|
|
124
|
-
index: props.index,
|
|
125
|
-
show: vm.exposed.isShow.value
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
} else {
|
|
130
|
-
isShow.value = !isShow.value;
|
|
131
|
-
emit('change', {
|
|
132
|
-
index: props.index,
|
|
133
|
-
show: isShow.value
|
|
134
|
-
});
|
|
151
|
+
|
|
152
|
+
// 通知父组件状态变化
|
|
153
|
+
if (parentExposed.value?.onChange) {
|
|
154
|
+
parentExposed.value.onChange(itemName.value);
|
|
135
155
|
}
|
|
136
|
-
if (isShow.value) parentExposed && parentExposed.onChange && parentExposed.onChange(props.index);
|
|
137
156
|
}
|
|
138
157
|
|
|
139
158
|
/**
|
|
140
159
|
* 查询内容高度
|
|
141
160
|
*/
|
|
142
161
|
function queryRect() {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
162
|
+
$u.getRect('#' + elId.value, instance)
|
|
163
|
+
.then((res: any) => {
|
|
164
|
+
if (res && res.height) {
|
|
165
|
+
height.value = res.height + 'px';
|
|
166
|
+
}
|
|
167
|
+
// #ifdef MP-TOUTIAO
|
|
168
|
+
if (isShow.value) {
|
|
169
|
+
height.value = 'auto';
|
|
170
|
+
}
|
|
171
|
+
// #endif
|
|
172
|
+
})
|
|
173
|
+
.catch((err: any) => {
|
|
174
|
+
console.warn('queryRect error:', err);
|
|
175
|
+
height.value = 'auto';
|
|
176
|
+
});
|
|
151
177
|
}
|
|
152
178
|
|
|
179
|
+
// 监听父组件的事件
|
|
180
|
+
const unsubscribeOpenSingle = onParentEvent(childId, 'openSingle', (data: any) => {
|
|
181
|
+
// 只有目标项展开,其他都关闭
|
|
182
|
+
const shouldShow = data.targetName === itemName.value;
|
|
183
|
+
setShowState(shouldShow);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const unsubscribeCloseAll = onParentEvent(childId, 'closeAll', () => {
|
|
187
|
+
setShowState(false);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const unsubscribeSetMultiple = onParentEvent(childId, 'setMultiple', (data: any) => {
|
|
191
|
+
const shouldShow = data.targetNames.includes(itemName.value);
|
|
192
|
+
setShowState(shouldShow);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const unsubscribeToggleSingle = onParentEvent(childId, 'toggleSingle', (data: any) => {
|
|
196
|
+
// 只有目标项才切换状态
|
|
197
|
+
if (data.targetName === itemName.value) {
|
|
198
|
+
setShowState(!isShow.value);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// 监听父组件的重连事件(热更新后)
|
|
203
|
+
const unsubscribeReconnect = onParentEvent(childId, 'reconnect', () => {
|
|
204
|
+
console.log('Collapse item reconnected to parent after hot update');
|
|
205
|
+
});
|
|
206
|
+
|
|
153
207
|
onMounted(() => {
|
|
208
|
+
// 关键修复:根据 open 属性设置初始状态
|
|
209
|
+
setShowState(props.open);
|
|
210
|
+
|
|
211
|
+
// 初始化
|
|
154
212
|
init();
|
|
155
213
|
});
|
|
156
214
|
|
|
215
|
+
// 监听 open 属性变化
|
|
216
|
+
watch(
|
|
217
|
+
() => props.open,
|
|
218
|
+
newVal => {
|
|
219
|
+
setShowState(newVal);
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// 监听父组件exposed的变化
|
|
224
|
+
watch(
|
|
225
|
+
parentExposed,
|
|
226
|
+
newExposed => {
|
|
227
|
+
if (newExposed) {
|
|
228
|
+
init();
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
{ deep: true, immediate: true }
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// 组件卸载时清理事件监听
|
|
235
|
+
onUnmounted(() => {
|
|
236
|
+
unsubscribeOpenSingle();
|
|
237
|
+
unsubscribeCloseAll();
|
|
238
|
+
unsubscribeSetMultiple();
|
|
239
|
+
unsubscribeToggleSingle();
|
|
240
|
+
unsubscribeReconnect();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// 热更新处理
|
|
244
|
+
if (import.meta.hot) {
|
|
245
|
+
import.meta.hot.accept(() => {
|
|
246
|
+
setTimeout(() => {
|
|
247
|
+
console.log('Collapse item hot updated, reinitializing...');
|
|
248
|
+
init();
|
|
249
|
+
}, 150);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
157
253
|
defineExpose({
|
|
158
254
|
init,
|
|
159
255
|
isShow,
|
|
@@ -164,7 +260,9 @@ defineExpose({
|
|
|
164
260
|
itemStyle,
|
|
165
261
|
arrowColor,
|
|
166
262
|
hoverClass,
|
|
167
|
-
|
|
263
|
+
itemName: itemName.value,
|
|
264
|
+
queryRect,
|
|
265
|
+
setShowState
|
|
168
266
|
});
|
|
169
267
|
</script>
|
|
170
268
|
|
package/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { $u, type RequestOptions } from './libs';
|
|
2
2
|
import type { UViewProOptions } from './types/global';
|
|
3
|
+
import { logger } from './libs/util/logger';
|
|
3
4
|
|
|
4
5
|
declare const uni: {
|
|
5
6
|
[key: string]: any;
|
|
@@ -13,8 +14,16 @@ declare const uni: {
|
|
|
13
14
|
// $u挂载到uni对象上
|
|
14
15
|
const install = (app: any, options?: UViewProOptions): void => {
|
|
15
16
|
uni.$u = $u;
|
|
16
|
-
if (options
|
|
17
|
-
|
|
17
|
+
if (options) {
|
|
18
|
+
// 配置主题
|
|
19
|
+
if (options.theme) {
|
|
20
|
+
$u.color = $u.deepMerge($u.color, options.theme);
|
|
21
|
+
}
|
|
22
|
+
// 设置调试模式
|
|
23
|
+
logger
|
|
24
|
+
.setDebugMode(options?.log?.debug ?? true)
|
|
25
|
+
.setPrefix(options?.log?.prefix)
|
|
26
|
+
.setShowCallerInfo(options?.log?.showCallerInfo ?? true);
|
|
18
27
|
}
|
|
19
28
|
// 可扩展更多配置项
|
|
20
29
|
app.config.globalProperties.$u = $u;
|
package/libs/hooks/index.ts
CHANGED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// utils/useComponent.ts
|
|
2
|
+
import { ref, reactive, getCurrentInstance, onUnmounted, nextTick, computed } from 'vue';
|
|
3
|
+
import { logger } from '../util/logger';
|
|
4
|
+
import { eventBus } from '../util/eventBus';
|
|
5
|
+
|
|
6
|
+
// 简化类型定义
|
|
7
|
+
interface ParentContext {
|
|
8
|
+
name: string;
|
|
9
|
+
addChild: (child: ChildContext) => void;
|
|
10
|
+
removeChild: (childId: string) => void;
|
|
11
|
+
broadcast: (event: string, data?: any) => void;
|
|
12
|
+
getChildren: () => ChildContext[];
|
|
13
|
+
getExposed: () => Record<string, any>;
|
|
14
|
+
getChildExposed: (childId: string) => Record<string, any>;
|
|
15
|
+
getChildrenExposed: () => Array<{ id: string; name: string; exposed: Record<string, any> }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ChildContext {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
emitToParent: (event: string, data?: any) => void;
|
|
22
|
+
getParentExposed: () => Record<string, any>;
|
|
23
|
+
getInstance: () => any;
|
|
24
|
+
getExposed: () => Record<string, any>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 全局存储
|
|
28
|
+
const parentMap = new Map<string, ParentContext>();
|
|
29
|
+
const childMap = new Map<string, ChildContext>();
|
|
30
|
+
|
|
31
|
+
// 事件常量
|
|
32
|
+
const PARENT_REGISTERED_EVENT = 'parent:registered';
|
|
33
|
+
const PARENT_UNMOUNTED_EVENT = 'parent:unmounted';
|
|
34
|
+
const CHILD_REGISTERED_EVENT = 'child:registered';
|
|
35
|
+
|
|
36
|
+
// 热更新清理函数
|
|
37
|
+
export function cleanupComponentRelations(): void {
|
|
38
|
+
logger.log('Cleaning up component relations for hot reload');
|
|
39
|
+
parentMap.clear();
|
|
40
|
+
childMap.clear();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 热更新处理
|
|
44
|
+
if (import.meta.hot) {
|
|
45
|
+
import.meta.hot.accept(() => {
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
cleanupComponentRelations();
|
|
48
|
+
}, 50);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 生成实例唯一ID
|
|
54
|
+
*/
|
|
55
|
+
function generateInstanceId(componentName: string): string {
|
|
56
|
+
return `${componentName}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 父组件 Hook
|
|
61
|
+
*/
|
|
62
|
+
export function useParent(componentName: string) {
|
|
63
|
+
const instance = getCurrentInstance();
|
|
64
|
+
if (!instance) {
|
|
65
|
+
throw new Error('useParent must be called within setup function');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!componentName) {
|
|
69
|
+
throw new Error('Component name is required for useParent');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 热更新时清理旧的父组件
|
|
73
|
+
if (parentMap.has(componentName)) {
|
|
74
|
+
logger.log(`Cleaning up existing parent ${componentName} for hot reload`);
|
|
75
|
+
parentMap.delete(componentName);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const children = reactive<ChildContext[]>([]);
|
|
79
|
+
|
|
80
|
+
// 父组件上下文
|
|
81
|
+
const parentContext: ParentContext = {
|
|
82
|
+
name: componentName,
|
|
83
|
+
|
|
84
|
+
addChild(child: ChildContext) {
|
|
85
|
+
if (!children.find(c => c.id === child.id)) {
|
|
86
|
+
children.push(child);
|
|
87
|
+
logger.log(`Parent ${componentName} added child: ${child.name}`);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
removeChild(childId: string) {
|
|
92
|
+
const index = children.findIndex(c => c.id === childId);
|
|
93
|
+
if (index > -1) {
|
|
94
|
+
children.splice(index, 1);
|
|
95
|
+
logger.log(`Parent ${componentName} removed child: ${childId}`);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
broadcast(event: string, data?: any) {
|
|
100
|
+
logger.log(`Parent ${componentName} broadcasting event: ${event}`);
|
|
101
|
+
children.forEach(child => {
|
|
102
|
+
eventBus.emit(`child:${child.id}:${event}`, data);
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
getChildren() {
|
|
107
|
+
return [...children];
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
getExposed() {
|
|
111
|
+
return instance.exposed || {};
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
getChildExposed(childId: string) {
|
|
115
|
+
const child = children.find(c => c.id === childId);
|
|
116
|
+
if (child && child.getExposed) {
|
|
117
|
+
return child.getExposed();
|
|
118
|
+
}
|
|
119
|
+
logger.warn(`Child ${childId} not found or does not have getExposed method`);
|
|
120
|
+
return {};
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
getChildrenExposed() {
|
|
124
|
+
return children
|
|
125
|
+
.filter(child => child.getExposed)
|
|
126
|
+
.map(child => {
|
|
127
|
+
const exposed = child.getExposed();
|
|
128
|
+
return {
|
|
129
|
+
id: child.id,
|
|
130
|
+
name: child.name,
|
|
131
|
+
exposed: exposed
|
|
132
|
+
};
|
|
133
|
+
})
|
|
134
|
+
.filter(item => Object.keys(item.exposed).length > 0);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// 注册父组件并广播事件
|
|
139
|
+
parentMap.set(componentName, parentContext);
|
|
140
|
+
logger.log(`Parent ${componentName} registered`);
|
|
141
|
+
|
|
142
|
+
// 广播父组件注册事件
|
|
143
|
+
eventBus.emit(PARENT_REGISTERED_EVENT, { name: componentName, parent: parentContext });
|
|
144
|
+
|
|
145
|
+
// 组件卸载时清理
|
|
146
|
+
onUnmounted(() => {
|
|
147
|
+
parentMap.delete(componentName);
|
|
148
|
+
eventBus.emit(PARENT_UNMOUNTED_EVENT, { name: componentName });
|
|
149
|
+
logger.log(`Parent ${componentName} unmounted`);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
parentName: componentName,
|
|
154
|
+
children,
|
|
155
|
+
broadcast: parentContext.broadcast,
|
|
156
|
+
getChildren: parentContext.getChildren,
|
|
157
|
+
getChildExposed: parentContext.getChildExposed,
|
|
158
|
+
getChildrenExposed: parentContext.getChildrenExposed
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 子组件 Hook
|
|
164
|
+
*/
|
|
165
|
+
export function useChildren(componentName: string, parentName: string) {
|
|
166
|
+
const instance = getCurrentInstance();
|
|
167
|
+
if (!instance) {
|
|
168
|
+
throw new Error('useChildren must be called within setup function');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!componentName || !parentName) {
|
|
172
|
+
throw new Error('Component name and parent name are required for useChildren');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const instanceId = generateInstanceId(componentName);
|
|
176
|
+
const parentRef = ref<ParentContext | null>(null);
|
|
177
|
+
const parentExposed = ref<Record<string, any>>({});
|
|
178
|
+
|
|
179
|
+
// 热更新时清理旧的子组件
|
|
180
|
+
if (childMap.has(instanceId)) {
|
|
181
|
+
logger.log(`Cleaning up existing child ${componentName} for hot reload`);
|
|
182
|
+
childMap.delete(instanceId);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 获取父组件暴露内容
|
|
186
|
+
const getParentExposed = (): Record<string, any> => {
|
|
187
|
+
if (parentRef.value) {
|
|
188
|
+
const exposed = parentRef.value.getExposed();
|
|
189
|
+
parentExposed.value = exposed;
|
|
190
|
+
return exposed;
|
|
191
|
+
}
|
|
192
|
+
return {};
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// 获取子组件exposed内容
|
|
196
|
+
const getExposed = (): Record<string, any> => {
|
|
197
|
+
return instance.exposed || {};
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// 链接到父组件
|
|
201
|
+
const linkParent = (): boolean => {
|
|
202
|
+
const parent = parentMap.get(parentName);
|
|
203
|
+
if (parent) {
|
|
204
|
+
parentRef.value = parent;
|
|
205
|
+
parent.addChild(childContext);
|
|
206
|
+
getParentExposed();
|
|
207
|
+
logger.log(`Child ${componentName} linked to parent ${parentName}`);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// 子组件上下文
|
|
214
|
+
const childContext: ChildContext = {
|
|
215
|
+
id: instanceId,
|
|
216
|
+
name: componentName,
|
|
217
|
+
|
|
218
|
+
emitToParent(event: string, data?: any) {
|
|
219
|
+
eventBus.emit(`parent:${parentName}:${event}`, {
|
|
220
|
+
data,
|
|
221
|
+
childId: instanceId,
|
|
222
|
+
childName: componentName
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
getParentExposed,
|
|
227
|
+
getInstance() {
|
|
228
|
+
return instance;
|
|
229
|
+
},
|
|
230
|
+
getExposed
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// 注册子组件
|
|
234
|
+
childMap.set(instanceId, childContext);
|
|
235
|
+
logger.log(`Child ${componentName} registered`);
|
|
236
|
+
|
|
237
|
+
// 广播子组件注册事件
|
|
238
|
+
eventBus.emit(CHILD_REGISTERED_EVENT, {
|
|
239
|
+
id: instanceId,
|
|
240
|
+
name: componentName,
|
|
241
|
+
parentName: parentName
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// 立即尝试连接父组件
|
|
245
|
+
let connected = linkParent();
|
|
246
|
+
|
|
247
|
+
// 如果没连接上,监听父组件注册事件
|
|
248
|
+
if (!connected) {
|
|
249
|
+
const parentRegisteredHandler = (eventData: any) => {
|
|
250
|
+
if (eventData.name === parentName) {
|
|
251
|
+
connected = linkParent();
|
|
252
|
+
if (connected) {
|
|
253
|
+
eventBus.off(PARENT_REGISTERED_EVENT, parentRegisteredHandler);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
eventBus.on(PARENT_REGISTERED_EVENT, parentRegisteredHandler);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 监听父组件卸载事件
|
|
261
|
+
const parentUnmountedHandler = (eventData: any) => {
|
|
262
|
+
if (eventData.name === parentName && parentRef.value) {
|
|
263
|
+
parentRef.value = null;
|
|
264
|
+
parentExposed.value = {};
|
|
265
|
+
logger.log(`Parent ${parentName} unmounted, child ${componentName} disconnected`);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
eventBus.on(PARENT_UNMOUNTED_EVENT, parentUnmountedHandler);
|
|
269
|
+
|
|
270
|
+
// 组件卸载时清理
|
|
271
|
+
onUnmounted(() => {
|
|
272
|
+
if (parentRef.value) {
|
|
273
|
+
parentRef.value.removeChild(instanceId);
|
|
274
|
+
}
|
|
275
|
+
childMap.delete(instanceId);
|
|
276
|
+
eventBus.off(PARENT_REGISTERED_EVENT);
|
|
277
|
+
eventBus.off(PARENT_UNMOUNTED_EVENT, parentUnmountedHandler);
|
|
278
|
+
logger.log(`Child ${componentName} unmounted`);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
childId: instanceId,
|
|
283
|
+
childName: componentName,
|
|
284
|
+
parent: parentRef,
|
|
285
|
+
emitToParent: childContext.emitToParent,
|
|
286
|
+
getParentExposed,
|
|
287
|
+
parentExposed: computed(() => parentExposed.value),
|
|
288
|
+
getExposed: childContext.getExposed
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 监听子组件事件
|
|
294
|
+
*/
|
|
295
|
+
export function onChildEvent(parentName: string, event: string, handler: Function) {
|
|
296
|
+
eventBus.on(`parent:${parentName}:${event}`, eventData => {
|
|
297
|
+
handler(eventData.data, eventData.childId, eventData.childName);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 监听父组件事件
|
|
303
|
+
*/
|
|
304
|
+
export function onParentEvent(childId: string, event: string, handler: Function) {
|
|
305
|
+
// 修复类型问题:将 Function 转换为 EventCallback
|
|
306
|
+
const eventCallback = (data?: any, ...args: any[]) => {
|
|
307
|
+
handler(data, ...args);
|
|
308
|
+
};
|
|
309
|
+
eventBus.on(`child:${childId}:${event}`, eventCallback);
|
|
310
|
+
|
|
311
|
+
// 返回取消监听函数
|
|
312
|
+
return () => {
|
|
313
|
+
eventBus.off(`child:${childId}:${event}`, eventCallback);
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 检查父组件是否存在
|
|
319
|
+
*/
|
|
320
|
+
export function hasParent(parentName: string): boolean {
|
|
321
|
+
return parentMap.has(parentName);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* 获取所有已注册的父组件名称
|
|
326
|
+
*/
|
|
327
|
+
export function getRegisteredParents(): string[] {
|
|
328
|
+
return Array.from(parentMap.keys());
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 获取父组件实例
|
|
333
|
+
*/
|
|
334
|
+
export function getParent(parentName: string): ParentContext | undefined {
|
|
335
|
+
return parentMap.get(parentName);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* 获取子组件实例
|
|
340
|
+
*/
|
|
341
|
+
export function getChild(childId: string): ChildContext | undefined {
|
|
342
|
+
return childMap.get(childId);
|
|
343
|
+
}
|