uni-oaview 1.2.1 → 1.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.
@@ -72,7 +72,7 @@
72
72
  margin-top: 16px;
73
73
  color: rgba(25, 36, 44, 1);
74
74
  font-family: PingFang SC;
75
- font-weight: 600;
75
+ font-weight: bold;
76
76
  font-size: 16px;
77
77
  }
78
78
  & > .content {
@@ -77,8 +77,9 @@
77
77
  ref="contentRef"
78
78
  :is="currentModal.component"
79
79
  v-bind="currentModal.props"
80
- @close="(result: any) => closeCurrentModal(result)"
81
- @update="(props: any) => updateCurrentModalProps(props)"
80
+ v-on="computedEventHandlers"
81
+ @handle-submit="handleSubmit"
82
+ @close="(reason?: any) => closeCurrentModal(undefined, false, reason || 'cancel')"
82
83
  />
83
84
  </view>
84
85
  </view>
@@ -90,6 +91,7 @@
90
91
  import { onShow, onHide } from '@dcloudio/uni-app';
91
92
  import { OPEN_MODAL, CLOSE_MODAL, UPDATE_MODAL } from '../../constants';
92
93
  import type { Component } from 'vue';
94
+ import type { ModalBaseEvents, ModalCloseReason, ModalOptions } from '../../src/utils/create-modal';
93
95
 
94
96
  interface ModalHeaderOptions {
95
97
  /** 是否显示头部 */
@@ -116,35 +118,13 @@
116
118
  submitProps?: Record<string, any>;
117
119
  }
118
120
 
119
- interface ModalOptions {
120
- /** 弹框类型:center(中间) | top(顶部) | bottom(底部) | left(左侧) | right(右侧) */
121
- type?: 'center' | 'top' | 'bottom' | 'left' | 'right';
122
- /** 点击蒙层是否关闭弹框(已废弃,使用 isMaskClick) */
123
- maskClick?: boolean;
124
- /** 点击蒙层是否关闭弹框 */
125
- isMaskClick?: boolean;
126
- /** 是否开启打开/关闭动画 */
127
- animation?: boolean;
128
- /** 是否适配底部安全区 */
129
- safeArea?: boolean;
130
- /** 弹框背景色 */
131
- backgroundColor?: string;
132
- /** 蒙层背景色,默认 'rgba(0, 0, 0, 0.4)' */
133
- maskBackgroundColor?: string;
134
- /** 弹框圆角,默认 '16px 16px 0 0' */
135
- borderRadius?: string;
136
- /** 头部配置 */
137
- header?: ModalHeaderOptions;
138
- /** 弹框标题(当 header.title 不存在时使用) */
139
- title?: string;
140
- /** 内容区是否显示内边距,默认 true */
141
- contentPadding?: boolean;
142
- /** 内容区最大高度 */
143
- maxHeight?: string;
144
- /** 是否显示蒙层,默认 true */
145
- showMask?: boolean;
146
- /** 头部操作按钮点击回调 */
147
- onHeaderAction?: (action: string, modalId: string) => void;
121
+ interface ModalComponentRef {
122
+ /** 提交方法(可选) */
123
+ submit?: () => Promise<any> | any;
124
+ /** 验证方法(可选) */
125
+ validate?: () => Promise<boolean>;
126
+ /** 重置方法(可选) */
127
+ reset?: () => void;
148
128
  }
149
129
 
150
130
  interface ModalInstance {
@@ -158,13 +138,36 @@
158
138
  const MAX_MODALS = 5;
159
139
  const modalStack = ref<ModalInstance[]>([]);
160
140
  const popupRef = ref<any>(null);
161
- const contentRef = ref<{ submit: () => Promise<any> | any } | null>(null);
141
+ const contentRef = ref<ModalComponentRef | null>(null);
162
142
  const isPageAlive = ref(false);
163
143
  const isClosing = ref(false);
164
144
  const isH5 = ref(false);
165
145
 
166
146
  const currentModal = computed(() => modalStack.value[modalStack.value.length - 1] || null);
167
147
 
148
+ /**
149
+ * 计算动态事件处理器
150
+ * 将 options.events 中的事件处理函数动态绑定到组件
151
+ */
152
+ const computedEventHandlers = computed(() => {
153
+ const handlers: Record<string, Function> = {};
154
+
155
+ if (currentModal.value?.options.events) {
156
+ Object.entries(currentModal.value.options.events).forEach(([eventName, handler]) => {
157
+ if (typeof handler === 'function') {
158
+ handlers[eventName] = handler;
159
+ }
160
+ });
161
+ }
162
+
163
+ // 向后兼容:onHeaderAction 和 onClose 回调
164
+ if (currentModal.value?.options.onHeaderAction) {
165
+ handlers.onHeaderAction = currentModal.value.options.onHeaderAction;
166
+ }
167
+
168
+ return handlers;
169
+ });
170
+
168
171
  onShow(() => {
169
172
  isPageAlive.value = true;
170
173
  isH5.value = process.env.UNI_PLATFORM === 'h5';
@@ -182,7 +185,7 @@
182
185
  return options.header?.show !== false;
183
186
  };
184
187
 
185
- const containerStyle = (options: ModalOptions) => {
188
+ const containerStyle = (options: ModalOptions): Record<string, string> => {
186
189
  const style: Record<string, string> = {};
187
190
  if (options.maxHeight) {
188
191
  style.maxHeight = options.maxHeight;
@@ -190,7 +193,7 @@
190
193
  return style;
191
194
  };
192
195
 
193
- const bodyStyle = (options: ModalOptions) => {
196
+ const bodyStyle = (options: ModalOptions): Record<string, string> => {
194
197
  const style: Record<string, string> = {};
195
198
  if (options.contentPadding !== false) {
196
199
  style.padding = '16px';
@@ -232,7 +235,7 @@
232
235
  });
233
236
  };
234
237
 
235
- const closeCurrentModal = (result?: any) => {
238
+ const closeCurrentModal = (result?: any, fromSubmit = false, closeReason: ModalCloseReason = 'cancel') => {
236
239
  if (!currentModal.value || isClosing.value) return;
237
240
 
238
241
  isClosing.value = true;
@@ -247,7 +250,21 @@
247
250
  const index = modalStack.value.findIndex((m) => m.id === modal.id);
248
251
  if (index !== -1) {
249
252
  const removed = modalStack.value.splice(index, 1)[0];
250
- removed?.resolve?.(result);
253
+
254
+ // 触发 onClose 事件(兼容旧的回调方式)
255
+ if (modal.options.onClose) {
256
+ modal.options.onClose(closeReason);
257
+ }
258
+
259
+ // 如果有 events.onClose,也触发(新的事件方式)
260
+ if (modal.options.events?.onClose) {
261
+ modal.options.events.onClose(closeReason);
262
+ }
263
+
264
+ // 只有 submit 时才 resolve 触发 Promise
265
+ if (fromSubmit) {
266
+ removed?.resolve?.(result);
267
+ }
251
268
  }
252
269
  isClosing.value = false;
253
270
 
@@ -260,16 +277,15 @@
260
277
  }, 320);
261
278
  };
262
279
 
263
- const updateCurrentModalProps = (newProps: Record<string, any>) => {
264
- if (currentModal.value) {
265
- Object.assign(currentModal.value.props, newProps);
266
- }
267
- };
268
-
269
280
  const handleHeaderAction = (action: string) => {
270
281
  if (currentModal.value?.options.onHeaderAction) {
271
282
  currentModal.value.options.onHeaderAction(action, currentModal.value.id);
272
283
  }
284
+
285
+ // 新的事件方式
286
+ if (currentModal.value?.options.events?.onHeaderAction) {
287
+ currentModal.value.options.events.onHeaderAction(action, currentModal.value.id);
288
+ }
273
289
  };
274
290
 
275
291
  const handleSubmit = async () => {
@@ -287,7 +303,7 @@
287
303
  return;
288
304
  }
289
305
 
290
- closeCurrentModal(result);
306
+ closeCurrentModal(result, true, 'submit');
291
307
  } catch (error) {
292
308
  console.error('Submit failed:', error);
293
309
  } finally {
@@ -300,12 +316,12 @@
300
316
  if (process.env.NODE_ENV === 'development') {
301
317
  console.warn('[oa-modal-wrapper] 内容组件未暴露 submit 方法,弹窗将直接关闭');
302
318
  }
303
- closeCurrentModal();
319
+ closeCurrentModal(undefined, true, 'submit');
304
320
  };
305
321
 
306
322
  const onPopupChange = (e: any) => {
307
323
  if (!e.show && !isClosing.value) {
308
- closeCurrentModal();
324
+ closeCurrentModal(undefined, false, 'mask');
309
325
  }
310
326
  };
311
327
 
@@ -326,7 +342,7 @@
326
342
  popupRef.value.close();
327
343
  }
328
344
  } else if (id === currentModal.value?.id) {
329
- closeCurrentModal(result);
345
+ closeCurrentModal(result, false, 'cancel');
330
346
  }
331
347
  };
332
348
 
@@ -334,7 +350,7 @@
334
350
  if (!isPageAlive.value) return;
335
351
  const { id, props } = params;
336
352
  if (id === currentModal.value?.id) {
337
- updateCurrentModalProps(props);
353
+ Object.assign(currentModal.value.props, props);
338
354
  }
339
355
  };
