stellar-ui-plus 1.25.7 → 1.25.9

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.
@@ -39,13 +39,9 @@ interface DownloadOptions {
39
39
  onProgressUpdate?: (res: UniApp.OnProgressDownloadResult) => void;
40
40
  }
41
41
 
42
- export function download(
43
- data: ClientData,
44
- options: DownloadOptions
45
- ): UniApp.DownloadTask {
42
+ export function download(data: ClientData, options: DownloadOptions): any {
46
43
  const { success, error, downloadSuccess, onProgressUpdate } = options;
47
44
 
48
- // 参数验证
49
45
  if (!data.updateFile) {
50
46
  const errorMsg = '更新文件地址不能为空';
51
47
  uni.showToast({ title: errorMsg, icon: 'none' });
@@ -54,15 +50,110 @@ export function download(
54
50
  }
55
51
 
56
52
  const package_type = data.package_type;
53
+
54
+ // #ifdef APP-PLUS
57
55
  let timeout: ReturnType<typeof setTimeout>;
58
56
 
57
+ const task = plus.downloader.createDownload(
58
+ data.updateFile,
59
+ {
60
+ filename: '_downloads/stellar_update/',
61
+ },
62
+ (download, status) => {
63
+ clearTimeout(timeout);
64
+
65
+ if (status === 200) {
66
+ if (!download.filename) {
67
+ const errorMsg = '下载文件路径为空';
68
+ uni.showToast({ title: errorMsg, icon: 'none' });
69
+ error?.(new Error(errorMsg));
70
+ return;
71
+ }
72
+
73
+ downloadSuccess?.(download.filename);
74
+
75
+ plus.runtime.install(
76
+ download.filename,
77
+ { force: true },
78
+ () => {
79
+ if (package_type == 1) {
80
+ uni.showModal({
81
+ title: '提示',
82
+ content: '升级成功,请重新启动!',
83
+ confirmText: '确定',
84
+ showCancel: false,
85
+ success: () => {
86
+ success?.();
87
+ plus.runtime.restart();
88
+ },
89
+ });
90
+ } else {
91
+ success?.();
92
+ }
93
+ },
94
+ e => {
95
+ const errorMsg = e.message || '安装失败';
96
+ uni.showModal({
97
+ title: '提示',
98
+ content: errorMsg,
99
+ showCancel: false,
100
+ success: () => {
101
+ error?.(e);
102
+ },
103
+ });
104
+ }
105
+ );
106
+ } else {
107
+ const errorMsg = `下载失败,状态码:${status}`;
108
+ uni.showToast({ title: errorMsg, icon: 'none' });
109
+ error?.(new Error(errorMsg));
110
+ }
111
+ }
112
+ );
113
+
114
+ task.addEventListener('statechanged', (download: any) => {
115
+ if (download.state === 3 && download.totalSize > 0) {
116
+ const progress = Math.round((download.downloadedSize / download.totalSize) * 100);
117
+ onProgressUpdate?.({
118
+ progress,
119
+ totalBytesWritten: download.downloadedSize,
120
+ totalBytesExpectedToWrite: download.totalSize,
121
+ } as UniApp.OnProgressDownloadResult);
122
+
123
+ clearTimeout(timeout);
124
+ timeout = setTimeout(() => {
125
+ task.abort();
126
+ const errorMsg = '下载超时,请检查网络连接';
127
+ uni.showToast({ title: errorMsg, icon: 'none' });
128
+ error?.(new Error(errorMsg));
129
+ }, 300000);
130
+ }
131
+
132
+ if (download.state === 4 || download.state === -1) {
133
+ clearTimeout(timeout);
134
+ }
135
+ });
136
+
137
+ timeout = setTimeout(() => {
138
+ task.abort();
139
+ const errorMsg = '下载超时,请检查网络连接';
140
+ uni.showToast({ title: errorMsg, icon: 'none' });
141
+ error?.(new Error(errorMsg));
142
+ }, 300000);
143
+
144
+ task.start();
145
+ return task;
146
+ // #endif
147
+
148
+ // #ifndef APP-PLUS
149
+ let timeoutFallback: ReturnType<typeof setTimeout>;
150
+
59
151
  const downloadTask = uni.downloadFile({
60
152
  url: data.updateFile,
61
153
  success: res => {
62
- clearTimeout(timeout);
154
+ clearTimeout(timeoutFallback);
63
155
 
64
156
  if (res.statusCode === 200) {
65
- // 文件完整性检查
66
157
  if (!res.tempFilePath) {
67
158
  const errorMsg = '下载文件路径为空';
68
159
  uni.showToast({ title: errorMsg, icon: 'none' });
@@ -88,7 +179,6 @@ export function download(
88
179
  },
89
180
  });
90
181
  } else {
91
- // 整包升级时,执行到此处更新并未完成,只是弹出了安装提示,无法获悉用户是否安装了更新包,若是在此处清除资源,会导致升级包被删除,后续流程无法继续执行
92
182
  success?.();
93
183
  }
94
184
  },
@@ -110,33 +200,29 @@ export function download(
110
200
  error?.(new Error(errorMsg));
111
201
  }
112
202
  },
113
- fail: (err) => {
114
- clearTimeout(timeout);
203
+ fail: err => {
204
+ clearTimeout(timeoutFallback);
115
205
  const errorMsg = `网络请求失败:${err.errMsg || '未知错误'}`;
116
206
  uni.showToast({ title: errorMsg, icon: 'none' });
117
207
  error?.(err);
118
- }
208
+ },
119
209
  });
120
210
 
121
- // 下载进度监控
122
211
  downloadTask.onProgressUpdate(res => {
123
- // 添加进度验证
124
212
  if (res.progress >= 0 && res.progress <= 100) {
125
213
  onProgressUpdate?.(res);
126
214
  }
127
215
 
128
- // 重置超时计时器
129
- clearTimeout(timeout);
130
- timeout = setTimeout(() => {
216
+ clearTimeout(timeoutFallback);
217
+ timeoutFallback = setTimeout(() => {
131
218
  downloadTask.abort();
132
219
  const errorMsg = '下载超时,请检查网络连接';
133
220
  uni.showToast({ title: errorMsg, icon: 'none' });
134
221
  error?.(new Error(errorMsg));
135
- }, 300000); // 5分钟超时
222
+ }, 300000);
136
223
  });
137
224
 
138
- // 初始超时设置
139
- timeout = setTimeout(() => {
225
+ timeoutFallback = setTimeout(() => {
140
226
  downloadTask.abort();
141
227
  const errorMsg = '下载超时,请检查网络连接';
142
228
  uni.showToast({ title: errorMsg, icon: 'none' });
@@ -144,6 +230,7 @@ export function download(
144
230
  }, 300000);
145
231
 
146
232
  return downloadTask;
233
+ // #endif
147
234
  }
148
235
 
149
236
  // 获取设备唯一标识
@@ -187,4 +274,86 @@ export const getVersion = (appVersion: string): Promise<string> => {
187
274
  });
188
275
  }
189
276
  });
