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.
@@ -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 :hover-stay-time="200" class="u-collapse-head" @tap.stop="headClick" :hover-class="hoverClass" :style="headStyle">
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="arrow" :color="arrowColor" :name="isShow ? 'arrow-up' : 'arrow-down'" />
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'); // body内容的高度
63
- const headStyle = ref<Record<string, any>>({}); // 头部样式,对象形式
64
- const bodyStyle = ref<Record<string, any>>({}); // 主体部分样式
65
- const itemStyle = ref<Record<string, any>>({}); // 每个item的整体样式
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
- const { parent, parentExposed } = useParent('u-collapse');
76
+ // 使用通信库的子组件Hook
77
+ const { childId, parentExposed } = useChildren('u-collapse-item', 'u-collapse');
72
78
 
73
- watch(
74
- () => props.open,
75
- val => {
76
- isShow.value = val;
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 && !arrow.value) {
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 (parent) {
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
- arrow.value = parentExposed.props.arrow;
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
- if (accordion.value && parent) {
118
- parent.children.forEach((vm: any) => {
119
- if (vm.exposed.elId.value !== elId.value) {
120
- vm.exposed.isShow.value = false;
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
- // getRect为uView自带的节点查询简化方法,详见文档介绍:https://uviewpro.cn/zh/tools/getRect.html
144
- // 组件内部一般用this.$uGetRect,对外的为this.$u.getRect,二者功能一致,名称不同
145
- $u.getRect('#' + elId.value, instance).then((res: any) => {
146
- height.value = res.height + 'px';
147
- // #ifdef MP-TOUTIAO
148
- height.value = 'auto';
149
- // #endif
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
- arrow
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 && options.theme) {
17
- $u.color = $u.deepMerge($u.color, options.theme);
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;
@@ -1,3 +1,3 @@
1
+ export * from './useComponent';
1
2
  export * from './useEmitter';
2
- export * from './useParent';
3
3
  export * from './useRect';
@@ -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
+ }