340
356
 
@@ -345,7 +361,7 @@
345
361
  defineExpose({
346
362
  closeTopModal: () => {
347
363
  if (currentModal.value) {
348
- closeCurrentModal();
364
+ closeCurrentModal(undefined, false, 'header');
349
365
  return true;
350
366
  }
351
367
  return false;
@@ -422,7 +438,7 @@
422
438
 
423
439
  .header-title {
424
440
  font-size: 16px;
425
- font-weight: 600;
441
+ font-weight: bold;
426
442
  color: #19242c;
427
443
  line-height: 22px;
428
444
  }
@@ -74,7 +74,7 @@
74
74
  margin-top: 16px;
75
75
  color: rgba(25, 36, 44, 1);
76
76
  font-family: PingFang SC;
77
- font-weight: 600;
77
+ font-weight: bold;
78
78
  font-size: 16px;
79
79
  }
80
80
  & > .content {
@@ -8,10 +8,10 @@
8
8
 
9
9
  <script lang="ts" setup>
10
10
  import { formatJson } from './utils';
11
- defineProps({
12
- log: {
13
- type: [Object, String, Boolean, Number, Array],
14
- required: true,
15
- },
16
- });
11
+
12
+ interface Props {
13
+ log: any;
14
+ }
15
+
16
+ defineProps<Props>();
17
17
  </script>
@@ -10,7 +10,10 @@
10
10
  <script lang="ts" setup>
11
11
  import { onBeforeUnmount, ref } from 'vue';
12
12
  import { consoleSubject, getConsoleLogs } from 'uniapp-log-sdk';
13
- const logConvert = (items: any[]) =>
13
+
14
+ const MAX_CONSOLE_LOGS = 500;
15
+
16
+ const logConvert = (items: any[]): string =>
14
17
  items.reduce((prev, current, index) => {
15
18
  try {
16
19
  prev +=
@@ -18,14 +21,18 @@
18
21
  ? JSON.stringify(current)
19
22
  : current + '' + (index === items.length - 1 ? '' : ',');
20
23
  } catch (e) {
21
- prev + current;
24
+ prev += current;
22
25
  }
23
26
  return prev;
24
27
  }, '');
25
- const consoleLogs = ref<string[]>(getConsoleLogs().map(logConvert));
28
+
29
+ const consoleLogs = ref<string[]>(getConsoleLogs().slice(-MAX_CONSOLE_LOGS).map(logConvert));
30
+
26
31
  const stop = consoleSubject.subscribe((data: any) => {
27
- consoleLogs.value = data.map(logConvert);
32
+ const converted = data.slice(-MAX_CONSOLE_LOGS).map(logConvert);
33
+ consoleLogs.value = converted;
28
34
  });
35
+
29
36
  onBeforeUnmount(() => {
30
37
  stop?.unsubscribe();
31
38
  });
@@ -11,10 +11,26 @@
11
11
  <script lang="ts" setup>
12
12
  import { ref, onBeforeUnmount } from 'vue';
13
13
  import { errorSubject, getErrorLogs } from 'uniapp-log-sdk';
14
- const errorLogs = ref<Record<string, any>[]>([...new Set(getErrorLogs()?.map((item) => item.join(';')))]);
14
+
15
+ const MAX_ERROR_LOGS = 100;
16
+
17
+ const errorLogs = ref<Record<string, any>[]>([
18
+ ...new Set(
19
+ getErrorLogs()
20
+ ?.slice(-MAX_ERROR_LOGS)
21
+ .map((item: any) => item.join(';')),
22
+ ),
23
+ ]);
24
+
15
25
  const stop = errorSubject.subscribe((data: Record<string, any>[]) => {
16
- errorLogs.value = [...new Set(data?.map((item) => item.join(';')))];
26
+ const unique = [...new Set(data?.map((item: any) => item.join(';')))];
27
+ if (unique.length > MAX_ERROR_LOGS) {
28
+ errorLogs.value = unique.slice(-MAX_ERROR_LOGS);
29
+ } else {
30
+ errorLogs.value = unique;
31
+ }
17
32
  });
33
+
18
34
  onBeforeUnmount(() => {
19
35
  stop?.unsubscribe();
20
36
  });
@@ -18,17 +18,35 @@
18
18
  import { ref, onBeforeUnmount } from 'vue';
19
19
  import awesomeDisplayInfo from './awesome-display-info.vue';
20
20
  import { nativeEventSubject, getNativeEventLogs } from 'uniapp-log-sdk';
21
- const nativeEventLogs = ref<Record<string, any>[]>(getNativeEventLogs());
22
- const stop = nativeEventSubject.subscribe((data) => {
23
- nativeEventLogs.value = data;
21
+
22
+ interface NativeEventLog {
23
+ key: string;
24
+ params: any;
25
+ response: any;
26
+ startTime: number;
27
+ endTime: number;
28
+ }
29
+
30
+ const MAX_NATIVE_EVENT_LOGS = 50;
31
+
32
+ const nativeEventLogs = ref<NativeEventLog[]>(getNativeEventLogs().slice(-MAX_NATIVE_EVENT_LOGS));
33
+
34
+ const stop = nativeEventSubject.subscribe((data: NativeEventLog[]) => {
35
+ if (data.length > MAX_NATIVE_EVENT_LOGS) {
36
+ nativeEventLogs.value = data.slice(-MAX_NATIVE_EVENT_LOGS);
37
+ } else {
38
+ nativeEventLogs.value = data;
39
+ }
24
40
  });
25
- const getTitle = (log: any) => {
41
+
42
+ const getTitle = (log: NativeEventLog): string => {
26
43
  const { startTime, endTime, key } = log;
27
44
  if (startTime && endTime) {
28
45
  return `${key};${(endTime - startTime) / 1000}s`;
29
46
  }
30
47
  return key;
31
48
  };
49
+
32
50
  onBeforeUnmount(() => {
33
51
  stop?.unsubscribe();
34
52
  });
@@ -1,25 +1,27 @@
1
1
  <template>
2
2
  <uni-collapse>
3
3
  <uni-collapse-item
4
- v-for="log in networkLogs"
4
+ v-for="(log, index) in networkLogs"
5
5
  :title="getTitle(log)"
6
6
  :key="log.request.url + log.response?.data?.errno"
7
- :class="{ 'uni-collapse-item-failed': log.response?.statusCode !== 200 || log.response?.data?.errno !== 0 }"
7
+ :class="{
8
+ 'uni-collapse-item-failed': log.response && (log.response.statusCode !== 200 || log.response.data?.errno !== 0),
9
+ }"
8
10
  >
9
- <view style="font-size: 10px">
10
- <view style="word-break: break-all; padding: 0 15px">
11
+ <view style="font-size: 10px; padding: 10px 15px">
12
+ <view style="word-break: break-all; padding: 8px 0; margin-bottom: 10px">
11
13
  <text :selectable="true" style="font-weight: bolder">状态码:</text>
12
14
  {{ log.request.method }} {{ log.response?.statusCode }}
13
15
  </view>
14
- <view style="word-break: break-all; padding: 0 15px">
16
+ <view style="word-break: break-all; padding: 8px 0; margin-bottom: 10px">
15
17
  <text :selectable="true" style="font-weight: bolder">请求头:</text>
16
18
  <awesome-display-info :log="log.request.header" />
17
19
  </view>
18
- <view style="word-break: break-all; padding: 0 15px">
20
+ <view style="word-break: break-all; padding: 8px 0; margin-bottom: 10px">
19
21
  <text :selectable="true" style="font-weight: bolder">请求参数:</text>
20
22
  <awesome-display-info :log="log.request.data" />
21
23
  </view>
22
- <view style="word-break: break-all; padding: 0 15px">
24
+ <view style="word-break: break-all; padding: 8px 0">
23
25
  <text :selectable="true" style="font-weight: bolder">响应数据:</text>
24
26
  <awesome-display-info :log="log.response" />
25
27
  </view>
@@ -32,17 +34,45 @@
32
34
  import { ref, onBeforeUnmount } from 'vue';
33
35
  import awesomeDisplayInfo from './awesome-display-info.vue';
34
36
  import { networkSubject, getNetworkLogs } from 'uniapp-log-sdk';
35
- const networkLogs = ref<{ request: any; response: any; startTime: number; endTime: number }[]>(getNetworkLogs());
36
- const stop = networkSubject.subscribe((data) => {
37
- networkLogs.value = data;
37
+
38
+ interface NetworkLog {
39
+ request: any;
40
+ response: any;
41
+ startTime: number;
42
+ endTime: number;
43
+ }
44
+
45
+ const MAX_NETWORK_LOGS = 100;
46
+
47
+ const networkLogs = ref<NetworkLog[]>(getNetworkLogs().slice(-MAX_NETWORK_LOGS));
48
+
49
+ const stop = networkSubject.subscribe((data: NetworkLog[]) => {
50
+ if (data.length > MAX_NETWORK_LOGS) {
51
+ networkLogs.value = data.slice(-MAX_NETWORK_LOGS);
52
+ } else {
53
+ networkLogs.value = data;
54
+ }
38
55
  });
39
- const getTitle = (log: any) => {
56
+
57
+ const getTitle = (log: NetworkLog): string => {
40
58
  const { startTime, endTime, request } = log;
41
- if (startTime && endTime) {
42
- return `${request.url};${(endTime - startTime) / 1000}s`;
43
- }
44
- return request.url;
59
+ const timeStr = formatTime(startTime);
60
+ const duration = endTime && startTime ? `${((endTime - startTime) / 1000).toFixed(2)}s` : 'pending';
61
+ return `[${timeStr}] ${request.url};${duration}`;
45
62
  };
63
+
64
+ const formatTime = (timestamp: number): string => {
65
+ const date = new Date(timestamp);
66
+ const year = date.getFullYear();
67
+ const month = String(date.getMonth() + 1).padStart(2, '0');
68
+ const day = String(date.getDate()).padStart(2, '0');
69
+ const hours = String(date.getHours()).padStart(2, '0');
70
+ const minutes = String(date.getMinutes()).padStart(2, '0');
71
+ const seconds = String(date.getSeconds()).padStart(2, '0');
72
+ const milliseconds = String(date.getMilliseconds()).padStart(3, '0');
73
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
74
+ };
75
+
46
76
  onBeforeUnmount(() => {
47
77
  stop?.unsubscribe();
48
78
  });
@@ -5,7 +5,7 @@
5
5
  styleType="text"
6
6
  :values="list"
7
7
  :current="current"
8
- @clickItem="(e:any) => (current = e.currentIndex)"
8
+ @clickItem="handleTabClick"
9
9
  ></uni-segmented-control>
10
10
  <view style="height: calc(100% - 48px); overflow-y: auto; padding: 8px">
11
11
  <template v-if="current === 0">
@@ -51,24 +51,38 @@
51
51
  import Storage from './storage.vue';
52
52
  import Console from './console.vue';
53
53
  import System from './system.vue';
54
- const popupRef = ref();
54
+
55
+ interface SegmentedControlClickEvent {
56
+ currentIndex: number;
57
+ }
58
+
59
+ const popupRef = ref<any>();
55
60
  const list = ref(['console', 'network', 'event', 'error', 'storage', 'system']);
56
61
  const current = ref(0);
62
+
63
+ const handleTabClick = (e: SegmentedControlClickEvent) => {
64
+ current.value = e.currentIndex;
65
+ };
66
+
57
67
  const openDebug = () => {
58
68
  popupRef.value?.open('bottom');
59
69
  };
70
+
60
71
  const bottom = ref(200);
61
72
  const right = ref(10);
62
- let pageX: number, pageY: number;
73
+ let pageX: number;
74
+ let pageY: number;
75
+
63
76
  const start = (e: any) => {
64
- let page = e.changedTouches[0];
77
+ const page = e.changedTouches[0];
65
78
  pageX = page.pageX;
66
79
  pageY = page.pageY;
67
80
  };
81
+
68
82
  const move = (e: any) => {
69
- let page = e.changedTouches[0];
70
- let x = page.pageX - pageX;
71
- let y = page.pageY - pageY;
83
+ const page = e.changedTouches[0];
84
+ const x = page.pageX - pageX;
85
+ const y = page.pageY - pageY;
72
86
  pageX = page.pageX;
73
87
  pageY = page.pageY;
74
88
  right.value = right.value - x;
@@ -8,17 +8,45 @@
8
8
  </uni-collapse>
9
9
  </template>
10
10
  <script lang="ts" setup>
11
- import { ref } from 'vue';
11
+ import { ref, onBeforeUnmount } from 'vue';
12
12
  import awesomeDisplayInfo from './awesome-display-info.vue';
13
- const storageLogs = ref<Record<string, any>[]>([]);
13
+
14
+ interface StorageItem {
15
+ key: string;
16
+ data: any;
17
+ }
18
+
19
+ const MAX_STORAGE_ITEMS = 50;
20
+
21
+ // 标记组件是否已挂载(用于防止异步回调导致的内存泄漏)
22
+ let isComponentMounted = true;
23
+
24
+ const storageLogs = ref<StorageItem[]>([]);
25
+
26
+ // 组件卸载时标记
27
+ onBeforeUnmount(() => {
28
+ isComponentMounted = false;
29
+ });
30
+
14
31
  uni.getStorageInfo({
15
32
  success: function (res) {
16
- res.keys?.forEach((key) => {
33
+ // 如果组件已卸载,直接返回,防止内存泄漏
34
+ if (!isComponentMounted) return;
35
+
36
+ const keys = res.keys || [];
37
+ const displayKeys = keys.slice(-MAX_STORAGE_ITEMS);
38
+
39
+ displayKeys.forEach((key) => {
17
40
  storageLogs.value.push({
18
41
  key,
19
42
  data: uni.getStorageSync(key),
20
43
  });
21
44
  });
45
+
46
+ // 如果有省略的 key,在列表开头显示提示
47
+ if (keys.length > MAX_STORAGE_ITEMS) {
48
+ console.warn(`[Storage] 共有 ${keys.length} 个存储项,仅显示最近 ${MAX_STORAGE_ITEMS} 个`);
49
+ }
22
50
  },
23
51
  });
24
52
  </script>
@@ -1,9 +1,19 @@
1
- export const formatJson = (data: any, indent = 0) => {
1
+ const MAX_DEPTH = 10;
2
+ const MAX_ARRAY_LENGTH = 10;
3
+ const MAX_STRING_LENGTH = 300;
4
+
5
+ interface RichTextNode {
6
+ type: string;
7
+ text: string;
8
+ }
9
+
10
+ export const formatJson = (data: any, indent = 0): RichTextNode[] => {
2
11
  const indentStr = ' '.repeat(indent);
3
12
 
4
- // 防止死循环,限制最大递归深度
5
- if (indent > 100) {
6
- return [{ type: 'text', text: 'Error: Maximum recursion depth exceeded.' }];
13
+ // 防止死循环,限制最大递归深度(indent 每层增加 4,所以深度 = indent / 4)
14
+ const currentDepth = Math.floor(indent / 4);
15
+ if (currentDepth > MAX_DEPTH) {
16
+ return [{ type: 'text', text: '...' }];
7
17
  }
8
18
 
9
19
  if (Array.isArray(data)) {
@@ -15,32 +25,46 @@ export const formatJson = (data: any, indent = 0) => {
15
25
  }
16
26
  };
17
27
 
18
- const formatArray = (data: any[], indent: number, indentStr: string) => {
28
+ const formatArray = (data: any[], indent: number, indentStr: string): RichTextNode[] => {
19
29
  if (data.length === 0) {
20
30
  return [{ type: 'text', text: '[]' }];
21
31
  }
22
32
 
23
- let formatted = [{ type: 'text', text: '[\n' }];
24
- data.forEach((item, index) => {
25
- formatted.push({ type: 'text', text: indentStr + ' '.repeat(2) });
26
- formatted.push(...formatJson(item, indent + 2));
27
- if (index < data.length - 1) {
33
+ let formatted: RichTextNode[] = [{ type: 'text', text: '[\n' }];
34
+
35
+ // 如果数组长度超过限制,只显示前 MAX_ARRAY_LENGTH
36
+ const displayLength = Math.min(data.length, MAX_ARRAY_LENGTH);
37
+ const hasOmitted = data.length > MAX_ARRAY_LENGTH;
38
+
39
+ data.slice(0, displayLength).forEach((item, index) => {
40
+ formatted.push({ type: 'text', text: indentStr + ' '.repeat(4) });
41
+ formatted.push(...formatJson(item, indent + 4));
42
+ if (index < displayLength - 1) {
28
43
  formatted.push({ type: 'text', text: ',\n' });
29
44
  } else {
30
45
  formatted.push({ type: 'text', text: '\n' });
31
46
  }
32
47
  });
48
+
49
+ // 如果有省略的元素,显示提示
50
+ if (hasOmitted) {
51
+ formatted.push({
52
+ type: 'text',
53
+ text: indentStr + `... (共 ${data.length} 个,显示 ${displayLength} 个)\n`,
54
+ });
55
+ }
56
+
33
57
  formatted.push({ type: 'text', text: indentStr + ']' });
34
58
 
35
59
  return formatted;
36
60
  };
37
61
 
38
- const formatObject = (data: Record<string, any>, indent: number, indentStr: string) => {
39
- let formatted = [{ type: 'text', text: '{\n' }];
62
+ const formatObject = (data: Record<string, any>, indent: number, indentStr: string): RichTextNode[] => {
63
+ let formatted: RichTextNode[] = [{ type: 'text', text: '{\n' }];
40
64
  const keys = Object.keys(data);
41
65
  keys.forEach((key, index) => {
42
- formatted.push({ type: 'text', text: indentStr + ' '.repeat(2) + key + ': ' });
43
- formatted.push(...formatJson(data[key], indent + 2));
66
+ formatted.push({ type: 'text', text: indentStr + ' '.repeat(4) + key + ': ' });
67
+ formatted.push(...formatJson(data[key], indent + 4));
44
68
  if (index < keys.length - 1) {
45
69
  formatted.push({ type: 'text', text: ',\n' });
46
70
  }
@@ -50,8 +74,17 @@ const formatObject = (data: Record<string, any>, indent: number, indentStr: stri
50
74
  return formatted;
51
75
  };
52
76
 
53
- const formatPrimitive = (data: any) => {
77
+ const formatPrimitive = (data: any): RichTextNode[] => {
54
78
  if (typeof data === 'string') {
79
+ // 字符串超过长度限制时截断
80
+ if (data.length > MAX_STRING_LENGTH) {
81
+ return [
82
+ {
83
+ type: 'text',
84
+ text: `"${data.slice(0, MAX_STRING_LENGTH)}..." (长度 ${data.length})`,
85
+ },
86
+ ];
87
+ }
55
88
  return [{ type: 'text', text: `"${data}"` }];
56
89
  } else {
57
90
  return [{ type: 'text', text: String(data) }];
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _mp_rt1_vue___Ref from 'vue';
2
- import { Component, App } from 'vue';
2
+ import { Component, Plugin } from 'vue';
3
3
 
4
4
  /**
5
5
  * 上报APP启动小程序的时候的入参
@@ -74,6 +74,18 @@ declare function createDebounceFn(callback: (...params: any[]) => void, time: nu
74
74
  */
75
75
  declare function getTempFilePathForCookie(url: string, cookie?: string): Promise<string>;
76
76
 
77
+ /** 弹框关闭原因类型 */
78
+ type ModalCloseReason = 'submit' | 'cancel' | 'mask' | 'header';
79
+ /** 基础事件接口 - 所有弹框都应该支持 */
80
+ interface ModalBaseEvents {
81
+ /** 弹框关闭时触发 */
82
+ onClose?: (reason: ModalCloseReason) => void;
83
+ }
84
+ /** 头部事件接口 */
85
+ interface ModalHeaderEvents {
86
+ /** 头部操作按钮点击回调 */
87
+ onHeaderAction?: (action: string, modalId: string) => void;
88
+ }
77
89
  interface ModalHeaderOptions {
78
90
  /** 是否显示头部 */
79
91
  show?: boolean;
@@ -98,7 +110,7 @@ interface ModalHeaderOptions {
98
110
  /** 提交按钮的属性 */
99
111
  submitProps?: Record<string, any>;
100
112
  }
101
- interface ModalOptions {
113
+ interface ModalOptions<E extends ModalBaseEvents = ModalBaseEvents> {
102
114
  /** 弹框类型:center(中间) | top(顶部) | bottom(底部) | left(左侧) | right(右侧) */
103
115
  type?: 'center' | 'top' | 'bottom' | 'left' | 'right';
104
116
  /** 点击蒙层是否关闭弹框(已废弃,使用 isMaskClick) */
@@ -125,16 +137,22 @@ interface ModalOptions {
125
137
  maxHeight?: string;
126
138
  /** 是否显示蒙层,默认 true */
127
139
  showMask?: boolean;
128
- /** 头部操作按钮点击回调 */
140
+ /** 内容组件的事件回调对象 */
141
+ events?: E & Record<string, (...args: any[]) => void>;
142
+ /** 头部操作按钮点击回调(已废弃,使用 events.onHeaderAction) */
129
143
  onHeaderAction?: (action: string, modalId: string) => void;
144
+ /** 弹框关闭回调(已废弃,使用 events.onClose) */
145
+ onClose?: (reason?: ModalCloseReason) => void;
130
146
  }
131
- interface CreateModalReturn {
132
- /** 订阅弹框关闭事件,获取返回结果 */
133
- subscribe: (callback: (result: any) => void) => CreateModalReturn;
147
+ interface CreateModalReturn<E extends ModalBaseEvents = ModalBaseEvents> extends Promise<any> {
148
+ /** 监听特定事件 */
149
+ on<K extends keyof E>(event: K, callback: E[K] extends (...args: infer Args) => void ? (...args: Args) => void : never): this;
150
+ /** 订阅弹框关闭事件,获取返回结果(向后兼容) */
151
+ subscribe(callback: (result: any) => void): this;
134
152
  /** 关闭弹框 */
135
- close: (result?: any) => void;
153
+ close(result?: any): void;
136
154
  /** 更新弹框内容组件的 props */
137
- updateProps: (props: Record<string, any>) => void;
155
+ updateProps(props: Record<string, any>): void;
138
156
  /** Promise then 方法 */
139
157
  then: Promise<any>['then'];
140
158
  /** Promise catch 方法 */
@@ -143,12 +161,32 @@ interface CreateModalReturn {
143
161
  /**
144
162
  * 函数式打开弹框
145
163
  * @param component 弹框内容组件
146
- * @param options 弹框配置
164
+ * @param options 弹框配置和内容组件属性
147
165
  * @returns 弹框控制器
166
+ *
167
+ * @example
168
+ * ```ts
169
+ * interface UserFormEvents extends ModalBaseEvents {
170
+ * onValidationError?: (errors: Record<string, string>) => void
171
+ * onFieldChange?: (field: string, value: any) => void
172
+ * }
173
+ *
174
+ * createModal<UserFormEvents>(UserForm, {
175
+ * title: '编辑用户',
176
+ * props: { userId: '123' },
177
+ * events: {
178
+ * onValidationError: (errors) => console.log(errors),
179
+ * onFieldChange: (field, value) => console.log(field, value),
180
+ * onClose: (reason) => console.log('关闭原因:', reason)
181
+ * }
182
+ * })
183
+ * .on('onValidationError', (errors) => { ... })
184
+ * .then((result) => console.log('最终结果:', result))
185
+ * ```
148
186
  */
149
- declare function createModal(component: Component, options?: ModalOptions & {
187
+ declare function createModal<E extends ModalBaseEvents = ModalBaseEvents>(component: Component, options?: ModalOptions<E> & {
150
188
  props?: Record<string, any>;
151
- }): CreateModalReturn;
189
+ }): CreateModalReturn<E>;
152
190
  /**
153
191
  * 关闭所有弹框
154
192
  */
@@ -281,9 +319,7 @@ declare function useCookieTempFileUrl(url: string): {
281
319
  tempUrl: _mp_rt1_vue___Ref<string>;
282
320
  };
283
321
 
284
- declare const _default: {
285
- install: (app: App<Element>) => void;
286
- };
322
+ declare const plugin: Plugin;
287
323
  /**
288
324
  * 向客户端获取数据
289
325
  * @param eventName 事件名称
@@ -293,4 +329,4 @@ declare const _default: {
293
329
  */
294
330
  declare function getDataByApp(eventName: string, params: Record<string, any>, immediate?: boolean): Promise<any>;
295
331
 
296
- export { CreateModalReturn, Message, MessageBox, ModalHeaderOptions, ModalOptions, RouteMethodName, VersionCheckRuntimeOptions, VersionCheckRuntimeState, VersionCheckSource, VersionCheckTriggerContext, closeAllModals, compareVersions, createDebounceFn, createModal, createVersionCheckRuntime, _default as default, getAppVersion, getDataByApp, getTempFilePathForCookie, isCurrentVersionHigher, sendLaunchAppParamsLog, useCookieTempFileUrl, versionCheckRuntime };
332
+ export { CreateModalReturn, Message, MessageBox, ModalBaseEvents, ModalCloseReason, ModalHeaderEvents, ModalHeaderOptions, ModalOptions, RouteMethodName, VersionCheckRuntimeOptions, VersionCheckRuntimeState, VersionCheckSource, VersionCheckTriggerContext, closeAllModals, compareVersions, createDebounceFn, createModal, createVersionCheckRuntime, plugin as default, getAppVersion, getDataByApp, getTempFilePathForCookie, isCurrentVersionHigher, sendLaunchAppParamsLog, useCookieTempFileUrl, versionCheckRuntime };
package/dist/index.esm.js CHANGED
@@ -741,8 +741,28 @@ function __rest(s, e) {
741
741
  /**
742
742
  * 函数式打开弹框
743
743
  * @param component 弹框内容组件
744
- * @param options 弹框配置
744
+ * @param options 弹框配置和内容组件属性
745
745
  * @returns 弹框控制器
746
+ *
747
+ * @example
748
+ * ```ts
749
+ * interface UserFormEvents extends ModalBaseEvents {
750
+ * onValidationError?: (errors: Record<string, string>) => void
751
+ * onFieldChange?: (field: string, value: any) => void
752
+ * }
753
+ *
754
+ * createModal<UserFormEvents>(UserForm, {
755
+ * title: '编辑用户',
756
+ * props: { userId: '123' },
757
+ * events: {
758
+ * onValidationError: (errors) => console.log(errors),
759
+ * onFieldChange: (field, value) => console.log(field, value),
760
+ * onClose: (reason) => console.log('关闭原因:', reason)
761
+ * }
762
+ * })
763
+ * .on('onValidationError', (errors) => { ... })
764
+ * .then((result) => console.log('最终结果:', result))
765
+ * ```
746
766
  */
747
767
  function createModal(component) {
748
768
  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
@@ -751,6 +771,7 @@ function createModal(component) {
751
771
  var promise = new Promise(function (resolve, reject) {
752
772
  promiseResolve = resolve;
753
773
  });
774
+ var eventListeners = new Map();
754
775
  var props = options.props,
755
776
  modalOptions = __rest(options, ["props"]);
756
777
  // 发送打开事件
@@ -764,6 +785,13 @@ function createModal(component) {
764
785
  }
765
786
  });
766
787
  return {
788
+ on: function on(event, callback) {
789
+ if (!eventListeners.has(event)) {
790
+ eventListeners.set(event, new Set());
791
+ }
792
+ eventListeners.get(event).add(callback);
793
+ return this;
794
+ },
767
795
  subscribe: function subscribe(callback) {
768
796
  promise.then(function (result) {
769
797
  return callback === null || callback === void 0 ? void 0 : callback(result);
@@ -3887,7 +3915,7 @@ var install = function install(app) {
3887
3915
  }
3888
3916
  });
3889
3917
  };
3890
- var index = {
3918
+ var plugin = {
3891
3919
  install: install
3892
3920
  };
3893
3921
  /**
@@ -3920,4 +3948,4 @@ function getDataByApp(eventName, params) {
3920
3948
  });
3921
3949
  }
3922
3950
 
3923
- export { Message, MessageBox, closeAllModals, compareVersions, createDebounceFn, createModal, createVersionCheckRuntime, index as default, getAppVersion, getDataByApp, getTempFilePathForCookie, isCurrentVersionHigher, sendLaunchAppParamsLog, useCookieTempFileUrl, versionCheckRuntime };
3951
+ export { Message, MessageBox, closeAllModals, compareVersions, createDebounceFn, createModal, createVersionCheckRuntime, plugin as default, getAppVersion, getDataByApp, getTempFilePathForCookie, isCurrentVersionHigher, sendLaunchAppParamsLog, useCookieTempFileUrl, versionCheckRuntime };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uni-oaview",
3
- "version": "1.2.01",
3
+ "version": "1.2.03",
4
4
  "description": "uniapp小程序组件库",
5
5
  "main": "dist/index.esm.js",
6
6
  "typings": "dist/index.d.ts",
package/src/index.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { App } from 'vue';
1
+ import type { App, Plugin } from 'vue';
2
2
 
3
3
  import { sendLaunchAppParamsLog } from './utils';
4
4
  export * from './utils';
5
5
  export * from './composables';
6
6
 
7
- const install = (app: App<Element>) => {
7
+ const install = (app: App) => {
8
8
  app.mixin({
9
9
  onLaunch(params: any) {
10
10
  sendLaunchAppParamsLog(params);
@@ -12,10 +12,12 @@ const install = (app: App<Element>) => {
12
12
  });
13
13
  };
14
14
 
15
- export default {
15
+ const plugin: Plugin = {
16
16
  install,
17
17
  };
18
18
 
19
+ export default plugin;
20
+
19
21
  /**
20
22
  * 向客户端获取数据
21
23
  * @param eventName 事件名称
@@ -1,6 +1,21 @@
1
1
  import type { Component } from 'vue';
2
2
  import { OPEN_MODAL, CLOSE_MODAL, UPDATE_MODAL } from '../../constants';
3
3
 
4
+ /** 弹框关闭原因类型 */
5
+ export type ModalCloseReason = 'submit' | 'cancel' | 'mask' | 'header';
6
+
7
+ /** 基础事件接口 - 所有弹框都应该支持 */
8
+ export interface ModalBaseEvents {
9
+ /** 弹框关闭时触发 */
10
+ onClose?: (reason: ModalCloseReason) => void;
11
+ }
12
+
13
+ /** 头部事件接口 */
14
+ export interface ModalHeaderEvents {
15
+ /** 头部操作按钮点击回调 */
16
+ onHeaderAction?: (action: string, modalId: string) => void;
17
+ }
18
+
4
19
  export interface ModalHeaderOptions {
5
20
  /** 是否显示头部 */
6
21
  show?: boolean;
@@ -26,7 +41,7 @@ export interface ModalHeaderOptions {
26
41
  submitProps?: Record<string, any>;
27
42
  }
28
43
 
29
- export interface ModalOptions {
44
+ export interface ModalOptions<E extends ModalBaseEvents = ModalBaseEvents> {
30
45
  /** 弹框类型:center(中间) | top(顶部) | bottom(底部) | left(左侧) | right(右侧) */
31
46
  type?: 'center' | 'top' | 'bottom' | 'left' | 'right';
32
47
  /** 点击蒙层是否关闭弹框(已废弃,使用 isMaskClick) */
@@ -53,19 +68,33 @@ export interface ModalOptions {
53
68
  maxHeight?: string;
54
69
  /** 是否显示蒙层,默认 true */
55
70
  showMask?: boolean;
56
- /** 头部操作按钮点击回调 */
71
+ /** 内容组件的事件回调对象 */
72
+ events?: E & Record<string, (...args: any[]) => void>;
73
+ /** 头部操作按钮点击回调(已废弃,使用 events.onHeaderAction) */
57
74
  onHeaderAction?: (action: string, modalId: string) => void;
75
+ /** 弹框关闭回调(已废弃,使用 events.onClose) */
76
+ onClose?: (reason?: ModalCloseReason) => void;
58
77
  }
59
78
 
60
- export interface CreateModalReturn {
61
- /** 订阅弹框关闭事件,获取返回结果 */
62
- subscribe: (callback: (result: any) => void) => CreateModalReturn;
79
+ export interface CreateModalReturn<E extends ModalBaseEvents = ModalBaseEvents> extends Promise<any> {
80
+ /** 监听特定事件 */
81
+ on<K extends keyof E>(
82
+ event: K,
83
+ callback: E[K] extends (...args: infer Args) => void ? (...args: Args) => void : never,
84
+ ): this;
85
+
86
+ /** 订阅弹框关闭事件,获取返回结果(向后兼容) */
87
+ subscribe(callback: (result: any) => void): this;
88
+
63
89
  /** 关闭弹框 */
64
- close: (result?: any) => void;
90
+ close(result?: any): void;
91
+
65
92
  /** 更新弹框内容组件的 props */
66
- updateProps: (props: Record<string, any>) => void;
93
+ updateProps(props: Record<string, any>): void;
94
+
67
95
  /** Promise then 方法 */
68
96
  then: Promise<any>['then'];
97
+
69
98
  /** Promise catch 方法 */
70
99
  catch: Promise<any>['catch'];
71
100
  }
@@ -73,13 +102,33 @@ export interface CreateModalReturn {
73
102
  /**
74
103
  * 函数式打开弹框
75
104
  * @param component 弹框内容组件
76
- * @param options 弹框配置
105
+ * @param options 弹框配置和内容组件属性
77
106
  * @returns 弹框控制器
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * interface UserFormEvents extends ModalBaseEvents {
111
+ * onValidationError?: (errors: Record<string, string>) => void
112
+ * onFieldChange?: (field: string, value: any) => void
113
+ * }
114
+ *
115
+ * createModal<UserFormEvents>(UserForm, {
116
+ * title: '编辑用户',
117
+ * props: { userId: '123' },
118
+ * events: {
119
+ * onValidationError: (errors) => console.log(errors),
120
+ * onFieldChange: (field, value) => console.log(field, value),
121
+ * onClose: (reason) => console.log('关闭原因:', reason)
122
+ * }
123
+ * })
124
+ * .on('onValidationError', (errors) => { ... })
125
+ * .then((result) => console.log('最终结果:', result))
126
+ * ```
78
127
  */
79
- export function createModal(
128
+ export function createModal<E extends ModalBaseEvents = ModalBaseEvents>(
80
129
  component: Component,
81
- options: ModalOptions & { props?: Record<string, any> } = {},
82
- ): CreateModalReturn {
130
+ options: ModalOptions<E> & { props?: Record<string, any> } = {},
131
+ ): CreateModalReturn<E> {
83
132
  const id = `modal-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
84
133
 
85
134
  let promiseResolve: (value: any) => void;
@@ -90,6 +139,8 @@ export function createModal(
90
139
  promiseReject = reject;
91
140
  });
92
141
 
142
+ const eventListeners = new Map<string, Set<Function>>();
143
+
93
144
  const { props, ...modalOptions } = options;
94
145
 
95
146
  // 发送打开事件
@@ -104,6 +155,14 @@ export function createModal(
104
155
  });
105
156
 
106
157
  return {
158
+ on(event: string, callback: Function) {
159
+ if (!eventListeners.has(event)) {
160
+ eventListeners.set(event, new Set());
161
+ }
162
+ eventListeners.get(event)!.add(callback);
163
+ return this;
164
+ },
165
+
107
166
  subscribe(callback: (result: any) => void) {
108
167
  promise.then((result) => callback?.(result));
109
168
  return this;
@@ -119,7 +178,7 @@ export function createModal(
119
178
 
120
179
  then: promise.then.bind(promise),
121
180
  catch: promise.catch.bind(promise),
122
- };
181
+ } as CreateModalReturn<E>;
123
182
  }
124
183
 
125
184
  /**