@zeewain/3d-avatar-sdk 1.2.4 → 1.2.5

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/README.md CHANGED
@@ -750,7 +750,7 @@ updateBroadcastCallbacks(callbacks: IBroadcastCallbacks): void
750
750
  ```typescript
751
751
  interface IBroadcastCallbacks {
752
752
  onStart?: () => void; // 播报开始
753
- onFinish?: () => void; // 播报完成
753
+ onFinish?: () => void; // 播报完成(每调用一次`startBroadcast`,在播报结束后都会触发一次回调)
754
754
  onError?: (error: Error) => void; // 播报错误
755
755
  onPause?: () => void; // 播报暂停
756
756
  onResume?: () => void; // 播报恢复
@@ -775,8 +775,6 @@ getBroadcastStatus(): {
775
775
  isActive: boolean;
776
776
  isGeneratingAudio: boolean;
777
777
  hasReceivedAudio: boolean;
778
- pendingCallbacks: number;
779
- hasController: boolean;
780
778
  queueInfo?: {
781
779
  totalTasks: number; // 队列中总任务数
782
780
  requestingTasks: number; // 正在请求中的任务数
@@ -788,14 +786,12 @@ getBroadcastStatus(): {
788
786
  }
789
787
  ```
790
788
 
791
- 获取播报状态信息,包括队列状态监控。
789
+ 获取播报状态信息,包括队列状态监控。`当播报队列中最后一条音频播放完毕后,队列状态信息将被重置。`
792
790
 
793
791
  **返回值说明:**
794
- - `isActive`: 播报服务是否活跃(有任务在队列中或正在生成音频)
792
+ - `isActive`: 播报服务是否活跃(是否正在播报音频或正在生成音频)
795
793
  - `isGeneratingAudio`: 是否正在生成音频
796
- - `hasReceivedAudio`: 是否已收到音频
797
- - `pendingCallbacks`: 待处理的回调数量
798
- - `hasController`: 是否有活跃的请求控制器
794
+ - `hasReceivedAudio`: 是否已收到至少一条音频数据
799
795
  - `queueInfo`: 队列详细信息
800
796
  - `totalTasks`: 队列中的总任务数
801
797
  - `requestingTasks`: 正在发起SSE请求的任务数
@@ -16,7 +16,7 @@
16
16
  "lint:all-fix": "npm run lint:fix && npm run lint:css-fix"
17
17
  },
18
18
  "dependencies": {
19
- "@zeewain/3d-avatar-sdk": "^1.2.4",
19
+ "@zeewain/3d-avatar-sdk": "^1.2.5",
20
20
  "core-js": "^3.8.3",
21
21
  "element-ui": "^2.15.13",
22
22
  "vue": "^2.6.14"
@@ -24,7 +24,7 @@
24
24
  "@element-plus/icons-vue": "^2.3.1",
25
25
  "@vueuse/core": "^13.5.0",
26
26
  "@vueuse/integrations": "^13.5.0",
27
- "@zeewain/3d-avatar-sdk": "^1.2.4",
27
+ "@zeewain/3d-avatar-sdk": "^1.2.5",
28
28
  "dayjs": "^1.11.13",
29
29
  "element-plus": "^2.10.4",
30
30
  "vite-plugin-html": "^3.2.2",
@@ -15,8 +15,8 @@ dependencies:
15
15
  specifier: ^13.5.0
16
16
  version: 13.5.0(vue@3.5.17)
17
17
  '@zeewain/3d-avatar-sdk':
18
- specifier: ^1.2.3
19
- version: 1.2.3
18
+ specifier: ^1.2.4
19
+ version: 1.2.4
20
20
  dayjs:
21
21
  specifier: ^1.11.13
22
22
  version: 1.11.13
@@ -1124,11 +1124,11 @@ packages:
1124
1124
  - vue
1125
1125
  dev: false
1126
1126
 
1127
- /@zeewain/3d-avatar-sdk@1.2.3:
1128
- resolution: {integrity: sha512-jI/ZaoIg+vMDYluxpzTSdG4Mthcv4WhGYQsjL5Kx39qyxf0wmIUVKf7d48tmoDuVr5PtdcCa7w6uLG3I6bjVQQ==}
1127
+ /@zeewain/3d-avatar-sdk@1.2.4:
1128
+ resolution: {integrity: sha512-a+y9HpUjbZgObzR+RfnfjxamdAa6Nk9CWkdHMejPIsYfSogCM6UtN/Jbh1A08RzIH9xrJnsUpTm1nXvvIsT6Vw==}
1129
1129
  dependencies:
1130
1130
  '@microsoft/fetch-event-source': 2.0.1
1131
- core-js: 3.44.0
1131
+ core-js: 3.45.1
1132
1132
  dev: false
1133
1133
 
1134
1134
  /acorn-jsx@5.3.2(acorn@8.15.0):
@@ -1493,8 +1493,8 @@ packages:
1493
1493
  resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==}