190
- };
277
+ };
278
+
279
+ export interface DownloadState {
280
+ versionCode: string;
281
+ updateFile: string;
282
+ startTime: number;
283
+ }
284
+
285
+ const DOWNLOAD_STATE_KEY = 'app_update_download_state';
286
+ export const DOWNLOAD_TIMEOUT = 30 * 60 * 1000;
287
+
288
+ export const saveDownloadState = (state: DownloadState): void => {
289
+ try {
290
+ uni.setStorageSync(DOWNLOAD_STATE_KEY, JSON.stringify(state));
291
+ } catch (e) {
292
+ console.warn('保存下载状态失败:', e);
293
+ }
294
+ };
295
+
296
+ export const getDownloadState = (): DownloadState | null => {
297
+ try {
298
+ const stored = uni.getStorageSync(DOWNLOAD_STATE_KEY);
299
+ if (stored) {
300
+ return JSON.parse(stored);
301
+ }
302
+ } catch (e) {
303
+ console.warn('读取下载状态失败:', e);
304
+ }
305
+ return null;
306
+ };
307
+
308
+ export const clearDownloadState = (): void => {
309
+ try {
310
+ uni.removeStorageSync(DOWNLOAD_STATE_KEY);
311
+ } catch (e) {
312
+ console.warn('清除下载状态失败:', e);
313
+ }
314
+ };
315
+
316
+ export const isDownloadStateExpired = (state: DownloadState): boolean => {
317
+ return Date.now() - state.startTime > DOWNLOAD_TIMEOUT;
318
+ };
319
+
320
+ export interface ExistingDownloadTask {
321
+ task: any;
322
+ state: number;
323
+ filename: string;
324
+ }
325
+
326
+ export const findExistingDownloadTask = (url: string): Promise<ExistingDownloadTask | null> => {
327
+ return new Promise(resolve => {
328
+ // #ifdef APP-PLUS
329
+ plus.downloader.enumerate((tasks: any[]) => {
330
+ if (!tasks || !tasks.length) {
331
+ resolve(null);
332
+ return;
333
+ }
334
+ for (let i = tasks.length - 1; i >= 0; i--) {
335
+ const task = tasks[i];
336
+ if (task.url === url) {
337
+ if (task.state === 0 || task.state === 1 || task.state === 2 || task.state === 3 || task.state === 4) {
338
+ resolve({ task, state: task.state, filename: task.filename || '' });
339
+ return;
340
+ }
341
+ if (task.state === -1) {
342
+ try {
343
+ task.abort();
344
+ } catch (_) {}
345
+ }
346
+ break;
347
+ }
348
+ }
349
+ // #endif
350
+ resolve(null);
351
+ // #ifdef APP-PLUS
352
+ });
353
+ // #endif
354
+
355
+ // #ifndef APP-PLUS
356
+ resolve(null);
357
+ // #endif
358
+ });
359
+ };
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, reactive, onUnmounted } from 'vue';
3
3
  import propsData from './props';
