@ledvance/base 1.3.61 → 1.3.63
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/package.json +1 -1
- package/src/api/native.ts +1 -0
- package/src/components/ColorAdjustView.tsx +77 -11
- package/src/components/ColorTempAdjustView.tsx +75 -6
- package/src/components/rect-color-and-bright-picker/ColourPicker.tsx +266 -0
- package/src/components/rect-color-and-bright-picker/RectPicker.tsx +398 -0
- package/src/components/rect-color-and-bright-picker/Slider.tsx +468 -0
- package/src/components/rect-color-and-bright-picker/Thumb.tsx +78 -0
- package/src/components/rect-color-and-bright-picker/WhitePicker.tsx +400 -0
- package/src/components/rect-color-and-bright-picker/index.tsx +5 -0
- package/src/components/rect-color-and-bright-picker/res/index.ts +3 -0
- package/src/components/rect-color-and-bright-picker/res/thumb-mask@2x.png +0 -0
- package/src/components/rect-color-and-bright-picker/res/thumb-mask@3x.png +0 -0
- package/src/components/rect-color-and-bright-picker/utils/color.ts +73 -0
- package/src/components/rect-color-and-bright-picker/utils/storage.ts +97 -0
- package/src/components/weekSelect.tsx +7 -7
- package/src/composeLayout.tsx +5 -1
- package/src/i18n/strings.ts +11 -11
- package/src/models/TuyaApi.ts +132 -0
- package/src/models/modules/NativePropsSlice.tsx +22 -2
- package/src/utils/common.ts +41 -9
- package/src/utils/index.ts +58 -0
package/src/i18n/strings.ts
CHANGED
|
@@ -5973,7 +5973,7 @@ export default {
|
|
|
5973
5973
|
"btsolar_groups_inductionsync_description": "Den Bewegungssensor aller Lampen zu synchronisieren, so dass, wenn eine von ihnen eine Bewegung erkennt, auch die anderen eingeschaltet werden.",
|
|
5974
5974
|
"camera_calibration": "Kamerakalibrierung",
|
|
5975
5975
|
"camera_calibration_desc": "Die Kamerakalibrierung dauert ca. 25 Sekunden. Möchtest Du fortfahren?",
|
|
5976
|
-
"camera_edit_site_name": "
|
|
5976
|
+
"camera_edit_site_name": "Standortnamen bearbeiten",
|
|
5977
5977
|
"camera_errmsg_site_point_limit": "Die Standortüberwachung konnte nicht aktiviert werden, da weniger als 2 Standorte hinzugefügt wurden.",
|
|
5978
5978
|
"camera_feature_1_headline": "Sirene",
|
|
5979
5979
|
"camera_feature_2_headline": "Mikrofon",
|
|
@@ -6047,10 +6047,10 @@ export default {
|
|
|
6047
6047
|
"camera_settings_talk_mode_firstbox_option2_topic": "Beidseitige Kommunikation",
|
|
6048
6048
|
"camera_settings_talk_mode_secondtopic": "Die Sprache kann von der Umgebung beeinflusst werden. Je nach Situation empfehlen wir entweder ein- oder beidseitige-Kommunikation.",
|
|
6049
6049
|
"camera_settings_talk_mode_topic": "Gesprächsmodus",
|
|
6050
|
-
"camera_site": "
|
|
6051
|
-
"camera_site_delete_dialog_title": "Möchtest Du
|
|
6052
|
-
"camera_site_name": "
|
|
6053
|
-
"camera_site_overview_empty_button_add_text": "
|
|
6050
|
+
"camera_site": "Standort",
|
|
6051
|
+
"camera_site_delete_dialog_title": "Möchtest Du den Standort wirklich löschen?",
|
|
6052
|
+
"camera_site_name": "Standortname",
|
|
6053
|
+
"camera_site_overview_empty_button_add_text": "Standort hinzufügen",
|
|
6054
6054
|
"camera_site_overview_empty_information_text": "Du hast noch keine Ort hinzugefügt",
|
|
6055
6055
|
"camera_site_overview_warning_max_number_text": "Die maximale Anzahl an Orte wurde erreicht.",
|
|
6056
6056
|
"camera_status_indicator": "Statusanzeige",
|
|
@@ -6486,7 +6486,7 @@ export default {
|
|
|
6486
6486
|
"hybrid_switchstate_setting3": "Einstellung 3",
|
|
6487
6487
|
"hybrid_switchstate_setting3_description": "Schaltertaste",
|
|
6488
6488
|
"hybrid_switchstate_setting_text": "Schaltereinstellungen",
|
|
6489
|
-
"hybrid_switchstate_title": "
|
|
6489
|
+
"hybrid_switchstate_title": "Schalterkonfiguration",
|
|
6490
6490
|
"infobutton_totalenergy": "Die Gesamtenergie zeigt nur die gesamten Verbrauchs-/Erzeugungsdaten nach dem letzten Reset an.\nDie Daten wurden an folgenden Tagen umgeschaltet:",
|
|
6491
6491
|
"intermittent_time": "Unterbrechungsdauer",
|
|
6492
6492
|
"irrigation": "Bewässerung",
|
|
@@ -6889,7 +6889,7 @@ export default {
|
|
|
6889
6889
|
"switch_4channels4setting": "Schalter 4 Einstellung",
|
|
6890
6890
|
"switch_active_timer_field_small_off_text": "Schalter aus bei {0}",
|
|
6891
6891
|
"switch_active_timer_field_small_on_text": "Schalter ein in {0}",
|
|
6892
|
-
"switch_doublepress": "
|
|
6892
|
+
"switch_doublepress": "Doppeltes Drücken",
|
|
6893
6893
|
"switch_interlock": "Verriegelung",
|
|
6894
6894
|
"switch_interlock_addbtn_text": "Verriegelung hinzufügen",
|
|
6895
6895
|
"switch_interlock_addtitle": "Eine neue Verriegelung hinzufügen",
|
|
@@ -6897,10 +6897,10 @@ export default {
|
|
|
6897
6897
|
"switch_interlock_editbtn_text": "Verriegelung löschen",
|
|
6898
6898
|
"switch_interlock_edittitle": "Bearbeiten der Verriegelung",
|
|
6899
6899
|
"switch_interlockingdescription": "Um die Bedienung jeweils nur eines Schalters zu ermöglichen",
|
|
6900
|
-
"switch_longpress": "Langes
|
|
6900
|
+
"switch_longpress": "Langes Drücken",
|
|
6901
6901
|
"switch_overcharge_headline_description": "Die Funktion schaltet das Aufladen Deiner mobilen Geräte, einschließlich Mobiltelefone und Powerbanks, automatisch aus, wenn sie vollständig aufgeladen sind, um ein Überladen zu verhindern und die Akkulaufzeit zu verlängern.",
|
|
6902
6902
|
"switch_overcharge_headline_text": "Überladeschutz",
|
|
6903
|
-
"switch_singlepress": "
|
|
6903
|
+
"switch_singlepress": "Einmaliges Drücken",
|
|
6904
6904
|
"switchdescription_energy": "Beachte, dass die Gesamtenergiedaten zurückgesetzt werden und nicht wiederhergestellt werden können.",
|
|
6905
6905
|
"switchinching_overview_description_text": "Um das Gerät nach einer bestimmten Zeit automatisch auszuschalten.",
|
|
6906
6906
|
"switchmodule_switch1description": "Schalter 1 Beschreibung",
|
|
@@ -6909,11 +6909,11 @@ export default {
|
|
|
6909
6909
|
"switchmodule_switch2title": "Schalter 2",
|
|
6910
6910
|
"switchmodule_switchdescription": "Beschreibung des Schalters",
|
|
6911
6911
|
"switchmodule_switchtitle": "Wechseln",
|
|
6912
|
-
"switchmodule_typesetting": "
|
|
6912
|
+
"switchmodule_typesetting": "Schalterkonfiguration",
|
|
6913
6913
|
"switchmodule_typesetting1": "Jedes Umschalten des Wandschalters löst eine Änderung des Gerätezustands aus.",
|
|
6914
6914
|
"switchmodule_typesetting2": "Die Positionen der Wandschalter entsprechen festen EIN- und AUS-Zuständen. Durch Einschalten des Schalters wird das Gerät eingeschaltet. Ist es bereits eingeschaltet, wird der Befehl ignoriert und der Zustand bleibt unverändert.",
|
|
6915
6915
|
"switchmodule_typesetting3": "Wenn dein Wandschalter nur in der EIN-Position bleibt, während er gedrückt wird, und automatisch in die AUS-Position zurückkehrt, wenn er losgelassen wird, verhält er sich wie ein Taster.",
|
|
6916
|
-
"switchmodule_typesettingdescription": "So konfigurierst du die Einstellung für den
|
|
6916
|
+
"switchmodule_typesettingdescription": "So konfigurierst du die Einstellung für den Wandschalter",
|
|
6917
6917
|
"switchname_1channel": "Smarte Schalter 1 Kanal",
|
|
6918
6918
|
"switchname_2channel": "Smarte Schalter 2 Kanal",
|
|
6919
6919
|
"switchname_4channel": "Smarte Schalter 4 Kanal",
|
package/src/models/TuyaApi.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {commonApi} from '@tuya/tuya-panel-api'
|
|
2
2
|
import { IGetDpResultByHourResponse, IGetDpResultByMonthResponse } from '@tuya/tuya-panel-api/lib/common/interface'
|
|
3
3
|
import {sendAppEvent} from "../api/native";
|
|
4
|
+
import {retryWithBackoff} from "../utils/index";
|
|
5
|
+
import {TYSdk} from "tuya-panel-kit";
|
|
4
6
|
|
|
5
7
|
export interface PagingResult<T> {
|
|
6
8
|
data: T
|
|
@@ -148,3 +150,133 @@ export async function getDpResultByHour(
|
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
152
|
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 获取设备所有数据点记录(支持失败重试)
|
|
156
|
+
* @param commonApi API实例
|
|
157
|
+
* @param devId 设备ID
|
|
158
|
+
* @param dpIds 数据点ID,如 '19'
|
|
159
|
+
* @param sortType 排序方式
|
|
160
|
+
* @param retryOptions 重试选项
|
|
161
|
+
* @returns 所有数据点记录
|
|
162
|
+
*/
|
|
163
|
+
export async function getAllDpReportLogs(
|
|
164
|
+
devId: string,
|
|
165
|
+
dpIds: string[],
|
|
166
|
+
sortType: 'ASC' | 'DESC' = 'ASC',
|
|
167
|
+
retryOptions: {
|
|
168
|
+
maxRetries?: number;
|
|
169
|
+
initialDelay?: number;
|
|
170
|
+
maxDelay?: number;
|
|
171
|
+
backoffFactor?: number;
|
|
172
|
+
} = {}
|
|
173
|
+
): Promise<DpReportSataData[]> {
|
|
174
|
+
const limit = 100; // 每次请求的数据量
|
|
175
|
+
let offset = 1;
|
|
176
|
+
let hasNext = true;
|
|
177
|
+
const allDps: DpReportSataData[] = [];
|
|
178
|
+
|
|
179
|
+
while (hasNext) {
|
|
180
|
+
try {
|
|
181
|
+
// 使用重试函数包装API调用
|
|
182
|
+
const res: DpReportSataResData = await retryWithBackoff(
|
|
183
|
+
() => commonApi.statApi.getDpReportLog({
|
|
184
|
+
devId,
|
|
185
|
+
dpIds: dpIds.join(','),
|
|
186
|
+
offset,
|
|
187
|
+
limit,
|
|
188
|
+
sortType
|
|
189
|
+
}),
|
|
190
|
+
{
|
|
191
|
+
...retryOptions,
|
|
192
|
+
// 自定义判断哪些错误需要重试
|
|
193
|
+
shouldRetry: (error) => {
|
|
194
|
+
// 网络错误、超时错误或服务器错误(5xx)通常需要重试
|
|
195
|
+
const isNetworkError = error.name === 'NetworkError' ||
|
|
196
|
+
error.name === 'TimeoutError' ||
|
|
197
|
+
(error.response && error.response.status >= 500);
|
|
198
|
+
|
|
199
|
+
// 对于特定的业务错误码也可以在这里添加判断
|
|
200
|
+
return isNetworkError;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
allDps.push(...res.dps);
|
|
206
|
+
|
|
207
|
+
if (res.hasNext) {
|
|
208
|
+
offset += limit;
|
|
209
|
+
} else {
|
|
210
|
+
hasNext = false;
|
|
211
|
+
}
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error('获取数据点记录失败,不再继续获取:', error);
|
|
214
|
+
hasNext = false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return allDps;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export const saveDeviceExtInfo = async (devId: string, key: string, value: string): Promise<boolean> => {
|
|
222
|
+
try {
|
|
223
|
+
return await new Promise<any>((resolve, reject) => {
|
|
224
|
+
TYSdk.native.apiRNRequest(
|
|
225
|
+
{
|
|
226
|
+
v: "1.0",
|
|
227
|
+
postData: {
|
|
228
|
+
value: value,
|
|
229
|
+
key: key,
|
|
230
|
+
devId: devId
|
|
231
|
+
},
|
|
232
|
+
a: "tuya.m.solution.device.storage.save"
|
|
233
|
+
},
|
|
234
|
+
(success: any) => {
|
|
235
|
+
console.log('tuya.m.solution.device.storage.save success', success)
|
|
236
|
+
resolve(success)
|
|
237
|
+
},
|
|
238
|
+
(error: any) => {
|
|
239
|
+
console.log('tuya.m.solution.device.storage.save error', error)
|
|
240
|
+
reject(error)
|
|
241
|
+
}
|
|
242
|
+
)
|
|
243
|
+
})
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.log('saveDeviceExtInfo error', error)
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export const getDeviceExtInfo = async (devId: string, key: string): Promise<string | null> => {
|
|
251
|
+
try {
|
|
252
|
+
const success = await new Promise<any>((resolve, reject) => {
|
|
253
|
+
TYSdk.native.apiRNRequest(
|
|
254
|
+
{
|
|
255
|
+
v: "1.0",
|
|
256
|
+
postData: {
|
|
257
|
+
"key": key,
|
|
258
|
+
"devId": devId
|
|
259
|
+
},
|
|
260
|
+
a: "tuya.m.solution.device.storage.get"
|
|
261
|
+
},
|
|
262
|
+
(success: any) => {
|
|
263
|
+
console.log('tuya.m.solution.device.storage.get success', success)
|
|
264
|
+
resolve(success)
|
|
265
|
+
},
|
|
266
|
+
(error: any) => {
|
|
267
|
+
console.log('tuya.m.solution.device.storage.get error', error)
|
|
268
|
+
reject(error)
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
let data = success
|
|
274
|
+
if (typeof success === 'string') {
|
|
275
|
+
data = JSON.parse(success)
|
|
276
|
+
}
|
|
277
|
+
return data.value
|
|
278
|
+
} catch (error) {
|
|
279
|
+
console.log('getDeviceExtInfo error', error)
|
|
280
|
+
return null
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -24,6 +24,7 @@ export interface NativeProps {
|
|
|
24
24
|
is24HourClock: boolean
|
|
25
25
|
timeZone: string
|
|
26
26
|
gestureControlValues: GestureControlType
|
|
27
|
+
newPalette?: boolean
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
interface FlagModeState {
|
|
@@ -34,6 +35,7 @@ interface FlagModeState {
|
|
|
34
35
|
export interface DeviceInfo {
|
|
35
36
|
devId: string
|
|
36
37
|
pId: string
|
|
38
|
+
category?: string
|
|
37
39
|
dps: any
|
|
38
40
|
}
|
|
39
41
|
|
|
@@ -86,7 +88,8 @@ const initialState: NativeProps = {
|
|
|
86
88
|
timeZone: 'Europe/Berlin',
|
|
87
89
|
gestureControlValues: {
|
|
88
90
|
isEnd: false
|
|
89
|
-
}
|
|
91
|
+
},
|
|
92
|
+
newPalette: false
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
// energy generation
|
|
@@ -113,6 +116,9 @@ const nativePropsSlice = createSlice({
|
|
|
113
116
|
if (!!action.payload.deviceInfo.pId) {
|
|
114
117
|
state.deviceInfo.pId = action.payload.deviceInfo.pId
|
|
115
118
|
}
|
|
119
|
+
if (!!action.payload.deviceInfo.category) {
|
|
120
|
+
state.deviceInfo.category = action.payload.deviceInfo.category
|
|
121
|
+
}
|
|
116
122
|
state.deviceInfo.dps = { ...state.deviceInfo.dps, ...action.payload.deviceInfo.dps }
|
|
117
123
|
state.initialDps = { ...state.initialDps, ...action.payload.initialDps }
|
|
118
124
|
},
|
|
@@ -184,6 +190,9 @@ const nativePropsSlice = createSlice({
|
|
|
184
190
|
keys.forEach(key => {
|
|
185
191
|
state.gestureControlValues[key] = action.payload[key]
|
|
186
192
|
})
|
|
193
|
+
},
|
|
194
|
+
setNewPalette(state, action: PayloadAction<boolean>) {
|
|
195
|
+
state.newPalette = action.payload
|
|
187
196
|
}
|
|
188
197
|
},
|
|
189
198
|
})
|
|
@@ -231,6 +240,10 @@ const useDeviceId = () => {
|
|
|
231
240
|
return useSelector(store => store.ldvModules.deviceInfo.devId)
|
|
232
241
|
}
|
|
233
242
|
|
|
243
|
+
const useDeviceCategory = () => {
|
|
244
|
+
return useSelector(store => store.ldvModules.deviceInfo.category)
|
|
245
|
+
}
|
|
246
|
+
|
|
234
247
|
const useGroupId = () => {
|
|
235
248
|
return useSelector(store => store.ldvModules.uaGroupInfo.tyGroupId)
|
|
236
249
|
}
|
|
@@ -471,6 +484,10 @@ function useGestureControl<K extends keyof GestureControlType>(key: K): [Gesture
|
|
|
471
484
|
return [value]
|
|
472
485
|
}
|
|
473
486
|
|
|
487
|
+
const useNewPalette = () => {
|
|
488
|
+
return useSelector(store => store.ldvModules.newPalette)
|
|
489
|
+
}
|
|
490
|
+
|
|
474
491
|
export const useFanMaxSpeed = () => {
|
|
475
492
|
const { productId } = useDeviceInfo()
|
|
476
493
|
return fanProductList.includes(productId) ? 20 : 3
|
|
@@ -497,6 +514,7 @@ export const {
|
|
|
497
514
|
setTimeZone,
|
|
498
515
|
setEnergieverbrauch,
|
|
499
516
|
setGestureControlValues,
|
|
517
|
+
setNewPalette,
|
|
500
518
|
} = nativePropsSlice.actions
|
|
501
519
|
|
|
502
520
|
export {
|
|
@@ -522,5 +540,7 @@ export {
|
|
|
522
540
|
useTimeZone,
|
|
523
541
|
useTimeZoneCity,
|
|
524
542
|
useEnergieverbrauch,
|
|
525
|
-
useGestureControl
|
|
543
|
+
useGestureControl,
|
|
544
|
+
useDeviceCategory,
|
|
545
|
+
useNewPalette
|
|
526
546
|
}
|
package/src/utils/common.ts
CHANGED
|
@@ -19,17 +19,49 @@ export const loopsText = [
|
|
|
19
19
|
I18n.getLang('timeschedule_add_schedule_weekday6_text'),
|
|
20
20
|
]
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* 根据循环设置和时间生成循环文本描述
|
|
24
|
+
* @param {number[]|string[]} loop - 表示每周哪几天启用的数组,1表示启用,0表示不启用
|
|
25
|
+
* @param {string} time - 可选的时间字符串,格式为 "HH:MM"
|
|
26
|
+
* @returns {string} 格式化后的循环文本
|
|
27
|
+
*/
|
|
28
|
+
export const loopText = (loop: number[] | string[], time: string = ''): string => {
|
|
29
|
+
// 判断是否为今天(如果提供了时间)
|
|
30
|
+
let isToday = true;
|
|
24
31
|
if (time) {
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
isToday = !
|
|
32
|
+
const [hours, minutes] = time.split(':').map(Number);
|
|
33
|
+
const currentTime = dayjs();
|
|
34
|
+
const targetTime = dayjs().set('hour', hours).set('minute', minutes);
|
|
35
|
+
isToday = !targetTime.isBefore(currentTime);
|
|
29
36
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
|
|
38
|
+
// 将循环数组转换为带有启用状态的对象数组
|
|
39
|
+
const weekdaysWithStatus = loopsText.map((title, index) => ({
|
|
40
|
+
title,
|
|
41
|
+
enable: Number(loop[index]) === 1
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
// 将周日移到数组末尾(如果需要按周一到周日的顺序显示)
|
|
45
|
+
const first = weekdaysWithStatus.shift();
|
|
46
|
+
first && weekdaysWithStatus.push(first);
|
|
47
|
+
|
|
48
|
+
// 获取已启用的星期几
|
|
49
|
+
const enabledWeekdays = weekdaysWithStatus
|
|
50
|
+
.filter(item => item.enable)
|
|
51
|
+
.map(item => item.title);
|
|
52
|
+
|
|
53
|
+
// 根据启用的天数返回不同的文本
|
|
54
|
+
switch (enabledWeekdays.length) {
|
|
55
|
+
case 0:
|
|
56
|
+
return I18n.getLang(isToday
|
|
57
|
+
? 'motion_detection_time_schedule_notifications_field_weekdays_text2'
|
|
58
|
+
: 'motion_detection_time_schedule_notifications_field_weekdays_text3');
|
|
59
|
+
case 7:
|
|
60
|
+
return I18n.getLang('motion_detection_time_schedule_notifications_field_weekdays_text4');
|
|
61
|
+
default:
|
|
62
|
+
return enabledWeekdays.join(' ');
|
|
63
|
+
}
|
|
64
|
+
};
|
|
33
65
|
|
|
34
66
|
const tommorrow = () => {
|
|
35
67
|
const text = I18n.getLang('feature_summary_frequency_txt_2').split(' ')
|
package/src/utils/index.ts
CHANGED
|
@@ -189,3 +189,61 @@ export function abbreviateMonths(str: string) {
|
|
|
189
189
|
export function overDays(date: string, days: number): boolean {
|
|
190
190
|
return dayjs().diff(dayjs(date), 'days') >= days
|
|
191
191
|
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 通用重试函数,支持指数退避算法
|
|
196
|
+
* @param fn 需要重试的异步函数
|
|
197
|
+
* @param options 重试选项
|
|
198
|
+
* @returns 异步函数的结果
|
|
199
|
+
*/
|
|
200
|
+
export async function retryWithBackoff<T>(
|
|
201
|
+
fn: () => Promise<T>,
|
|
202
|
+
options: {
|
|
203
|
+
maxRetries?: number; // 最大重试次数
|
|
204
|
+
initialDelay?: number; // 初始延迟时间(毫秒)
|
|
205
|
+
maxDelay?: number; // 最大延迟时间(毫秒)
|
|
206
|
+
backoffFactor?: number; // 退避因子
|
|
207
|
+
shouldRetry?: (error: any) => boolean; // 自定义判断是否应该重试的函数
|
|
208
|
+
} = {}
|
|
209
|
+
): Promise<T> {
|
|
210
|
+
const {
|
|
211
|
+
maxRetries = 3,
|
|
212
|
+
initialDelay = 1000,
|
|
213
|
+
maxDelay = 30000,
|
|
214
|
+
backoffFactor = 2,
|
|
215
|
+
shouldRetry = () => true
|
|
216
|
+
} = options;
|
|
217
|
+
|
|
218
|
+
let retries = 0;
|
|
219
|
+
let delay = initialDelay;
|
|
220
|
+
|
|
221
|
+
const execute = async (): Promise<T> => {
|
|
222
|
+
try {
|
|
223
|
+
return await fn();
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (retries >= maxRetries || !shouldRetry(error)) {
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
retries++;
|
|
230
|
+
|
|
231
|
+
// 计算下一次重试的延迟时间(指数退避)
|
|
232
|
+
delay = Math.min(delay * backoffFactor, maxDelay);
|
|
233
|
+
|
|
234
|
+
// 添加一些随机性,避免多个请求同时重试
|
|
235
|
+
const jitter = delay * 0.2 * Math.random();
|
|
236
|
+
const actualDelay = delay + jitter;
|
|
237
|
+
|
|
238
|
+
console.log(`请求失败,${retries}/${maxRetries} 次重试,等待 ${Math.round(actualDelay)}ms...`, error);
|
|
239
|
+
|
|
240
|
+
// 等待延迟时间
|
|
241
|
+
await new Promise(resolve => setTimeout(resolve, actualDelay));
|
|
242
|
+
|
|
243
|
+
// 递归重试
|
|
244
|
+
return execute();
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
return execute();
|
|
249
|
+
}
|