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