1494
1494
  dev: false
1495
1495
 
1496
- /core-js@3.44.0:
1497
- resolution: {integrity: sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw==}
1496
+ /core-js@3.45.1:
1497
+ resolution: {integrity: sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==}
1498
1498
  requiresBuild: true
1499
1499
  dev: false
1500
1500
 
@@ -47,6 +47,7 @@
47
47
 
48
48
  <el-tab-pane label="智能播报" name="text-broadcast">
49
49
  <BroadcastAPI
50
+ ref="broadcastAPI"
50
51
  :sdk-status="sdkStatus"
51
52
  :global-config="globalConfig"
52
53
  @text-broadcast="handleTextBroadcast"
@@ -54,6 +55,7 @@
54
55
  @pause-broadcast="handlePauseBroadcast"
55
56
  @resume-broadcast="handleResumeBroadcast"
56
57
  @stop-broadcast="handleStopBroadcast"
58
+ @get-broadcast-status="handleGetBroadcastStatus"
57
59
  />
58
60
  </el-tab-pane>
59
61
 
@@ -135,6 +137,7 @@
135
137
 
136
138
  <el-tab-pane label="智能播报" name="text-broadcast">
137
139
  <BroadcastAPI
140
+ ref="broadcastAPI"
138
141
  :sdk-status="sdkStatus"
139
142
  :global-config="globalConfig"
140
143
  @text-broadcast="handleTextBroadcast"
@@ -142,6 +145,7 @@
142
145
  @pause-broadcast="handlePauseBroadcast"
143
146
  @resume-broadcast="handleResumeBroadcast"
144
147
  @stop-broadcast="handleStopBroadcast"
148
+ @get-broadcast-status="handleGetBroadcastStatus"
145
149
  />
146
150
  </el-tab-pane>
147
151
 
@@ -229,6 +233,7 @@ import InfoCards from './components/InfoCards.vue';
229
233
  import UnityPreview from './components/UnityPreview.vue';
230
234
 
231
235
  import type { IGlobalConfig } from './types';
236
+ import { loadFromCache, saveToCache } from './utils';
232
237
 
