@djvlc/runtime-host-vue 1.0.2 → 1.0.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/dist/index.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ DJVLC_CONFIG_KEY: () => DJVLC_CONFIG_KEY,
23
24
  DJVPlugin: () => DJVPlugin,
24
25
  DJVProvider: () => DJVProvider,
25
26
  DJVRenderer: () => DJVRenderer,
@@ -28,13 +29,19 @@ __export(index_exports, {
28
29
  injectRuntime: () => injectRuntime,
29
30
  provideRuntime: () => provideRuntime,
30
31
  useAction: () => useAction,
32
+ useComponentState: () => useComponentState,
31
33
  useDJVRuntime: () => useDJVRuntime,
32
34
  useData: () => useData,
35
+ useDebouncedAction: () => useDebouncedAction,
36
+ useGlobalConfig: () => useGlobalConfig,
33
37
  useHostApi: () => useHostApi,
38
+ useLifecycle: () => useLifecycle,
39
+ usePageInfo: () => usePageInfo,
34
40
  useQuery: () => useQuery,
35
41
  useRuntimeEvent: () => useRuntimeEvent,
36
42
  useRuntimeState: () => useRuntimeState,
37
- useRuntimeStateWritable: () => useRuntimeStateWritable
43
+ useRuntimeStateWritable: () => useRuntimeStateWritable,
44
+ useWhen: () => useWhen
38
45
  });
39
46
  module.exports = __toCommonJS(index_exports);
40
47
 