4
- import { type ClientData, type ResponseData, download as downloadMethod, getAppId, getDeviceId, getPlatform, getVersion } from './method';
4
+ import { type ClientData, type ResponseData, download as downloadMethod, getAppId, getDeviceId, getPlatform, getVersion, saveDownloadState, getDownloadState, clearDownloadState, isDownloadStateExpired, findExistingDownloadTask } from './method';
5
5
 
6
6
  // 类型定义
7
7
  interface AppUpdateData extends ClientData {
@@ -34,7 +34,10 @@ const tempFilePath = ref('');
34
34
 
35
35
  // 资源管理
36
36
  let timeoutTimer: ReturnType<typeof setTimeout> | null = null;
37
- let downloadTask: UniApp.DownloadTask | null = null;
37
+ let downloadTask: any = null;
38
+ let nativeDownloadTask: any = null;
39
+ let nativeDownloadListener: ((download: any) => void) | null = null;
40
+ let progressPollTimer: ReturnType<typeof setInterval> | null = null;
38
41
 
39
42
  // 跳过版本相关
40
43
  const skippedVersions = ref<string[]>([]);
@@ -103,12 +106,29 @@ const skipVersion = () => {
103
106
  };
104
107
 
105
108
  // 清理函数
109
+ const stopProgressPolling = () => {
110
+ if (progressPollTimer) {
111
+ clearInterval(progressPollTimer);
112
+ progressPollTimer = null;
113
+ }
114
+ };
115
+
106
116
  const cleanup = () => {
107
117
  if (timeoutTimer) {
108
118
  clearTimeout(timeoutTimer);
109
119
  timeoutTimer = null;
110
120
  }
121
+ stopProgressPolling();
122
+ // #ifdef APP-PLUS
123
+ if (nativeDownloadTask && nativeDownloadListener) {
124
+ try {
125
+ nativeDownloadTask.removeEventListener('statechanged', nativeDownloadListener);
126
+ } catch (_) { }
127
+ }
128
+ // #endif
111
129
  downloadTask = null;
130
+ nativeDownloadTask = null;
131
+ nativeDownloadListener = null;
112
132
  };
113
133
 
114
134
  // 组件卸载时清理资源
@@ -116,7 +136,7 @@ onUnmounted(() => {
116
136
  cleanup();
117
137
  });
118
138
 
119
- const getData = (callback?: (resVersion: { name: string; code: string; updateFile: string }, version: string) => void) => {
139
+ const getData = async (callback?: (resVersion: { name: string; code: string; updateFile: string }, version: string) => void) => {
120
140
  // 参数验证
121
141
  if (!props.apiUrl) {
122
142
  console.error('API地址不能为空');
@@ -130,7 +150,7 @@ const getData = (callback?: (resVersion: { name: string; code: string; updateFil
130
150
  header: {
131
151
  Authorization: `Basic ${btoa(props.clientId + ':' + props.clientSecret)}`,
132
152
  },
133
- success: (res: any) => {
153
+ success: async (res: any) => {
134
154
  try {
135
155
  const _data: {
136
156
  code: number;
@@ -187,6 +207,36 @@ const getData = (callback?: (resVersion: { name: string; code: string; updateFil
187
207
  }
188
208
 
189
209
  if (data.updateFile && data.code !== version.value) {
210
+ const downloadState = getDownloadState();
211
+ const hasValidDownloadState = downloadState
212
+ && downloadState.versionCode === data.code
213
+ && downloadState.updateFile === data.updateFile
214
+ && !isDownloadStateExpired(downloadState);
215
+
216
+ if (hasValidDownloadState) {
217
+ const existing = await findExistingDownloadTask(data.updateFile);
218
+
219
+ if (existing) {
220
+ open.value = true;
221
+ emits('update');
222
+ if (existing.state === 0 || existing.state === 1 || existing.state === 2 || existing.state === 3) {
223
+ updateBtn.value = false;
224
+ resumeDownloadProgress(existing.task);
225
+ } else if (existing.state === 4) {
226
+ updateBtn.value = false;
227
+ tempFilePath.value = existing.filename;
228
+ if (data.package_type === 1 && existing.filename) {
229
+ installWgt(existing.filename);
230
+ }
231
+ }
232
+ return;
233
+ }
234
+ }
235
+
236
+ if (downloadState) {
237
+ clearDownloadState();
238
+ }
239
+
190
240
  open.value = true;
191
241
  emits('update');
192
242
  if (data.isForce) confirm();
@@ -279,11 +329,11 @@ const start = async (callback?: (resVersion: { name: string; code: string; updat
279
329
 
280
330
  const v = await getVersion(props.appVersion);
281
331
  if (v) version.value = v;
282
-
332
+ // #ifdef APP-PLUS
283
333
  // 兜底检查:如果配置了 fallbackApiUrl,先调兜底接口
284
334
  const hit = await checkFallback();
285
335
  if (hit) return; // 命中兜底,不继续正常流程
286
-
336
+ // #endif
287
337
  // 正常更新流程
288
338
  getData(callback);
289
339
  };
@@ -297,16 +347,102 @@ const onProgressUpdate = (res: UniApp.OnProgressDownloadResult) => {
297
347
  }
298
348
  };
299
349
 
300
- const confirm = () => {
301
- // 先清理之前的任务
350
+ const resumeDownloadProgress = (task: any) => {
351
+ // #ifdef APP-PLUS
352
+ if (nativeDownloadTask && nativeDownloadListener) {
353
+ nativeDownloadTask.removeEventListener('statechanged', nativeDownloadListener);
354
+ }
355
+ stopProgressPolling();
356
+
357
+ nativeDownloadTask = task;
358
+
359
+ const updateProgress = () => {
360
+ if (task.downloadedSize !== undefined && task.totalSize > 0) {
361
+ percent.value = Math.round((task.downloadedSize / task.totalSize) * 100);
362
+ downloadedSize.value = (task.downloadedSize / Math.pow(1024, 2)).toFixed(2);
363
+ packageFileSize.value = (task.totalSize / Math.pow(1024, 2)).toFixed(2);
364
+ }
365
+ };
366
+ updateProgress();
367
+
368
+ nativeDownloadListener = (download: any) => {
369
+ if (download.state === 4) {
370
+ stopProgressPolling();
371
+ percent.value = 100;
372
+ downloadedSize.value = packageFileSize.value;
373
+ tempFilePath.value = download.filename;
374
+ if (open.value && data.package_type === 1 && download.filename) {
375
+ installWgt(download.filename);
376
+ }
377
+ } else if (download.state === -1) {
378
+ stopProgressPolling();
379
+ updateBtn.value = true;
380
+ clearDownloadState();
381
+ cleanup();
382
+ }
383
+ };
384
+
385
+ task.addEventListener('statechanged', nativeDownloadListener);
386
+
387
+ if (task.state === 3) {
388
+ progressPollTimer = setInterval(updateProgress, 500);
389
+ }
390
+
391
+ if (task.state === 0) {
392
+ task.start();
393
+ }
394
+ // #endif
395
+ };
396
+
397
+ const installWgt = (filePath: string) => {
398
+ // #ifdef APP-PLUS
399
+ plus.runtime.install(
400
+ filePath,
401
+ { force: true },
402
+ () => {
403
+ uni.showModal({
404
+ title: '提示',
405
+ content: '升级成功,请重新启动!',
406
+ confirmText: '确定',
407
+ showCancel: false,
408
+ success: () => {
409
+ clearDownloadState();
410
+ plus.runtime.restart();
411
+ },
412
+ });
413
+ },
414
+ e => {
415
+ uni.showModal({
416
+ title: '安装失败',
417
+ content: e.message || '安装过程中出现错误',
418
+ showCancel: false,
419
+ });
420
+ }
421
+ );
422
+ // #endif
423
+ };
424
+
425
+ const confirm = async () => {
302
426
  cleanup();
303
427
 
304
- // 参数验证
305
428
  if (!data.updateFile) {
306
429
  uni.showToast({ title: '更新文件地址不能为空', icon: 'none' });
307
430
  return;
308
431
  }
309
432
 
433
+ // #ifdef APP-PLUS
434
+ const stale = await findExistingDownloadTask(data.updateFile);
435
+ if (stale) {
436
+ try { stale.task.abort(); } catch (_) { }
437
+ }
438
+ // #endif
439
+
440
+ saveDownloadState({
441
+ versionCode: data.code,
442
+ updateFile: data.updateFile,
443
+ startTime: Date.now(),
444
+ });
445
+
310
446
  if (data.package_type == 0) {
311
447
  if (data.updateFile.includes('.apk')) {
312
448
  updateBtn.value = false;
@@ -315,9 +451,11 @@ const confirm = () => {
315
451
  downloadSuccess: path => (tempFilePath.value = path),
316
452
  error: () => {
317
453
  updateBtn.value = true;
454
+ clearDownloadState();
318
455
  cleanup();
319
456
  },
320
457
  success: () => {
458
+ clearDownloadState();
321
459
  cleanup();
322
460
  },
323
461
  });
@@ -334,9 +472,11 @@ const confirm = () => {
334
472
  downloadSuccess: path => (tempFilePath.value = path),
335
473
  error: () => {
336
474
  updateBtn.value = true;
475
+ clearDownloadState();
337
476
  cleanup();
338
477
  },
339
478
  success: () => {
479
+ clearDownloadState();
340
480
  cleanup();
341
481
  },
342
482
  });
@@ -359,6 +499,7 @@ const install = () => {
359
499
  tempFilePath.value,
360
500
  { force: true },
361
501
  () => {
502
+ clearDownloadState();
362
503
  if (data.package_type == 1) {
363
504
  uni.showModal({
364
505
  title: '提示',
@@ -388,10 +529,10 @@ const cancelDownload = () => {
388
529
  content: '确定要取消下载吗?',
389
530
  success: res => {
390
531
  if (res.confirm) {
391
- // 取消下载时才 abort 下载任务
392
532
  if (downloadTask) {
393
- downloadTask.abort();
533
+ try { downloadTask.abort(); } catch (_) { }
394
534
  }
535
+ clearDownloadState();
395
536
  cleanup();
396
537
  updateBtn.value = true;
397
538
  percent.value = 0;
@@ -432,7 +573,8 @@ defineExpose({
432
573
  <view class="update-footer">
433
574
  <view class="update-progress-box" v-if="!updateBtn">
434
575
  <view class="progress-container">
435
- <progress class="update-progress" border-radius="35" :percent="percent" activeColor="#3DA7FF" backgroundColor="#f0f0f0" show-info stroke-width="12" />
576
+ <progress class="update-progress" border-radius="35" :percent="percent" activeColor="#3DA7FF"
577
+ backgroundColor="#f0f0f0" show-info stroke-width="12" />
436
578
  <view class="progress-text">{{ percent }}%</view>
437
579
  </view>
438
580
 
@@ -441,10 +583,12 @@ defineExpose({
441
583
  <text class="success-icon">✓</text>
442
584
  下载完成,准备安装...
443
585
  </text>
444
- <text class="update-down-msg" v-else>正在下载,请稍后 ({{ downloadedSize }}/{{ packageFileSize }}MB)</text>
586
+ <text class="update-down-msg" v-else>正在下载,请稍后 ({{ downloadedSize }}/{{ packageFileSize
587
+ }}MB)</text>
445
588
  </view>
446
589
 
447
- <button v-if="!tempFilePath && !data.isForce" class="cancel-download-btn" @click="cancelDownload">取消下载</button>
590
+ <button v-if="!tempFilePath && !data.isForce" class="cancel-download-btn"
591
+ @click="cancelDownload">取消下载</button>
448
592
  </view>
449
593
 
450
594
  <template v-if="updateBtn">
@@ -456,7 +600,8 @@ defineExpose({
456
600
  <button v-if="!data.isForce" class="update-button skip" plain @click="skipVersion">跳过此版本</button>
457
601
  </template>
458
602
 
459
- <button class="update-button secondary" plain @click="install" v-else-if="data.package_type === 0 && tempFilePath">立即安装</button>
603
+ <button class="update-button secondary" plain @click="install"
604
+ v-else-if="data.package_type === 0 && tempFilePath">立即安装</button>
460
605
  </view>
461
606
 
462
607
  <view class="update-close" v-if="!data.isForce" @click.stop="close">✖</view>