233
238
  interface ISDKStatus {
234
239
  unityLoaded: boolean;
@@ -238,6 +243,7 @@ interface ISDKStatus {
238
243
  canGetMotion: boolean;
239
244
  canControlBroadcast: boolean;
240
245
  canControlCamera: boolean;
246
+ isStartBroadcast: boolean;
241
247
  }
242
248
 
243
249
  interface ILogEntry {
@@ -267,7 +273,8 @@ const sdkStatus = reactive<ISDKStatus>({
267
273
  canBroadcast: false,
268
274
  canGetMotion: false,
269
275
  canControlBroadcast: false,
270
- canControlCamera: false
276
+ canControlCamera: false,
277
+ isStartBroadcast: false,
271
278
  });
272
279
 
273
280
  // 操作日志
@@ -278,6 +285,7 @@ const logs = reactive<ILogEntry[]>([
278
285
 
279
286
  // SDK实例
280
287
  const sdk = ref<ZEEAvatarSDK | null>(null);
288
+ const broadcastAPI = ref<InstanceType<typeof BroadcastAPI> | null>(null);
281
289
 
282
290
  // 初始化SDK配置
283
291
  function initializeSDKConfig(): ZEEAvatarSDK | null {
@@ -316,12 +324,15 @@ function initializeSDKConfig(): ZEEAvatarSDK | null {
316
324
  // 播报回调
317
325
  broadcastCallbacks: {
318
326
  onStart: () => {
327
+ sdkStatus.isStartBroadcast = true;
319
328
  addLog('播报开始', 'info');
320
329
  },
321
330
  onFinish: () => {
331
+ sdkStatus.isStartBroadcast = false;
322
332
  addLog('播报完成', 'success');
323
333
  },
324
334
  onError: (error:any) => {
335
+ sdkStatus.isStartBroadcast = false;
325
336
  addLog(`播报失败: ${error.message}`, 'error');
326
337
  },
327
338
  onPause: () => {
@@ -331,7 +342,8 @@ function initializeSDKConfig(): ZEEAvatarSDK | null {
331
342
  addLog('播报恢复', 'success');
332
343
  },
333
344
  onStop: () => {
334
- addLog('播报停止', 'error');
345
+ sdkStatus.isStartBroadcast = false;
346
+ addLog('播报停止', 'warning');
335
347
  },
336
348
  }
337
349
  };
@@ -342,9 +354,8 @@ function initializeSDKConfig(): ZEEAvatarSDK | null {
342
354
 
343
355
  // 从本地存储加载配置
344
356
  function loadStoredConfig(): void {
345
- const config = localStorage.getItem('zee_config');
346
- if (config) {
347
- const configObj = JSON.parse(config);
357
+ const configObj = loadFromCache('global-config');
358
+ if (configObj) {
348
359
  Object.assign(globalConfig, configObj);
349
360
  }
350
361
  }
@@ -352,7 +363,7 @@ function loadStoredConfig(): void {
352
363
  // 保存全局配置
353
364
  function handleSaveConfig(): void {
354
365
 
355
- localStorage.setItem('zee_config', JSON.stringify(globalConfig));
366
+ saveToCache('global-config', globalConfig);
356
367
 
357
368
  addLog('全局配置已保存', 'success');
358
369
  ElMessage.success('配置保存成功');
@@ -452,7 +463,6 @@ async function handleTextBroadcast(params: {
452
463
  voiceCode?: string;
453
464
  speed?: number;
454
465
  broadcastMotionString?: string;
455
- isAppend?: boolean;
456
466
  }): Promise<void> {
457
467
  if (!sdk.value) return;
458
468
 
@@ -471,7 +481,8 @@ async function handleTextBroadcast(params: {
471
481
  motionPlayMode: 'random'
472
482
  };
473
483
 
474
- await sdk.value.startBroadcast(broadcastParams, params.isAppend);
484
+ console.warn('handleTextBroadcast: ', broadcastParams, sdkStatus.isStartBroadcast);
485
+ await sdk.value.startBroadcast(broadcastParams, sdkStatus.isStartBroadcast);
475
486
  addLog('文本播报已开始', 'success');
476
487
  // ElMessage.success('文本播报已开始');
477
488
  } catch (error) {
@@ -508,7 +519,7 @@ async function handleAudioBroadcast(params: {
508
519
  motionPlayMode: 'random'
509
520
  };
510
521
 
511
- await sdk.value.startBroadcast(broadcastParams, params.isAppend);
522
+ await sdk.value.startBroadcast(broadcastParams, sdkStatus.isStartBroadcast);
512
523
  addLog('音频播报已开始', 'success');
513
524
  ElMessage.success('音频播报已开始');
514
525
  } catch (error) {
@@ -585,6 +596,14 @@ async function handleStopBroadcast(): Promise<void> {
585
596
  }
586
597
  }
587
598
 
599
+ function handleGetBroadcastStatus() {
600
+ if (!sdk.value) return;
601
+ const status = sdk.value.getBroadcastStatus();
602
+ addLog(`播报状态: ${JSON.stringify(status)}`, 'success');
603
+ console.warn('AAAAAAAAA播报状态: ', status);
604
+ ElMessage.success(`播报状态: ${JSON.stringify(status)}`);
605
+ }
606
+
588
607
  // 设置预设镜头
589
608
  async function handleSetCameraPreset(params: { preset: string }): Promise<void> {
590
609
  if (!sdk.value) return;
@@ -645,6 +664,7 @@ async function handleUnloadAvatar(): Promise<void> {
645
664
  sdkStatus.canGetMotion = false;
646
665
  sdkStatus.canControlBroadcast = false;
647
666
  sdkStatus.canControlCamera = false;
667
+ sdkStatus.isStartBroadcast = false; // 重置播报状态
648
668
  loadingProgress.value = 0;
649
669
 
650
670
  ElMessage.success('Avatar已卸载');
@@ -668,9 +688,6 @@ function handleDestroySDK(){
668
688
  sdk.value = null;
669
689
  }
670
690
 
671
- addLog('SDK 实例已销毁', 'warning');
672
- ElMessage.warning('SDK 实例已销毁');
673
-
674
691
  // 重置状态
675
692
  sdkStatus.unityLoaded = false;
676
693
  sdkStatus.avatarLoaded = false;
@@ -679,8 +696,13 @@ function handleDestroySDK(){
679
696
  sdkStatus.canGetMotion = false;
680
697
  sdkStatus.canControlBroadcast = false;
681
698
  sdkStatus.canControlCamera = false;
699
+ sdkStatus.isStartBroadcast = false; // 重置播报状态
682
700
  loadingProgress.value = 0;
683
701
 
702
+
703
+ addLog('SDK 实例已销毁', 'warning');
704
+ ElMessage.warning('SDK 实例已销毁');
705
+
684
706
  } catch (error) {
685
707
  const message = error instanceof Error ? error.message : String(error);
686
708
  addLog(`销毁失败: ${message}`, 'error');
@@ -74,11 +74,6 @@
74
74
  prefix-icon="microphone"
75
75
  :disabled="!sdkStatus.canBroadcast"
76
76
  />
77
- <div class="input-help">
78
- <span class="help-text">
79
- 常用音色:woman008、man001、child001等
80
- </span>
81
- </div>
82
77
  </el-form-item>
83
78
 
84
79
  <el-form-item label="音量" required>
@@ -118,14 +113,19 @@
118
113
  <div class="action-buttons-container">
119
114
  <el-button
120
115
  type="primary"
121
- icon="chat-dot-round"
122
116
  :loading="isTextLoading"
123
117
  :disabled="!sdkStatus.canBroadcast || !textFormData.text || !textFormData.voiceCode || !globalConfig.avatarCode"
124
118
  @click="handleTextBroadcast"
125
119
  >
126
120
  {{ isTextLoading ? '播报中...' : '执行文本播报' }}
127
121
  </el-button>
128
- <el-button @click="handleResetBroadcast">重置</el-button>
122
+ <el-button
123
+ type="info"
124
+ size="default"
125
+ style="width: 170px;"
126
+ @click="handleGetBroadcastStatus">
127
+ 获取播报状态
128
+ </el-button>
129
129
  </div>
130
130
  </el-form-item>
131
131
  </el-form>
@@ -194,7 +194,13 @@
194
194
  >
195
195
  {{ isAudioLoading ? '播报中...' : '执行音频播报' }}
196
196
  </el-button>
197
- <el-button @click="handleResetBroadcast">重置</el-button>
197
+ <el-button
198
+ type="info"
199
+ size="default"
200
+ style="width: 170px;"
201
+ @click="handleGetBroadcastStatus">
202
+ 获取播报状态
203
+ </el-button>
198
204
  </div>
199
205
  </el-form-item>
200
206
  </el-form>
@@ -241,6 +247,7 @@
241
247
  >
242
248
  {{ stopLoading ? '停止中...' : '停止' }}
243
249
  </el-button>
250
+
244
251
  </div>
245
252
  </div>
246
253
 
@@ -288,9 +295,10 @@
288
295
  </template>
289
296
 
290
297
  <script setup lang="ts">
291
- import { ref, reactive } from 'vue';
298
+ import { ref, reactive, watch, onMounted } from 'vue';
292
299
  import { ElMessage } from 'element-plus';
293
- import { VideoPause, VideoPlay, Close } from '@element-plus/icons-vue';
300
+ import { VideoPause, VideoPlay, Close, Refresh } from '@element-plus/icons-vue';
301
+ import { loadFromCache, saveToCache } from '@/utils';
294
302
 
295
303
  interface ISDKStatus {
296
304
  canBroadcast: boolean;
@@ -310,31 +318,76 @@ const emit = defineEmits<{
310
318
  (e: 'pause-broadcast'): void;
311
319
  (e: 'resume-broadcast'): void;
312
320
  (e: 'stop-broadcast'): void;
321
+ (e: 'get-broadcast-status'): void;
313
322
  }>();
314
323
 
315
- const broadcastMode = ref<'text' | 'audio'>('text');
316
- const textFormData = reactive({
324
+ // 本地缓存键名常量
325
+ const BROADCAST_MODE_CACHE_KEY = 'broadcast-mode';
326
+ const TEXT_FORM_CACHE_KEY = 'broadcast-text-form-data';
327
+ const AUDIO_FORM_CACHE_KEY = 'broadcast-audio-form-data';
328
+
329
+
330
+ // 初始化表单数据,优先从缓存加载
331
+ const broadcastMode = ref<'text' | 'audio'>(loadFromCache(BROADCAST_MODE_CACHE_KEY, 'text')!);
332
+
333
+ const textFormData = reactive(loadFromCache(TEXT_FORM_CACHE_KEY, {
317
334
  text: '欢迎使用ZEEAvatarSDK智能体播报功能。这是一个强大的数字人解决方案,支持文本和音频两种播报模式,让您的应用更具互动性!',
318
335
  volume: 1.0,
319
336
  speed: 1.0,
320
- voiceCode: 'VOICE_INT_W_000002',
337
+ voiceCode: 'VOICE_EXT_W_000011',
321
338
  broadcastMotionString: 'DH_ACTION_MODEL103_000003,DH_ACTION_MODEL103_000005',
322
- });
323
- const audioFormData = reactive({
339
+ })!);
340
+
341
+ const audioFormData = reactive(loadFromCache(AUDIO_FORM_CACHE_KEY, {
324
342
  audioText: '欢迎使用ZEEAvatarSDK智能体播报功能',
325
343
  audioUrl: 'https://example.com/audio/sample.mp3',
326
344
  volume: 1.0,
327
345
  broadcastMotionString: 'DH_ACTION_MODEL103_000023,DH_ACTION_MODEL103_000010',
328
- });
346
+ })!);
347
+
329
348
  const result = ref(
330
- '等待播报操作...\n\n新版SDK特性:\n- 支持流式播报,实时响应\n- 内置事件回调机制\n- token统一管理,无需重复传递\n- 支持音量和语速控制\n- 实时播报控制(暂停/恢复/停止)'
349
+ '等待播报操作...\n\n新版SDK特性:\n- 支持流式播报,实时响应\n- 内置事件回调机制\n- token统一管理,无需重复传递\n- 支持音量和语速控制\n- 实时播报控制(暂停/恢复/停止)\n\n💾 表单数据已启用自动缓存功能'
331
350
  );
332
351
  const isTextLoading = ref(false);
333
352
  const isAudioLoading = ref(false);
334
353
  const pauseLoading = ref(false);
335
354
  const resumeLoading = ref(false);
336
355
  const stopLoading = ref(false);
337
- const isStartBroadcast = ref(false);
356
+
357
+ // 监听表单数据变化,自动保存到本地缓存
358
+ watch(
359
+ broadcastMode,
360
+ (newMode) => {
361
+ saveToCache(BROADCAST_MODE_CACHE_KEY, newMode);
362
+ }
363
+ );
364
+
365
+ watch(
366
+ textFormData,
367
+ (newData) => {
368
+ saveToCache(TEXT_FORM_CACHE_KEY, newData);
369
+ },
370
+ { deep: true }
371
+ );
372
+
373
+ watch(
374
+ audioFormData,
375
+ (newData) => {
376
+ saveToCache(AUDIO_FORM_CACHE_KEY, newData);
377
+ },
378
+ { deep: true }
379
+ );
380
+
381
+ // 组件挂载时的初始化操作
382
+ onMounted(() => {
383
+ console.log('BroadcastAPI: 已从本地缓存加载表单数据', {
384
+ broadcastMode: broadcastMode.value,
385
+ textFormData: textFormData,
386
+ audioFormData: audioFormData
387
+ });
388
+ });
389
+
390
+
338
391
 
339
392
  async function handleTextBroadcast() {
340
393
  const { text, volume, speed, voiceCode, broadcastMotionString } = textFormData;
@@ -352,8 +405,7 @@ async function handleTextBroadcast() {
352
405
  isTextLoading.value = true;
353
406
  result.value = `🔄 正在执行文本播报...\n📝 播报文本: ${text.substring(0, 30)}${text.length > 30 ? '...' : ''}\n🎤 音色编码: ${voiceCode}\n🔊 音量: ${volume}\n⚡ 语速: ${speed}\n\n📡 流式播报中,请等待服务器响应...`;
354
407
  try {
355
- emit('text-broadcast', { avatarCode, text, volume, speed, voiceCode, broadcastMotionString, isAppend: isStartBroadcast.value });
356
- isStartBroadcast.value = true;
408
+ emit('text-broadcast', { avatarCode, text, volume, speed, voiceCode, broadcastMotionString });
357
409
  setTimeout(() => {
358
410
  isTextLoading.value = false;
359
411
  result.value = '✅ 文本播报请求已发送\n📋 请查看右侧日志面板获取详细信息\n\n📝 操作说明:\n- 播报将以流式方式进行\n- 可使用下方控制按钮暂停/继续/停止\n- 播报过程中会触发相应的事件回调';
@@ -381,8 +433,7 @@ async function handleAudioBroadcast() {
381
433
  isAudioLoading.value = true;
382
434
  result.value = `🔄 正在执行音频播报...\n🎵 音频地址: ${audioUrl}\n📝 字幕文本: ${audioText || '无'}\n🔊 音量: ${volume}\n\n📡 正在验证音频文件并加载...`;
383
435
  try {
384
- emit('audio-broadcast', { avatarCode, audioText, audioUrl, volume, broadcastMotionString, isAppend: isStartBroadcast.value });
385
- isStartBroadcast.value = true;
436
+ emit('audio-broadcast', { avatarCode, audioText, audioUrl, volume, broadcastMotionString });
386
437
  setTimeout(() => {
387
438
  isAudioLoading.value = false;
388
439
  result.value = '✅ 音频播报请求已发送\n📋 请查看右侧日志面板获取详细信息\n\n📝 操作说明:\n- 音频将直接驱动数字人口型\n- 支持MP3、WAV等格式\n- 可使用下方控制按钮进行控制';
@@ -425,7 +476,6 @@ function handleResumeBroadcast() {
425
476
  }
426
477
 
427
478
  function handleStopBroadcast() {
428
- handleResetBroadcast();
429
479
  stopLoading.value = true;
430
480
  result.value = '🔄 正在停止播报...\n📡 发送停止指令到Unity引擎...\n🧹 清理播报资源和队列...';
431
481
  try {
@@ -440,13 +490,15 @@ function handleStopBroadcast() {
440
490
  }
441
491
  }
442
492
 
443
- function handleResetBroadcast() {
444
- isStartBroadcast.value = false;
493
+ function handleGetBroadcastStatus() {
494
+ emit('get-broadcast-status');
445
495
  }
446
496
 
497
+
447
498
  function clearResult() {
448
- result.value = '等待播报操作...\n\n新版SDK特性:\n- 支持流式播报,实时响应\n- 内置事件回调机制\n- token统一管理,无需重复传递\n- 支持音量和语速控制\n- 实时播报控制(暂停/恢复/停止)';
499
+ result.value = '等待播报操作...\n\n新版SDK特性:\n- 支持流式播报,实时响应\n- 内置事件回调机制\n- token统一管理,无需重复传递\n- 支持音量和语速控制\n- 实时播报控制(暂停/恢复/停止)\n\n💾 表单数据已启用自动缓存功能';
449
500
  }
501
+
450
502
  </script>
451
503
 
452
504
  <style lang="scss" scoped>
@@ -84,6 +84,7 @@
84
84
  <el-button
85
85
  type="primary"
86
86
  size="default"
87
+ style="height: 40px;"
87
88
  @click="saveConfig"
88
89
  :disabled="!modelValue.token || !modelValue.avatarCode"
89
90
  >
@@ -73,9 +73,9 @@ watch(
73
73
  () => props.logs,
74
74
  async () => {
75
75
  await nextTick();
76
- if (logContainer.value) {
77
- logContainer.value.scrollTop = logContainer.value.scrollHeight;
78
- }
76
+ scrollToBottom();
77
+ },{
78
+ deep: true,
79
79
  }
80
80
  );
81
81
 
@@ -133,14 +133,14 @@ function getTypeLabel(type: ILogEntry['type']) {
133
133
  <style lang="scss" scoped>
134
134
  .log-panel {
135
135
  height: 100%;
136
- max-height: 650px;
136
+ max-height: calc(100vh - 80px);
137
137
  display: flex;
138
138
  flex-direction: column;
139
139
  .log-container {
140
140
  height: 100%;
141
141
  display: flex;
142
142
  flex-direction: column;
143
- overflow: auto;
143
+ overflow: hidden;
144
144
  .log-list {
145
145
  flex: 1;
146
146
  overflow-y: auto;
@@ -160,8 +160,9 @@
160
160
  </template>
161
161
 
162
162
  <script setup lang="ts">
163
- import { ref, reactive } from 'vue';
163
+ import { ref, reactive, watch, onMounted } from 'vue';
164
164
  import { ElMessage } from 'element-plus';
165
+ import { loadFromCache, saveToCache } from '@/utils';
165
166
 
166
167
  interface ISDKStatus {
167
168
  canPlayMotion: boolean;
@@ -180,18 +181,53 @@ const emit = defineEmits<{
180
181
  (e: 'get-current-motion', params: { getRemainingTime: boolean }): void;
181
182
  }>();
182
183
 
183
- const motionData = reactive({
184
+ // 本地缓存键名常量
185
+ const MOTION_CACHE_KEY = 'motion-data';
186
+ const QUERY_CACHE_KEY = 'query-data';
187
+
188
+
189
+ // 初始化表单数据,优先从缓存加载
190
+ const motionData = reactive(loadFromCache(MOTION_CACHE_KEY, {
184
191
  clipCode: 'DH_ACTION_MODEL103_000023',
185
- });
186
- const queryData = reactive({
192
+ })!);
193
+
194
+ const queryData = reactive(loadFromCache(QUERY_CACHE_KEY, {
187
195
  getRemainingTime: true,
188
- });
196
+ })!);
197
+
189
198
  const result = ref(
190
- '等待动作操作...\n\n📋 功能说明:\n- 播放动作:输入动作编码来播放指定动作\n- 查询动作:获取当前正在播放的动作信息\n- 剩余时间:可选择查询动作剩余播放时间\n\n'
199
+ '等待动作操作...\n\n📋 功能说明:\n- 播放动作:输入动作编码来播放指定动作\n- 查询动作:获取当前正在播放的动作信息\n- 剩余时间:可选择查询动作剩余播放时间\n\n💾 表单数据已启用自动缓存功能'
191
200
  );
192
201
  const playLoading = ref(false);
193
202
  const queryLoading = ref(false);
194
203
 
204
+
205
+ // 监听表单数据变化,自动保存到本地缓存
206
+ watch(
207
+ motionData,
208
+ (newData) => {
209
+ saveToCache(MOTION_CACHE_KEY, newData);
210
+ },
211
+ { deep: true }
212
+ );
213
+
214
+ watch(
215
+ queryData,
216
+ (newData) => {
217
+ saveToCache(QUERY_CACHE_KEY, newData);
218
+ },
219
+ { deep: true }
220
+ );
221
+
222
+ // 组件挂载时的初始化操作
223
+ onMounted(() => {
224
+ console.log('MotionControlAPI: 已从本地缓存加载表单数据', {
225
+ motionData: motionData,
226
+ queryData: queryData
227
+ });
228
+ });
229
+
230
+
195
231
  async function handlePlayMotion() {
196
232
  const { avatarCode } = props.globalConfig;
197
233
  const { clipCode } = motionData;
@@ -241,8 +277,9 @@ async function handleGetCurrentMotion() {
241
277
  }
242
278
 
243
279
  function clearResult() {
244
- result.value = '等待动作操作...\n\n📋 功能说明:\n- 播放动作:输入动作编码来播放指定动作\n- 查询动作:获取当前正在播放的动作信息\n- 剩余时间:可选择查询动作剩余播放时间\n\n🎭 ';
280
+ result.value = '等待动作操作...\n\n📋 功能说明:\n- 播放动作:输入动作编码来播放指定动作\n- 查询动作:获取当前正在播放的动作信息\n- 剩余时间:可选择查询动作剩余播放时间\n\n💾 表单数据已启用自动缓存功能';
245
281
  }
282
+
246
283
  </script>
247
284
 
248
285
  <style lang="scss" scoped>
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 缓存键名前缀
3
+ */
4
+ export const CACHE_KEY_PREFIX = '3d-avatar-sdk__';
5
+
6
+ /**
7
+ * 从本地缓存加载数据
8
+ * @param key 缓存键名
9
+ * @param defaultValue 默认值
10
+ * @returns 缓存的数据或默认值
11
+ */
12
+ export function loadFromCache<T>(key: string, defaultValue?: T): T | undefined {
13
+ try {
14
+ const cached = localStorage.getItem(CACHE_KEY_PREFIX + key);
15
+ if (cached) {
16
+ return JSON.parse(cached);
17
+ }
18
+ } catch (error) {
19
+ console.warn(`加载缓存失败 [${key}]:`, error);
20
+ }
21
+ return defaultValue;
22
+ }
23
+
24
+ /**
25
+ * 保存数据到本地缓存
26
+ * @param key 缓存键名
27
+ * @param data 要保存的数据
28
+ */
29
+ export function saveToCache<T>(key: string, data: T): void {
30
+ try {
31
+ localStorage.setItem(CACHE_KEY_PREFIX + key, JSON.stringify(data));
32
+ } catch (error) {
33
+ console.warn(`保存缓存失败 [${key}]:`, error);
34
+ }
35
+ }