@@ -48,11 +55,33 @@ function createVueRuntime(options) {
48
55
  const page = (0, import_vue.shallowRef)(null);
49
56
  const error = (0, import_vue.shallowRef)(null);
50
57
  const hostApi = (0, import_vue.shallowRef)(null);
58
+ const isReady = (0, import_vue.computed)(() => phase.value === "ready");
59
+ const hasError = (0, import_vue.computed)(() => phase.value === "error" || error.value !== null);
60
+ const metrics = (0, import_vue.shallowRef)({
61
+ initTime: 0,
62
+ loadTime: 0,
63
+ renderTime: 0,
64
+ totalTime: 0,
65
+ initTimestamp: null,
66
+ readyTimestamp: null
67
+ });
68
+ let startTime = 0;
69
+ let initStartTime = 0;
70
+ let loadStartTime = 0;
71
+ let renderStartTime = 0;
51
72
  const init = async () => {
52
73
  const container = options.containerRef?.value;
53
74
  if (!container) {
54
75
  throw new Error("Container element not found");
55
76
  }
77
+ startTime = performance.now();
78
+ initStartTime = startTime;
79
+ if (options.enableMetrics) {
80
+ metrics.value = {
81
+ ...metrics.value,
82
+ initTimestamp: Date.now()
83
+ };
84
+ }
56
85
  const runtimeInstance = (0, import_runtime_core.createRuntime)({
57
86
  ...options,
58
87
  container,
@@ -77,22 +106,45 @@ function createVueRuntime(options) {
77
106
  });
78
107
  await runtimeInstance.init();
79
108
  hostApi.value = runtimeInstance.getHostApi();
109
+ if (options.enableMetrics) {
110
+ metrics.value = {
111
+ ...metrics.value,
112
+ initTime: performance.now() - initStartTime
113
+ };
114
+ }
80
115
  };
81
116
  const load = async () => {
82
117
  if (!runtime.value) {
83
118
  throw new Error("Runtime not initialized");
84
119
  }
120
+ loadStartTime = performance.now();
85
121
  const result = await runtime.value.load();
86
122
  page.value = result;
87
123
  hostApi.value = runtime.value.getHostApi();
124
+ if (options.enableMetrics) {
125
+ metrics.value = {
126
+ ...metrics.value,
127
+ loadTime: performance.now() - loadStartTime
128
+ };
129
+ }
88
130
  return result;
89
131
  };
90
132
  const render = async () => {
91
133
  if (!runtime.value) {
92
134
  throw new Error("Runtime not initialized");
93
135
  }
136
+ renderStartTime = performance.now();
94
137
  await runtime.value.render();
95
138
  loading.value = false;
139
+ if (options.enableMetrics) {
140
+ const now = performance.now();
141
+ metrics.value = {
142
+ ...metrics.value,
143
+ renderTime: now - renderStartTime,
144
+ totalTime: now - startTime,
145
+ readyTimestamp: Date.now()
146
+ };
147
+ }
96
148
  };
97
149
  const destroy = () => {
98
150
  runtime.value?.destroy();
@@ -103,12 +155,41 @@ function createVueRuntime(options) {
103
155
  phase.value = "idle";
104
156
  loading.value = true;
105
157
  };
158
+ const reload = async () => {
159
+ if (!runtime.value) {
160
+ throw new Error("Runtime not initialized");
161
+ }
162
+ error.value = null;
163
+ loading.value = true;
164
+ await load();
165
+ await render();
166
+ };
106
167
  const setVariable = (key, value) => {
107
168
  runtime.value?.setVariable(key, value);
108
169
  };
170
+ const setVariables = (variables) => {
171
+ if (!runtime.value) return;
172
+ Object.entries(variables).forEach(([key, value]) => {
173
+ runtime.value?.setVariable(key, value);
174
+ });
175
+ };
176
+ const getVariable = (key) => {
177
+ return runtime.value?.getState().variables[key];
178
+ };
109
179
  const refreshData = async (queryId) => {
110
180
  await runtime.value?.refreshData(queryId);
111
181
  };
182
+ const executeAction = async (actionType, params) => {
183
+ const api = hostApi.value;
184
+ if (!api) {
185
+ throw new Error("HostAPI not available");
186
+ }
187
+ const response = await api.executeAction(actionType, params || {});
188
+ if (response.success) {
189
+ return response.data;
190
+ }
191
+ throw new Error(response.errorMessage || "Action failed");
192
+ };
112
193
  return {
113
194
  runtime,
114
195
  loading: (0, import_vue.readonly)(loading),
@@ -116,12 +197,19 @@ function createVueRuntime(options) {
116
197
  page,
117
198
  error,
118
199
  hostApi,
200
+ isReady,
201
+ hasError,
202
+ metrics,
119
203
  init,
120
204
  load,
121
205
  render,
122
206
  destroy,
207
+ reload,
123
208
  setVariable,
124
- refreshData
209
+ setVariables,
210
+ getVariable,
211
+ refreshData,
212
+ executeAction
125
213
  };
126
214
  }
127
215
 
@@ -143,16 +231,30 @@ function injectRuntime() {
143
231
  }
144
232
  function useDJVRuntime() {
145
233
  const context = injectRuntime();
234
+ const loading = (0, import_vue2.computed)(() => {
235
+ const phase = context.value.state.phase;
236
+ return phase !== "ready" && phase !== "error";
237
+ });
238
+ const isReady = (0, import_vue2.computed)(() => context.value.state.phase === "ready");
239
+ const hasError = (0, import_vue2.computed)(() => context.value.state.phase === "error" || context.value.state.error !== null);
240
+ const reload = async () => {
241
+ const runtime = context.value.runtime;
242
+ if (!runtime) {
243
+ throw new Error("Runtime not available");
244
+ }
245
+ await runtime.load();
246
+ await runtime.render();
247
+ };
146
248
  return {
147
249
  runtime: (0, import_vue2.computed)(() => context.value.runtime),
148
250
  state: (0, import_vue2.computed)(() => context.value.state),
149
- loading: (0, import_vue2.computed)(() => {
150
- const phase = context.value.state.phase;
151
- return phase !== "ready" && phase !== "error";
152
- }),
251
+ loading,
153
252
  phase: (0, import_vue2.computed)(() => context.value.state.phase),
154
253
  error: (0, import_vue2.computed)(() => context.value.state.error),
155
- page: (0, import_vue2.computed)(() => context.value.state.page)
254
+ page: (0, import_vue2.computed)(() => context.value.state.page),
255
+ isReady,
256
+ hasError,
257
+ reload
156
258
  };
157
259
  }
158
260
  function useHostApi() {
@@ -179,10 +281,12 @@ function useRuntimeStateWritable(key, defaultValue) {
179
281
  };
180
282
  return [value, setValue];
181
283
  }
182
- function useQuery(queryId) {
284
+ function useQuery(queryId, options) {
183
285
  const context = injectRuntime();
184
286
  const loading = (0, import_vue2.ref)(false);
185
287
  const error = (0, import_vue2.ref)(null);
288
+ const lastUpdated = (0, import_vue2.ref)(null);
289
+ let intervalTimer = null;
186
290
  const data = (0, import_vue2.computed)(() => {
187
291
  return context.value.state.queries[queryId];
188
292
  });
@@ -191,41 +295,84 @@ function useQuery(queryId) {
191
295
  error.value = null;
192
296
  try {
193
297
  await context.value.runtime?.refreshData(queryId);
298
+ lastUpdated.value = Date.now();
194
299
  } catch (e) {
195
300
  error.value = e;
196
301
  } finally {
197
302
  loading.value = false;
198
303
  }
199
304
  };
305
+ (0, import_vue2.onMounted)(() => {
306
+ if (options?.refreshOnMount && context.value.runtime) {
307
+ refetch();
308
+ }
309
+ if (options?.refreshInterval && options.refreshInterval > 0) {
310
+ intervalTimer = setInterval(refetch, options.refreshInterval);
311
+ }
312
+ if (options?.refreshOnFocus) {
313
+ window.addEventListener("focus", refetch);
314
+ }
315
+ });
316
+ (0, import_vue2.onUnmounted)(() => {
317
+ if (intervalTimer) {
318
+ clearInterval(intervalTimer);
319
+ }
320
+ if (options?.refreshOnFocus) {
321
+ window.removeEventListener("focus", refetch);
322
+ }
323
+ });
200
324
  return {
201
325
  data,
202
326
  loading,
203
327
  error,
204
- refetch
328
+ refetch,
329
+ lastUpdated
205
330
  };
206
331
  }
207
- function useAction(actionType) {
332
+ function useAction(actionType, options) {
208
333
  const context = injectRuntime();
209
334
  const loading = (0, import_vue2.ref)(false);
210
- const result = (0, import_vue2.ref)();
335
+ const result = (0, import_vue2.shallowRef)();
211
336
  const error = (0, import_vue2.ref)(null);
212
- const execute = async (params) => {
337
+ const executionCount = (0, import_vue2.ref)(0);
338
+ const reset = () => {
339
+ result.value = void 0;
340
+ error.value = null;
341
+ executionCount.value = 0;
342
+ };
343
+ const executeWithRetry = async (params, retriesLeft) => {
213
344
  const hostApi = context.value.hostApi;
214
345
  if (!hostApi) {
215
346
  throw new Error("HostAPI not available");
216
347
  }
217
- loading.value = true;
218
- error.value = null;
219
348
  try {
220
349
  const response = await hostApi.executeAction(actionType, params);
221
350
  if (response.success) {
222
- result.value = response.data;
223
351
  return response.data;
224
352
  } else {
225
- throw new Error(response.message || "Action failed");
353
+ throw new Error(response.errorMessage || "Action failed");
226
354
  }
227
355
  } catch (e) {
228
- error.value = e;
356
+ if (retriesLeft > 0) {
357
+ await new Promise((resolve) => setTimeout(resolve, options?.retryDelay || 1e3));
358
+ return executeWithRetry(params, retriesLeft - 1);
359
+ }
360
+ throw e;
361
+ }
362
+ };
363
+ const execute = async (params) => {
364
+ loading.value = true;
365
+ error.value = null;
366
+ executionCount.value++;
367
+ try {
368
+ const data = await executeWithRetry(params, options?.retryCount || 0);
369
+ result.value = data;
370
+ options?.onSuccess?.(data);
371
+ return data;
372
+ } catch (e) {
373
+ const err = e;
374
+ error.value = err;
375
+ options?.onError?.(err);
229
376
  throw e;
230
377
  } finally {
231
378
  loading.value = false;
@@ -235,14 +382,25 @@ function useAction(actionType) {
235
382
  execute,
236
383
  loading,
237
384
  result,
238
- error
385
+ error,
386
+ reset,
387
+ executionCount
239
388
  };
240
389
  }
241
390
  function useData(queryId, params, options) {
242
391
  const context = injectRuntime();
243
- const data = (0, import_vue2.ref)();
392
+ const data = (0, import_vue2.shallowRef)();
244
393
  const loading = (0, import_vue2.ref)(false);
245
394
  const error = (0, import_vue2.ref)(null);
395
+ const isFetched = (0, import_vue2.ref)(false);
396
+ let intervalTimer = null;
397
+ const getCurrentParams = () => {
398
+ if (!params) return void 0;
399
+ if (typeof params === "object" && "value" in params) {
400
+ return params.value;
401
+ }
402
+ return params;
403
+ };
246
404
  const refetch = async (newParams) => {
247
405
  const hostApi = context.value.hostApi;
248
406
  if (!hostApi) {
@@ -251,29 +409,47 @@ function useData(queryId, params, options) {
251
409
  loading.value = true;
252
410
  error.value = null;
253
411
  try {
254
- const response = await hostApi.requestData(
412
+ const result = await hostApi.requestData(
255
413
  queryId,
256
- newParams || params
414
+ newParams || getCurrentParams()
257
415
  );
258
- if (response.success) {
259
- data.value = response.data;
260
- } else {
261
- throw new Error(response.message || "Query failed");
262
- }
416
+ data.value = result;
417
+ isFetched.value = true;
418
+ options?.onSuccess?.(result);
263
419
  } catch (e) {
264
- error.value = e;
420
+ const err = e;
421
+ error.value = err;
422
+ options?.onError?.(err);
265
423
  } finally {
266
424
  loading.value = false;
267
425
  }
268
426
  };
269
- if (options?.immediate !== false) {
270
- refetch();
427
+ if (params && typeof params === "object" && "value" in params && options?.refreshOnParamsChange !== false) {
428
+ (0, import_vue2.watch)(params, () => {
429
+ if (isFetched.value) {
430
+ refetch();
431
+ }
432
+ }, { deep: true });
271
433
  }
434
+ (0, import_vue2.onMounted)(() => {
435
+ if (options?.immediate !== false && context.value.hostApi) {
436
+ refetch();
437
+ }
438
+ if (options?.refreshInterval && options.refreshInterval > 0) {
439
+ intervalTimer = setInterval(refetch, options.refreshInterval);
440
+ }
441
+ });
442
+ (0, import_vue2.onUnmounted)(() => {
443
+ if (intervalTimer) {
444
+ clearInterval(intervalTimer);
445
+ }
446
+ });
272
447
  return {
273
448
  data,
274
449
  loading,
275
450
  error,
276
- refetch
451
+ refetch,
452
+ isFetched
277
453
  };
278
454
  }
279
455
  function useRuntimeEvent(eventType, handler) {
@@ -285,6 +461,145 @@ function useRuntimeEvent(eventType, handler) {
285
461
  unsubscribe?.();
286
462
  });
287
463
  }
464
+ function usePageInfo() {
465
+ const context = injectRuntime();
466
+ return {
467
+ /** 页面 UID */
468
+ pageUid: (0, import_vue2.computed)(() => context.value.state.page?.pageUid),
469
+ /** 页面版本 ID */
470
+ pageVersionId: (0, import_vue2.computed)(() => context.value.state.page?.pageVersionId),
471
+ /** Schema 版本 */
472
+ schemaVersion: (0, import_vue2.computed)(() => context.value.state.page?.pageJson?.schemaVersion),
473
+ /** 页面标题 */
474
+ title: (0, import_vue2.computed)(() => {
475
+ const page = context.value.state.page;
476
+ return page?.title;
477
+ }),
478
+ /** 页面配置 */
479
+ config: (0, import_vue2.computed)(() => {
480
+ const page = context.value.state.page;
481
+ return page?.config;
482
+ }),
483
+ /** 页面是否已加载 */
484
+ isLoaded: (0, import_vue2.computed)(() => context.value.state.page !== null)
485
+ };
486
+ }
487
+ function useComponentState(componentId) {
488
+ const context = injectRuntime();
489
+ const componentStatus = (0, import_vue2.computed)(() => {
490
+ return context.value.state.components.get(componentId);
491
+ });
492
+ return {
493
+ /** 组件是否已加载 */
494
+ isLoaded: (0, import_vue2.computed)(() => componentStatus.value?.status === "loaded"),
495
+ /** 组件是否加载中 */
496
+ isLoading: (0, import_vue2.computed)(() => componentStatus.value?.status === "loading"),
497
+ /** 组件是否加载失败 */
498
+ hasError: (0, import_vue2.computed)(() => componentStatus.value?.status === "failed"),
499
+ /** 加载耗时 */
500
+ loadTime: (0, import_vue2.computed)(() => componentStatus.value?.loadTime),
501
+ /** 组件信息 */
502
+ info: componentStatus
503
+ };
504
+ }
505
+ function useLifecycle(options) {
506
+ const context = injectRuntime();
507
+ const hasMounted = (0, import_vue2.ref)(false);
508
+ const hasReady = (0, import_vue2.ref)(false);
509
+ (0, import_vue2.watch)(
510
+ () => context.value.state.phase,
511
+ async (newPhase, oldPhase) => {
512
+ options?.onPhaseChange?.(newPhase);
513
+ if (!hasMounted.value && newPhase !== "idle") {
514
+ hasMounted.value = true;
515
+ try {
516
+ await options?.onMounted?.();
517
+ } catch (error) {
518
+ options?.onError?.(error);
519
+ }
520
+ }
521
+ if (!hasReady.value && newPhase === "ready") {
522
+ hasReady.value = true;
523
+ try {
524
+ await options?.onReady?.();
525
+ } catch (error) {
526
+ options?.onError?.(error);
527
+ }
528
+ }
529
+ if (newPhase === "error" && oldPhase !== "error") {
530
+ options?.onError?.(context.value.state.error);
531
+ }
532
+ },
533
+ { immediate: true }
534
+ );
535
+ return {
536
+ /** 当前阶段 */
537
+ phase: (0, import_vue2.computed)(() => context.value.state.phase),
538
+ /** 是否已 mounted */
539
+ hasMounted: (0, import_vue2.readonly)(hasMounted),
540
+ /** 是否已 ready */
541
+ hasReady: (0, import_vue2.readonly)(hasReady)
542
+ };
543
+ }
544
+ function useWhen(condition, callback, options) {
545
+ const executed = (0, import_vue2.ref)(false);
546
+ const { once = true, immediate = true } = options || {};
547
+ const stop = (0, import_vue2.watch)(
548
+ condition,
549
+ async (value) => {
550
+ if (value && (!once || !executed.value)) {
551
+ executed.value = true;
552
+ await callback();
553
+ if (once) {
554
+ stop();
555
+ }
556
+ }
557
+ },
558
+ { immediate }
559
+ );
560
+ (0, import_vue2.onUnmounted)(() => {
561
+ stop();
562
+ });
563
+ return {
564
+ /** 是否已执行 */
565
+ executed: (0, import_vue2.readonly)(executed),
566
+ /** 手动停止监听 */
567
+ stop
568
+ };
569
+ }
570
+ function useDebouncedAction(actionType, delay = 300) {
571
+ const { execute: rawExecute, loading, result, error } = useAction(actionType);
572
+ let timeoutId = null;
573
+ const execute = (params) => {
574
+ if (timeoutId) {
575
+ clearTimeout(timeoutId);
576
+ }
577
+ timeoutId = setTimeout(() => {
578
+ rawExecute(params).catch(() => {
579
+ });
580
+ }, delay);
581
+ };
582
+ const cancel = () => {
583
+ if (timeoutId) {
584
+ clearTimeout(timeoutId);
585
+ timeoutId = null;
586
+ }
587
+ };
588
+ (0, import_vue2.onUnmounted)(() => {
589
+ cancel();
590
+ });
591
+ return {
592
+ execute,
593
+ loading,
594
+ result,
595
+ error,
596
+ cancel
597
+ };
598
+ }
599
+ function useGlobalConfig() {
600
+ const config = (0, import_vue2.inject)("djvlc-config", {});
601
+ return config;
602
+ }
288
603
 
289
604
  // src/components/DJVRenderer.ts
290
605
  var DJVRenderer = (0, import_vue3.defineComponent)({
@@ -317,11 +632,26 @@ var DJVRenderer = (0, import_vue3.defineComponent)({
317
632
  enableSRI: {
318
633
  type: Boolean,
319
634
  default: true
635
+ },
636
+ retryCount: {
637
+ type: Number,
638
+ default: 3
639
+ },
640
+ retryDelay: {
641
+ type: Number,
642
+ default: 1e3
643
+ },
644
+ timeout: {
645
+ type: Number,
646
+ default: 3e4
320
647
  }
321
648
  },
322
- emits: ["load", "error", "ready"],
649
+ emits: ["load", "error", "ready", "phase-change"],
323
650
  setup(props, { emit, slots }) {
324
651
  const containerRef = (0, import_vue3.ref)(null);
652
+ const mounted = (0, import_vue3.ref)(true);
653
+ const retryAttempt = (0, import_vue3.ref)(0);
654
+ const phase = (0, import_vue3.ref)("idle");
325
655
  const contextValue = (0, import_vue3.shallowRef)({
326
656
  runtime: null,
327
657
  state: {
@@ -337,8 +667,16 @@ var DJVRenderer = (0, import_vue3.defineComponent)({
337
667
  });
338
668
  provideRuntime(contextValue);
339
669
  let vueRuntime = null;
340
- const initAndLoad = async () => {
341
- if (!containerRef.value) return;
670
+ const withTimeout = (promise, ms) => {
671
+ return Promise.race([
672
+ promise,
673
+ new Promise(
674
+ (_, reject) => setTimeout(() => reject(new Error("\u52A0\u8F7D\u8D85\u65F6")), ms)
675
+ )
676
+ ]);
677
+ };
678
+ const initWithRetry = async () => {
679
+ if (!containerRef.value || !mounted.value) return;
342
680
  const options = {
343
681
  pageUid: props.pageUid,
344
682
  apiBaseUrl: props.apiBaseUrl,
@@ -357,56 +695,126 @@ var DJVRenderer = (0, import_vue3.defineComponent)({
357
695
  };
358
696
  vueRuntime = createVueRuntime(options);
359
697
  try {
360
- await vueRuntime.init();
361
- contextValue.value = {
362
- runtime: vueRuntime.runtime.value,
363
- state: vueRuntime.runtime.value?.getState() || contextValue.value.state,
364
- hostApi: vueRuntime.hostApi.value
365
- };
366
- const page = await vueRuntime.load();
367
- emit("load", page);
368
- contextValue.value = {
369
- ...contextValue.value,
370
- state: vueRuntime.runtime.value?.getState() || contextValue.value.state,
371
- hostApi: vueRuntime.hostApi.value
372
- };
373
- await vueRuntime.render();
374
- emit("ready");
375
- vueRuntime.runtime.value?.onStateChange((state) => {
376
- contextValue.value = {
377
- ...contextValue.value,
378
- state
379
- };
380
- });
698
+ await withTimeout(
699
+ (async () => {
700
+ await vueRuntime.init();
701
+ if (!mounted.value) return;
702
+ phase.value = "loading";
703
+ emit("phase-change", "loading");
704
+ contextValue.value = {
705
+ runtime: vueRuntime.runtime.value,
706
+ state: vueRuntime.runtime.value?.getState() || contextValue.value.state,
707
+ hostApi: vueRuntime.hostApi.value
708
+ };
709
+ const pageData = await vueRuntime.load();
710
+ if (!mounted.value) return;
711
+ emit("load", pageData);
712
+ contextValue.value = {
713
+ ...contextValue.value,
714
+ state: vueRuntime.runtime.value?.getState() || contextValue.value.state,
715
+ hostApi: vueRuntime.hostApi.value
716
+ };
717
+ await vueRuntime.render();
718
+ if (!mounted.value) return;
719
+ phase.value = "ready";
720
+ emit("phase-change", "ready");
721
+ emit("ready");
722
+ retryAttempt.value = 0;
723
+ vueRuntime.runtime.value?.onStateChange((state) => {
724
+ if (!mounted.value) return;
725
+ contextValue.value = {
726
+ ...contextValue.value,
727
+ state
728
+ };
729
+ phase.value = state.phase;
730
+ emit("phase-change", state.phase);
731
+ });
732
+ })(),
733
+ props.timeout
734
+ );
381
735
  } catch (error) {
382
- emit("error", error);
736
+ if (!mounted.value) return;
737
+ if (retryAttempt.value < props.retryCount) {
738
+ retryAttempt.value++;
739
+ if (props.debug) {
740
+ console.log(`[DJVRenderer] \u91CD\u8BD5 ${retryAttempt.value}/${props.retryCount}...`);
741
+ }
742
+ setTimeout(() => {
743
+ if (mounted.value) {
744
+ initWithRetry();
745
+ }
746
+ }, props.retryDelay);
747
+ } else {
748
+ phase.value = "error";
749
+ emit("phase-change", "error");
750
+ emit("error", error);
751
+ }
383
752
  }
384
753
  };
754
+ const handleRetry = () => {
755
+ retryAttempt.value = 0;
756
+ initWithRetry();
757
+ };
385
758
  (0, import_vue3.onMounted)(() => {
386
- initAndLoad();
759
+ mounted.value = true;
760
+ initWithRetry();
387
761
  });
388
762
  (0, import_vue3.onUnmounted)(() => {
763
+ mounted.value = false;
389
764
  vueRuntime?.destroy();
390
765
  });
391
766
  (0, import_vue3.watch)(
392
767
  () => props.pageUid,
393
768
  () => {
394
769
  vueRuntime?.destroy();
395
- initAndLoad();
770
+ retryAttempt.value = 0;
771
+ initWithRetry();
396
772
  }
397
773
  );
774
+ const renderDefaultLoading = () => {
775
+ return (0, import_vue3.h)("div", { class: "djvlc-loading" }, [
776
+ (0, import_vue3.h)("div", { class: "djvlc-loading-spinner" }),
777
+ (0, import_vue3.h)("span", {}, "\u52A0\u8F7D\u4E2D..."),
778
+ retryAttempt.value > 0 && (0, import_vue3.h)("span", { class: "djvlc-loading-retry" }, `\u91CD\u8BD5 ${retryAttempt.value}/${props.retryCount}`)
779
+ ]);
780
+ };
781
+ const renderDefaultError = (error) => {
782
+ return (0, import_vue3.h)("div", { class: "djvlc-error" }, [
783
+ (0, import_vue3.h)("div", { class: "djvlc-error-icon" }, "\u26A0\uFE0F"),
784
+ (0, import_vue3.h)("span", { class: "djvlc-error-message" }, `\u52A0\u8F7D\u5931\u8D25\uFF1A${error.message}`),
785
+ (0, import_vue3.h)(
786
+ "button",
787
+ {
788
+ class: "djvlc-error-retry-btn",
789
+ onClick: handleRetry,
790
+ type: "button"
791
+ },
792
+ "\u91CD\u8BD5"
793
+ )
794
+ ]);
795
+ };
796
+ const renderDefaultEmpty = () => {
797
+ return (0, import_vue3.h)("div", { class: "djvlc-empty" }, [(0, import_vue3.h)("span", {}, "\u6682\u65E0\u5185\u5BB9")]);
798
+ };
398
799
  return () => {
800
+ const isLoading = vueRuntime?.loading.value ?? false;
801
+ const currentError = vueRuntime?.error.value;
802
+ const currentPage = vueRuntime?.page.value;
399
803
  return (0, import_vue3.h)(
400
804
  "div",
401
805
  {
402
806
  ref: containerRef,
403
- class: "djvlc-renderer"
807
+ class: "djvlc-renderer",
808
+ "data-phase": phase.value,
809
+ "data-page-uid": props.pageUid
404
810
  },
405
811
  [
406
812
  // 加载中插槽
407
- vueRuntime?.loading.value && slots.loading?.(),
813
+ isLoading && (slots.loading?.() || renderDefaultLoading()),
408
814
  // 错误插槽
409
- vueRuntime?.error.value && slots.error?.({ error: vueRuntime.error.value }),
815
+ currentError && (slots.error?.({ error: currentError, retry: handleRetry }) || renderDefaultError(currentError)),
816
+ // 空状态
817
+ !isLoading && !currentError && !currentPage && phase.value === "ready" && (slots.empty?.() || renderDefaultEmpty()),
410
818
  // 默认插槽(额外内容)
411
819
  slots.default?.()
412
820
  ]
@@ -417,6 +825,15 @@ var DJVRenderer = (0, import_vue3.defineComponent)({
417
825
 
418
826
  // src/components/DJVProvider.ts
419
827
  var import_vue4 = require("vue");
828
+ var defaultState = {
829
+ phase: "idle",
830
+ page: null,
831
+ variables: {},
832
+ queries: {},
833
+ components: /* @__PURE__ */ new Map(),
834
+ error: null,
835
+ destroyed: false
836
+ };
420
837
  var DJVProvider = (0, import_vue4.defineComponent)({
421
838
  name: "DJVProvider",
422
839
  props: {
@@ -427,50 +844,220 @@ var DJVProvider = (0, import_vue4.defineComponent)({
427
844
  hostApi: {
428
845
  type: Object,
429
846
  default: null
847
+ },
848
+ class: {
849
+ type: String,
850
+ default: ""
851
+ },
852
+ debug: {
853
+ type: Boolean,
854
+ default: false
430
855
  }
431
856
  },
432
- setup(props, { slots }) {
857
+ emits: ["state-change", "phase-change", "error"],
858
+ setup(props, { slots, emit }) {
859
+ let unsubscribe = null;
433
860
  const contextValue = (0, import_vue4.shallowRef)({
434
861
  runtime: props.runtime,
435
- state: props.runtime?.getState() || {
436
- phase: "idle",
437
- page: null,
438
- variables: {},
439
- queries: {},
440
- components: /* @__PURE__ */ new Map(),
441
- error: null,
442
- destroyed: false
443
- },
862
+ state: props.runtime?.getState() || defaultState,
444
863
  hostApi: props.hostApi
445
864
  });
446
865
  provideRuntime(contextValue);
447
- if (props.runtime) {
448
- props.runtime.onStateChange((state) => {
866
+ const subscribeToRuntime = (runtime) => {
867
+ if (unsubscribe) {
868
+ unsubscribe();
869
+ unsubscribe = null;
870
+ }
871
+ if (!runtime) return;
872
+ unsubscribe = runtime.onStateChange((state) => {
873
+ const prevPhase = contextValue.value.state.phase;
449
874
  contextValue.value = {
450
875
  ...contextValue.value,
451
876
  state
452
877
  };
878
+ emit("state-change", state);
879
+ if (state.phase !== prevPhase) {
880
+ emit("phase-change", state.phase);
881
+ if (props.debug) {
882
+ console.log(`[DJVProvider] Phase changed: ${prevPhase} -> ${state.phase}`);
883
+ }
884
+ }
885
+ if (state.error) {
886
+ emit("error", state.error);
887
+ }
453
888
  });
454
- }
455
- return () => (0, import_vue4.h)("div", { class: "djvlc-provider" }, slots.default?.());
889
+ };
890
+ subscribeToRuntime(props.runtime);
891
+ (0, import_vue4.watch)(
892
+ () => props.runtime,
893
+ (newRuntime, oldRuntime) => {
894
+ if (newRuntime !== oldRuntime) {
895
+ contextValue.value = {
896
+ runtime: newRuntime,
897
+ state: newRuntime?.getState() || defaultState,
898
+ hostApi: props.hostApi
899
+ };
900
+ subscribeToRuntime(newRuntime);
901
+ }
902
+ }
903
+ );
904
+ (0, import_vue4.watch)(
905
+ () => props.hostApi,
906
+ (newHostApi) => {
907
+ contextValue.value = {
908
+ ...contextValue.value,
909
+ hostApi: newHostApi
910
+ };
911
+ }
912
+ );
913
+ (0, import_vue4.onUnmounted)(() => {
914
+ if (unsubscribe) {
915
+ unsubscribe();
916
+ unsubscribe = null;
917
+ }
918
+ });
919
+ return () => (0, import_vue4.h)(
920
+ "div",
921
+ {
922
+ class: ["djvlc-provider", props.class].filter(Boolean).join(" "),
923
+ "data-phase": contextValue.value.state.phase
924
+ },
925
+ slots.default?.()
926
+ );
456
927
  }
457
928
  });
458
929
 
459
930
  // src/plugin.ts
931
+ var DJVLC_CONFIG_KEY = "djvlc-config";
932
+ function createTrackDirective() {
933
+ const observedElements = /* @__PURE__ */ new WeakSet();
934
+ return {
935
+ mounted(el, binding) {
936
+ const { event, data = {}, trigger = "click" } = binding.value;
937
+ if (trigger === "click") {
938
+ el.addEventListener("click", () => {
939
+ dispatchTrackEvent(event, { ...data, element: el.tagName });
940
+ });
941
+ } else if (trigger === "view") {
942
+ if (!observedElements.has(el)) {
943
+ observedElements.add(el);
944
+ const observer = new IntersectionObserver(
945
+ (entries) => {
946
+ entries.forEach((entry) => {
947
+ if (entry.isIntersecting) {
948
+ dispatchTrackEvent(event, { ...data, element: el.tagName });
949
+ observer.unobserve(el);
950
+ }
951
+ });
952
+ },
953
+ { threshold: 0.5 }
954
+ );
955
+ observer.observe(el);
956
+ }
957
+ } else if (trigger === "mounted") {
958
+ dispatchTrackEvent(event, { ...data, element: el.tagName });
959
+ }
960
+ }
961
+ };
962
+ }
963
+ function dispatchTrackEvent(event, data) {
964
+ window.dispatchEvent(
965
+ new CustomEvent("djvlc:track", {
966
+ detail: { event, data, timestamp: Date.now() }
967
+ })
968
+ );
969
+ }
970
+ function createVisibleDirective() {
971
+ return {
972
+ mounted(el, binding) {
973
+ updateVisibility(el, binding.value);
974
+ },
975
+ updated(el, binding) {
976
+ updateVisibility(el, binding.value);
977
+ }
978
+ };
979
+ }
980
+ function updateVisibility(el, visible) {
981
+ el.style.display = visible ? "" : "none";
982
+ }
983
+ function createLoadingDirective() {
984
+ const loadingOverlays = /* @__PURE__ */ new WeakMap();
985
+ return {
986
+ mounted(el, binding) {
987
+ el.style.position = "relative";
988
+ updateLoading(el, binding.value, loadingOverlays);
989
+ },
990
+ updated(el, binding) {
991
+ updateLoading(el, binding.value, loadingOverlays);
992
+ },
993
+ unmounted(el) {
994
+ const overlay = loadingOverlays.get(el);
995
+ if (overlay) {
996
+ overlay.remove();
997
+ loadingOverlays.delete(el);
998
+ }
999
+ }
1000
+ };
1001
+ }
1002
+ function updateLoading(el, loading, overlays) {
1003
+ let overlay = overlays.get(el);
1004
+ if (loading) {
1005
+ if (!overlay) {
1006
+ overlay = document.createElement("div");
1007
+ overlay.className = "djvlc-loading-overlay";
1008
+ overlay.innerHTML = `
1009
+ <div class="djvlc-loading-spinner"></div>
1010
+ `;
1011
+ overlay.style.cssText = `
1012
+ position: absolute;
1013
+ inset: 0;
1014
+ display: flex;
1015
+ align-items: center;
1016
+ justify-content: center;
1017
+ background: rgba(255, 255, 255, 0.8);
1018
+ z-index: 100;
1019
+ `;
1020
+ el.appendChild(overlay);
1021
+ overlays.set(el, overlay);
1022
+ }
1023
+ overlay.style.display = "flex";
1024
+ } else if (overlay) {
1025
+ overlay.style.display = "none";
1026
+ }
1027
+ }
460
1028
  var DJVPlugin = {
461
1029
  install(app, options = {}) {
1030
+ const prefix = options.componentPrefix || "";
462
1031
  if (options.registerComponents !== false) {
463
- app.component("DJVRenderer", DJVRenderer);
464
- app.component("DJVProvider", DJVProvider);
1032
+ app.component(`${prefix}DJVRenderer`, DJVRenderer);
1033
+ app.component(`${prefix}DJVProvider`, DJVProvider);
465
1034
  }
466
- app.provide("djvlc-config", {
467
- apiBaseUrl: options.defaultApiBaseUrl,
468
- cdnBaseUrl: options.defaultCdnBaseUrl
469
- });
1035
+ if (options.registerDirectives !== false) {
1036
+ app.directive("djv-track", createTrackDirective());
1037
+ app.directive("djv-visible", createVisibleDirective());
1038
+ app.directive("djv-loading", createLoadingDirective());
1039
+ }
1040
+ const globalConfig = {
1041
+ apiBaseUrl: options.apiBaseUrl,
1042
+ cdnBaseUrl: options.cdnBaseUrl,
1043
+ channel: options.channel,
1044
+ debug: options.debug,
1045
+ enableSRI: options.enableSRI,
1046
+ enableMetrics: options.enableMetrics,
1047
+ retryCount: options.retryCount,
1048
+ retryDelay: options.retryDelay,
1049
+ timeout: options.timeout
1050
+ };
1051
+ app.provide(DJVLC_CONFIG_KEY, globalConfig);
1052
+ app.config.globalProperties.$djvlc = {
1053
+ config: globalConfig,
1054
+ track: dispatchTrackEvent
1055
+ };
470
1056
  }
471
1057
  };
472
1058
  // Annotate the CommonJS export names for ESM import in node:
473
1059
  0 && (module.exports = {
1060
+ DJVLC_CONFIG_KEY,
474
1061
  DJVPlugin,
475
1062
  DJVProvider,
476
1063
  DJVRenderer,
@@ -479,11 +1066,17 @@ var DJVPlugin = {
479
1066
  injectRuntime,
480
1067
  provideRuntime,
481
1068
  useAction,
1069
+ useComponentState,
482
1070
  useDJVRuntime,
483
1071
  useData,
1072
+ useDebouncedAction,
1073
+ useGlobalConfig,
484
1074
  useHostApi,
1075
+ useLifecycle,
1076
+ usePageInfo,
485
1077
  useQuery,
486
1078
  useRuntimeEvent,
487
1079
  useRuntimeState,
488
- useRuntimeStateWritable
1080
+ useRuntimeStateWritable,
1081
+ useWhen
489
1082
  });