@haluo/util 2.0.29 → 2.0.30
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 +350 -350
- package/dist/index.d.ts +39 -39
- package/dist/index.js +43 -43
- package/dist/modules/cookie/index.d.ts +27 -27
- package/dist/modules/cookie/index.js +49 -49
- package/dist/modules/date/index.d.ts +52 -52
- package/dist/modules/date/index.js +185 -185
- package/dist/modules/dom/index.d.ts +28 -28
- package/dist/modules/dom/index.js +55 -55
- package/dist/modules/filter/index.d.ts +24 -24
- package/dist/modules/filter/index.js +38 -38
- package/dist/modules/format/index.d.ts +15 -15
- package/dist/modules/format/index.js +16 -16
- package/dist/modules/match/index.d.ts +12 -12
- package/dist/modules/match/index.js +27 -27
- package/dist/modules/monitor/index.d.ts +3 -3
- package/dist/modules/monitor/index.js +10 -10
- package/dist/modules/monitor/lib/jsError.d.ts +1 -1
- package/dist/modules/monitor/lib/jsError.js +57 -57
- package/dist/modules/monitor/lib/timing.d.ts +1 -1
- package/dist/modules/monitor/lib/timing.js +65 -65
- package/dist/modules/monitor/lib/xhr.d.ts +1 -1
- package/dist/modules/monitor/lib/xhr.js +41 -41
- package/dist/modules/monitor/utils/onload.d.ts +1 -1
- package/dist/modules/monitor/utils/onload.js +8 -8
- package/dist/modules/monitor/utils/tracker.d.ts +7 -7
- package/dist/modules/monitor/utils/tracker.js +49 -49
- package/dist/modules/number/index.d.ts +47 -47
- package/dist/modules/number/index.js +114 -114
- package/dist/modules/open-app/index.d.ts +84 -84
- package/dist/modules/open-app/index.js +244 -244
- package/dist/modules/sentry/index.d.ts +15 -15
- package/dist/modules/sentry/index.js +73 -73
- package/dist/modules/tools/index.d.ts +166 -166
- package/dist/modules/tools/index.js +382 -382
- package/dist/modules/upload/aliOss.d.ts +291 -291
- package/dist/modules/upload/aliOss.js +629 -629
- package/dist/modules/upload/index.d.ts +38 -38
- package/dist/modules/upload/index.js +44 -44
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.js +1 -1
- package/package.json +88 -88
- package/dist/types/modules/cookie/index.d.ts +0 -27
- package/dist/types/modules/date/index.d.ts +0 -52
- package/dist/types/modules/dom/index.d.ts +0 -28
- package/dist/types/modules/filter/index.d.ts +0 -24
- package/dist/types/modules/format/index.d.ts +0 -15
- package/dist/types/modules/match/index.d.ts +0 -12
- package/dist/types/modules/monitor/index.d.ts +0 -3
- package/dist/types/modules/monitor/lib/jsError.d.ts +0 -1
- package/dist/types/modules/monitor/lib/timing.d.ts +0 -1
- package/dist/types/modules/monitor/lib/xhr.d.ts +0 -1
- package/dist/types/modules/monitor/utils/onload.d.ts +0 -1
- package/dist/types/modules/monitor/utils/tracker.d.ts +0 -7
- package/dist/types/modules/number/index.d.ts +0 -41
- package/dist/types/modules/sentry/index.d.ts +0 -15
- package/dist/types/modules/tools/index.d.ts +0 -166
- package/dist/types/types/index.d.ts +0 -3
|
@@ -1,629 +1,629 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file 阿里云OSS上传统一工具类 v4
|
|
3
|
-
* @description 整合所有项目的aliOss业务逻辑,通过传参形式支持不同业务场景
|
|
4
|
-
* @Author: wanghui
|
|
5
|
-
* @createBy: @2025.11.17
|
|
6
|
-
*
|
|
7
|
-
* 使用示例:
|
|
8
|
-
* ```typescript
|
|
9
|
-
* import { createAliOssUploader } from '@haluo/util'
|
|
10
|
-
*
|
|
11
|
-
* // 创建上传实例,传入业务相关的API函数
|
|
12
|
-
* const ossUploader = createAliOssUploader({
|
|
13
|
-
* getSts: (params) => api.getSts(params),
|
|
14
|
-
* darkWaterUploadImage: (params) => api.darkWaterUploadImage(params),
|
|
15
|
-
* multiTransferImage: (params) => api.multiTransferImage(params),
|
|
16
|
-
* generatePrePresignedUrl: (params) => api.generatePrePresignedUrl(params),
|
|
17
|
-
* messageWarning: (msg) => window.$message.warning(msg)
|
|
18
|
-
* })
|
|
19
|
-
*
|
|
20
|
-
* // 图片上传
|
|
21
|
-
* ossUploader.ossUploadImage({
|
|
22
|
-
* file: file,
|
|
23
|
-
* businessType: BusinessType.CONTENT, // 业务类型 BusinessType
|
|
24
|
-
* imageType: 'official', // nowater、official、panoram、forum、avatar、square、carport
|
|
25
|
-
* quality: 0.7, // 压缩质量
|
|
26
|
-
* batchTransfer: false, // 是否批量转换水印
|
|
27
|
-
* notCompress: false, // 是否不压缩
|
|
28
|
-
* idCard: false, // 是否身份证上传
|
|
29
|
-
* onProgress: (e) => console.log(e.percent),
|
|
30
|
-
* onSuccess: (val) => console.log(val),
|
|
31
|
-
* onError: (err) => console.error(err)
|
|
32
|
-
* })
|
|
33
|
-
*
|
|
34
|
-
* // 文件上传
|
|
35
|
-
* ossUploader.ossUploadFile({
|
|
36
|
-
* file: file,
|
|
37
|
-
* businessType: BusinessType.CONTENT, // 业务类型 BusinessType
|
|
38
|
-
* onProgress: (e) => console.log(e.percent),
|
|
39
|
-
* onSuccess: (val) => console.log(val),
|
|
40
|
-
* onError: (err) => console.error(err)
|
|
41
|
-
* })
|
|
42
|
-
* ```
|
|
43
|
-
*/
|
|
44
|
-
import lrz from 'lrz';
|
|
45
|
-
// ==================== 常量定义 ====================
|
|
46
|
-
/** 支持的图片格式 */
|
|
47
|
-
const SUPPORTED_IMAGE_TYPES = [
|
|
48
|
-
'image/jpeg',
|
|
49
|
-
'image/png',
|
|
50
|
-
'image/gif',
|
|
51
|
-
'image/bmp',
|
|
52
|
-
'image/webp',
|
|
53
|
-
'image/svg+xml',
|
|
54
|
-
'image/tiff',
|
|
55
|
-
];
|
|
56
|
-
/** 支持的图片格式 ext */
|
|
57
|
-
const SUPPORTED_IMAGE_EXTS = [
|
|
58
|
-
'jpg',
|
|
59
|
-
'jpeg',
|
|
60
|
-
'png',
|
|
61
|
-
'gif',
|
|
62
|
-
'bmp',
|
|
63
|
-
'webp',
|
|
64
|
-
'svg',
|
|
65
|
-
'tiff',
|
|
66
|
-
];
|
|
67
|
-
/** 小图片阈值(KB) */
|
|
68
|
-
const SMALL_IMAGE_THRESHOLD = 100;
|
|
69
|
-
/** 长图宽高比阈值 */
|
|
70
|
-
const LONG_IMAGE_RATIO = 2;
|
|
71
|
-
/** 默认文件大小限制(MB) */
|
|
72
|
-
const DEFAULT_FILE_SIZE_LIMIT = 10;
|
|
73
|
-
/** 默认压缩质量 */
|
|
74
|
-
const DEFAULT_QUALITY = 0.7;
|
|
75
|
-
/** 预签名URL过期时间(毫秒) */
|
|
76
|
-
const PRESIGNED_URL_EXPIRE_TIME = 60000;
|
|
77
|
-
// ==================== 枚举定义 ====================
|
|
78
|
-
/**
|
|
79
|
-
* 业务类型枚举
|
|
80
|
-
*/
|
|
81
|
-
export var BusinessType;
|
|
82
|
-
(function (BusinessType) {
|
|
83
|
-
/** 客户端上传图片(跟用户相关) */
|
|
84
|
-
/** 隐私图片 */
|
|
85
|
-
BusinessType[BusinessType["CREDIT"] = 130541736] = "CREDIT";
|
|
86
|
-
/** 内容 */
|
|
87
|
-
BusinessType[BusinessType["CONTENT"] = 101300784] = "CONTENT";
|
|
88
|
-
/** 头像 */
|
|
89
|
-
BusinessType[BusinessType["AVATAR"] = 134539448] = "AVATAR";
|
|
90
|
-
/** 二手车 */
|
|
91
|
-
BusinessType[BusinessType["USEDCAR"] = 171978237] = "USEDCAR";
|
|
92
|
-
/** 商城 */
|
|
93
|
-
BusinessType[BusinessType["MALL"] = 115259415] = "MALL";
|
|
94
|
-
/** 魔友圈 */
|
|
95
|
-
BusinessType[BusinessType["HOOP"] = 155669648] = "HOOP";
|
|
96
|
-
/** 用户 */
|
|
97
|
-
BusinessType[BusinessType["USER"] = 170244368] = "USER";
|
|
98
|
-
/** 骑行 */
|
|
99
|
-
BusinessType[BusinessType["CYCLING"] = 165103952] = "CYCLING";
|
|
100
|
-
/** 玩车 */
|
|
101
|
-
BusinessType[BusinessType["MOTORCYCLE"] = 151637735] = "MOTORCYCLE";
|
|
102
|
-
/** 租车 */
|
|
103
|
-
BusinessType[BusinessType["RENTALCAR"] = 149095014] = "RENTALCAR";
|
|
104
|
-
/** 经销商 */
|
|
105
|
-
BusinessType[BusinessType["SHOP"] = 172655993] = "SHOP";
|
|
106
|
-
/** 财务 */
|
|
107
|
-
BusinessType[BusinessType["FINANCE"] = 188717541] = "FINANCE";
|
|
108
|
-
/** 反馈 */
|
|
109
|
-
BusinessType[BusinessType["FEEDBACK"] = 182411383] = "FEEDBACK";
|
|
110
|
-
/** 驾校 */
|
|
111
|
-
BusinessType[BusinessType["DRIVINGSCHOOL"] = 172137757] = "DRIVINGSCHOOL";
|
|
112
|
-
/** 评论图片 */
|
|
113
|
-
BusinessType[BusinessType["REPLY"] = 195929698] = "REPLY";
|
|
114
|
-
/** 厂家 */
|
|
115
|
-
BusinessType[BusinessType["FACTORY"] = 118733601] = "FACTORY";
|
|
116
|
-
/** 骑行数据 */
|
|
117
|
-
BusinessType[BusinessType["CYCLINGDATA"] = 148681294] = "CYCLINGDATA";
|
|
118
|
-
/** 动图 */
|
|
119
|
-
BusinessType[BusinessType["GIF"] = 191290831] = "GIF";
|
|
120
|
-
/** 其他(原则上不可以使用) */
|
|
121
|
-
BusinessType[BusinessType["OTHER"] = 100694193] = "OTHER";
|
|
122
|
-
/** 运营文件(跟用户无关) */
|
|
123
|
-
/** apk */
|
|
124
|
-
BusinessType[BusinessType["APK"] = 154033062] = "APK";
|
|
125
|
-
/** 证书 */
|
|
126
|
-
BusinessType[BusinessType["CERT"] = 142668067] = "CERT";
|
|
127
|
-
/** oss数据文件(xlxs等) */
|
|
128
|
-
BusinessType[BusinessType["OSSDATA"] = 105038102] = "OSSDATA";
|
|
129
|
-
/** 数据(题库等) */
|
|
130
|
-
BusinessType[BusinessType["DB"] = 180241065] = "DB";
|
|
131
|
-
/** PDF */
|
|
132
|
-
BusinessType[BusinessType["PDF"] = 198157532] = "PDF";
|
|
133
|
-
/** 视频 */
|
|
134
|
-
BusinessType[BusinessType["VIDEOOPS"] = 172023381] = "VIDEOOPS";
|
|
135
|
-
/** 运营图片(跟用户无关) */
|
|
136
|
-
/** 商城运营图片 */
|
|
137
|
-
BusinessType[BusinessType["MALLOPS"] = 175705586] = "MALLOPS";
|
|
138
|
-
/** 运营图片 */
|
|
139
|
-
BusinessType[BusinessType["OSSOPS"] = 157532694] = "OSSOPS";
|
|
140
|
-
/** 车库运营图片 */
|
|
141
|
-
BusinessType[BusinessType["CARPORT"] = 137563987] = "CARPORT";
|
|
142
|
-
})(BusinessType || (BusinessType = {}));
|
|
143
|
-
/**
|
|
144
|
-
* bucket 图片类型后缀枚举
|
|
145
|
-
*/
|
|
146
|
-
export const SuffixEnum = {
|
|
147
|
-
nowater: '!nowater',
|
|
148
|
-
official: '!official',
|
|
149
|
-
panoram: '!panoram',
|
|
150
|
-
forum: '!forum',
|
|
151
|
-
avatar: '!avatar',
|
|
152
|
-
square: '!square',
|
|
153
|
-
carport: '!carport', // 车库、经销商水印
|
|
154
|
-
};
|
|
155
|
-
// ==================== 工具函数 ====================
|
|
156
|
-
/**
|
|
157
|
-
* 检查文件类型是否支持
|
|
158
|
-
* @param fileType 文件类型
|
|
159
|
-
* @returns 是否支持
|
|
160
|
-
*/
|
|
161
|
-
function isSupportedImageType(fileType) {
|
|
162
|
-
return SUPPORTED_IMAGE_TYPES.includes(fileType);
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* 计算文件大小(KB)
|
|
166
|
-
* @param size 文件大小(字节)
|
|
167
|
-
* @returns 文件大小(KB)
|
|
168
|
-
*/
|
|
169
|
-
function getFileSizeInKB(size) {
|
|
170
|
-
return Math.floor(size / 1024);
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* 计算文件大小(MB)
|
|
174
|
-
* @param size 文件大小(字节)
|
|
175
|
-
* @returns 文件大小(MB)
|
|
176
|
-
*/
|
|
177
|
-
function getFileSizeInMB(size) {
|
|
178
|
-
return size / (1024 * 1024);
|
|
179
|
-
}
|
|
180
|
-
// ==================== 主类 ====================
|
|
181
|
-
/**
|
|
182
|
-
* 阿里云OSS上传类
|
|
183
|
-
* 整合所有项目的aliOss业务逻辑,通过依赖注入实现业务API解耦
|
|
184
|
-
*/
|
|
185
|
-
export class AliOssClass {
|
|
186
|
-
static instance = null;
|
|
187
|
-
apiConfig;
|
|
188
|
-
constructor(apiConfig) {
|
|
189
|
-
this.apiConfig = apiConfig;
|
|
190
|
-
}
|
|
191
|
-
static getInstance(apiConfig) {
|
|
192
|
-
if (!AliOssClass.instance) {
|
|
193
|
-
AliOssClass.instance = new AliOssClass(apiConfig);
|
|
194
|
-
}
|
|
195
|
-
return AliOssClass.instance;
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* 判断文件是否为图片(支持常见图片格式)
|
|
199
|
-
* @param {File} file - 上传的File对象
|
|
200
|
-
* @returns {boolean} 是否为图片
|
|
201
|
-
*/
|
|
202
|
-
isImageFile(file) {
|
|
203
|
-
if (!file || !(file instanceof File))
|
|
204
|
-
return false;
|
|
205
|
-
// 1. 校验文件MIME类型(优先,更可靠)
|
|
206
|
-
if (SUPPORTED_IMAGE_TYPES.includes(file.type))
|
|
207
|
-
return true;
|
|
208
|
-
// 2. 校验文件后缀名(兜底,防止MIME类型缺失)
|
|
209
|
-
const fileExt = file.name.split('.').pop()?.toLowerCase() || '';
|
|
210
|
-
return SUPPORTED_IMAGE_EXTS.includes(fileExt);
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* 通过V4签名上传
|
|
214
|
-
* @param file 上传的文件
|
|
215
|
-
* @param businessType 业务类型
|
|
216
|
-
* @param size 文件的尺寸
|
|
217
|
-
* @param resolve Promse
|
|
218
|
-
* @param reject Promse
|
|
219
|
-
* @returns 上传图片结果
|
|
220
|
-
*/
|
|
221
|
-
async upload(params) {
|
|
222
|
-
const { file, businessType, resolve, reject, callbacks } = params;
|
|
223
|
-
let { size } = params;
|
|
224
|
-
let result = {};
|
|
225
|
-
try {
|
|
226
|
-
if (!size && this.isImageFile(file)) {
|
|
227
|
-
const image = await this.getImageInfo(file);
|
|
228
|
-
size = `_${image?.width}_${image?.height}`;
|
|
229
|
-
}
|
|
230
|
-
// 步骤 1:请求后端获取所有签名字段
|
|
231
|
-
const signResponse = await this.apiConfig.getSts({
|
|
232
|
-
businessType: businessType,
|
|
233
|
-
docType: file.type,
|
|
234
|
-
size: size, // _1024_567(可选)
|
|
235
|
-
});
|
|
236
|
-
if (signResponse.data.code !== 0) {
|
|
237
|
-
const errorMessage = `获取签名失败:${signResponse.data.message}`;
|
|
238
|
-
callbacks?.onError(errorMessage);
|
|
239
|
-
reject && reject(errorMessage);
|
|
240
|
-
console.error(errorMessage);
|
|
241
|
-
}
|
|
242
|
-
const signData = await signResponse.data.data;
|
|
243
|
-
// console.log('后端返回的签名字段:', signData)
|
|
244
|
-
// 步骤 2:构造 FormData(字段名必须和后端返回一致
|
|
245
|
-
const formData = new FormData();
|
|
246
|
-
// formData.append('autherid', signData['autherid'])
|
|
247
|
-
// formData.append('uid', signData['uid'])
|
|
248
|
-
formData.append('key', signData['key']);
|
|
249
|
-
formData.append('policy', signData['policy']);
|
|
250
|
-
formData.append('success_action_status', '200');
|
|
251
|
-
formData.append('x-oss-credential', signData['credential']);
|
|
252
|
-
formData.append('x-oss-date', signData['date']);
|
|
253
|
-
if (signData['securityToken']) {
|
|
254
|
-
formData.append('x-oss-security-token', signData['securityToken']);
|
|
255
|
-
}
|
|
256
|
-
formData.append('x-oss-signature', signData['signature']);
|
|
257
|
-
formData.append('x-oss-signature-version', signData['signatureVersion']);
|
|
258
|
-
formData.append('file', file);
|
|
259
|
-
// 步骤 3:发送 POST 请求到 OSS
|
|
260
|
-
const uploadResponse = await fetch(signData.uploadUrl, {
|
|
261
|
-
method: 'POST',
|
|
262
|
-
body: formData,
|
|
263
|
-
// 重点:不要手动设置 Content-Type!浏览器会自动处理为 multipart/form-data 并带边界符
|
|
264
|
-
headers: { Accept: '*/*' },
|
|
265
|
-
});
|
|
266
|
-
// 步骤 4:处理上传结果
|
|
267
|
-
const responseText = await uploadResponse.text();
|
|
268
|
-
if (uploadResponse.ok) {
|
|
269
|
-
result = {
|
|
270
|
-
url: signData['fileUrl'],
|
|
271
|
-
imgUrl: signData['fileUrl'],
|
|
272
|
-
imgOrgUrl: signData['fileUrl'],
|
|
273
|
-
name: file.name,
|
|
274
|
-
fileName: file.name,
|
|
275
|
-
};
|
|
276
|
-
callbacks?.onSuccess(result);
|
|
277
|
-
resolve && resolve(result);
|
|
278
|
-
}
|
|
279
|
-
else {
|
|
280
|
-
const errorMessage = `上传失败(OSS 返回 ${uploadResponse.status}):${responseText}`;
|
|
281
|
-
callbacks?.onError(errorMessage);
|
|
282
|
-
reject && reject(errorMessage);
|
|
283
|
-
console.error(errorMessage);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
catch (err) {
|
|
287
|
-
callbacks?.onError(err.message);
|
|
288
|
-
reject && reject(err.message);
|
|
289
|
-
console.error('上传异常:', err.message);
|
|
290
|
-
}
|
|
291
|
-
return result;
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* 预加载图片获取尺寸
|
|
295
|
-
* @param file 文件对象
|
|
296
|
-
* @returns 图片对象
|
|
297
|
-
*/
|
|
298
|
-
getImageInfo(file) {
|
|
299
|
-
return new Promise((resolve, reject) => {
|
|
300
|
-
const URL = window.URL || window.webkitURL;
|
|
301
|
-
const image = new Image();
|
|
302
|
-
const objectUrl = URL.createObjectURL(file);
|
|
303
|
-
image.onload = () => {
|
|
304
|
-
URL.revokeObjectURL(objectUrl); // 释放内存
|
|
305
|
-
resolve(image);
|
|
306
|
-
};
|
|
307
|
-
image.onerror = () => {
|
|
308
|
-
URL.revokeObjectURL(objectUrl); // 释放内存
|
|
309
|
-
reject(new Error('图片加载失败'));
|
|
310
|
-
};
|
|
311
|
-
image.src = objectUrl;
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* 检查图片是否为长图
|
|
316
|
-
* @param File 图片
|
|
317
|
-
* @returns 是否为长图
|
|
318
|
-
*/
|
|
319
|
-
async isLongImage(file) {
|
|
320
|
-
// 预加载图片获取尺寸
|
|
321
|
-
const image = await this.getImageInfo(file);
|
|
322
|
-
const { width, height } = image;
|
|
323
|
-
const maxDimension = Math.max(width, height);
|
|
324
|
-
const minDimension = Math.min(width, height);
|
|
325
|
-
return maxDimension / minDimension > LONG_IMAGE_RATIO;
|
|
326
|
-
}
|
|
327
|
-
/**
|
|
328
|
-
* 加载图片并返回结果
|
|
329
|
-
* 用于标准的图片上传场景
|
|
330
|
-
* @param params 加载参数
|
|
331
|
-
*/
|
|
332
|
-
loadImage(params) {
|
|
333
|
-
const { url, val, file, option, resolve, reject } = params;
|
|
334
|
-
let img = new Image();
|
|
335
|
-
img.src = url;
|
|
336
|
-
img.onload = function () {
|
|
337
|
-
if (!img)
|
|
338
|
-
return;
|
|
339
|
-
val.imgOrgUrl = `${url}?_${img.width}_${img.height}`;
|
|
340
|
-
val.imgUrl = val.imgOrgUrl;
|
|
341
|
-
val.name = file.name;
|
|
342
|
-
val.fileName = file.name;
|
|
343
|
-
option.onSuccess?.(val);
|
|
344
|
-
resolve(val);
|
|
345
|
-
img = null;
|
|
346
|
-
};
|
|
347
|
-
img.onerror = function () {
|
|
348
|
-
option.onError?.('图片加载失败');
|
|
349
|
-
reject('图片加载失败');
|
|
350
|
-
img = null;
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* 图片上传主方法
|
|
355
|
-
* 整合了所有项目的图片上传逻辑
|
|
356
|
-
* @param option 上传选项
|
|
357
|
-
* @returns Promise
|
|
358
|
-
*/
|
|
359
|
-
ossUploadImage = async (option) => {
|
|
360
|
-
if (!(option.file instanceof File)) {
|
|
361
|
-
return Promise.reject('file is not instanceof File');
|
|
362
|
-
}
|
|
363
|
-
if (!option.businessType) {
|
|
364
|
-
return Promise.reject('businessType不能为空');
|
|
365
|
-
}
|
|
366
|
-
const file = option.file;
|
|
367
|
-
// 初始化回调函数
|
|
368
|
-
const callbacks = {
|
|
369
|
-
onError: option.onError || (() => { }),
|
|
370
|
-
onSuccess: option.onSuccess || (() => { }),
|
|
371
|
-
onProgress: option.onProgress || (() => { }),
|
|
372
|
-
};
|
|
373
|
-
option.onError = callbacks.onError;
|
|
374
|
-
option.onSuccess = callbacks.onSuccess;
|
|
375
|
-
option.onProgress = callbacks.onProgress;
|
|
376
|
-
// 文件类型验证
|
|
377
|
-
if (!isSupportedImageType(file.type)) {
|
|
378
|
-
callbacks.onError('');
|
|
379
|
-
this.apiConfig.messageWarning?.('上传失败,请上传后缀为png、gif、jpg的文件');
|
|
380
|
-
return Promise.reject('文件类型不支持');
|
|
381
|
-
}
|
|
382
|
-
return new Promise(async (resolve, reject) => {
|
|
383
|
-
try {
|
|
384
|
-
// 注释:小图片(≤100KB)不压缩,quality设为1 (MAIN-2481)
|
|
385
|
-
const isSmallImage = getFileSizeInKB(file.size) <= SMALL_IMAGE_THRESHOLD;
|
|
386
|
-
let quality = isSmallImage ? 1 : option.quality || DEFAULT_QUALITY;
|
|
387
|
-
const businessType = option.businessType;
|
|
388
|
-
if (businessType === BusinessType.CREDIT) {
|
|
389
|
-
return await this.upload({
|
|
390
|
-
file,
|
|
391
|
-
businessType,
|
|
392
|
-
resolve,
|
|
393
|
-
reject,
|
|
394
|
-
callbacks,
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
// 注释:batchTransfer选项 - 批量转换水印(用于某些特殊业务场景)
|
|
398
|
-
if (option.batchTransfer && this.apiConfig.multiTransferImage) {
|
|
399
|
-
const res = await this.apiConfig.multiTransferImage({ file });
|
|
400
|
-
if (res.data.code === 0) {
|
|
401
|
-
// 盲水印兼容报错
|
|
402
|
-
if (res.data.data) {
|
|
403
|
-
this.loadImage({
|
|
404
|
-
url: res.data.data,
|
|
405
|
-
val: res.data,
|
|
406
|
-
option,
|
|
407
|
-
file,
|
|
408
|
-
resolve,
|
|
409
|
-
reject,
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
else {
|
|
414
|
-
callbacks.onError('上传失败');
|
|
415
|
-
reject('上传失败');
|
|
416
|
-
}
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
// 注释:carport类型需要打暗水印
|
|
420
|
-
if (option.imageType === 'carport' &&
|
|
421
|
-
this.apiConfig.darkWaterUploadImage) {
|
|
422
|
-
const res = await this.apiConfig.darkWaterUploadImage({ file });
|
|
423
|
-
if (res.data.code === 0) {
|
|
424
|
-
this.loadImage({
|
|
425
|
-
url: res.data.data,
|
|
426
|
-
val: res.data,
|
|
427
|
-
option,
|
|
428
|
-
file,
|
|
429
|
-
resolve,
|
|
430
|
-
reject,
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
else {
|
|
434
|
-
callbacks.onError('上传失败');
|
|
435
|
-
reject('上传失败');
|
|
436
|
-
}
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
// 图片压缩
|
|
440
|
-
const rst = await lrz(file, { quality });
|
|
441
|
-
// 注释:长图压缩有问题,长宽比>2:1的图片不压缩
|
|
442
|
-
const isLong = (await this.isLongImage(file)) || false;
|
|
443
|
-
// 确定最终上传的文件
|
|
444
|
-
let postFile;
|
|
445
|
-
if (option.notCompress || file.type === 'image/gif' || isLong) {
|
|
446
|
-
// 注释:GIF、notCompress选项或长图不压缩
|
|
447
|
-
postFile = file;
|
|
448
|
-
}
|
|
449
|
-
else {
|
|
450
|
-
postFile = rst.file;
|
|
451
|
-
}
|
|
452
|
-
// 文件大小验证
|
|
453
|
-
const imgSize = postFile.size;
|
|
454
|
-
const maxSizeMB = DEFAULT_FILE_SIZE_LIMIT;
|
|
455
|
-
if (getFileSizeInMB(imgSize) > maxSizeMB) {
|
|
456
|
-
const errorMsg = `图片不能超过${maxSizeMB}M`;
|
|
457
|
-
callbacks.onError(errorMsg);
|
|
458
|
-
reject(errorMsg);
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
// 设置文件名(确保压缩后的文件有名称)
|
|
462
|
-
if (!postFile.name) {
|
|
463
|
-
Object.defineProperty(postFile, 'name', {
|
|
464
|
-
value: file.name,
|
|
465
|
-
writable: false,
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
const result = await this.upload({
|
|
469
|
-
file,
|
|
470
|
-
businessType,
|
|
471
|
-
resolve,
|
|
472
|
-
reject,
|
|
473
|
-
callbacks,
|
|
474
|
-
});
|
|
475
|
-
// 注释:idCard场景 - 生成预签名URL
|
|
476
|
-
if ((option.idCard || false) &&
|
|
477
|
-
this.apiConfig.generatePrePresignedUrl) {
|
|
478
|
-
const res = await this.apiConfig.generatePrePresignedUrl({
|
|
479
|
-
objectId: result.name,
|
|
480
|
-
expireMils: PRESIGNED_URL_EXPIRE_TIME,
|
|
481
|
-
});
|
|
482
|
-
if (res.data.code === 0) {
|
|
483
|
-
this.loadImage({
|
|
484
|
-
url: res.data.data,
|
|
485
|
-
val: result,
|
|
486
|
-
file: postFile,
|
|
487
|
-
option,
|
|
488
|
-
resolve,
|
|
489
|
-
reject,
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
else {
|
|
493
|
-
callbacks.onError('生成预签名URL失败');
|
|
494
|
-
reject('生成预签名URL失败');
|
|
495
|
-
}
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
catch (error) {
|
|
500
|
-
const errorMsg = error.message || '上传异常';
|
|
501
|
-
callbacks.onError(errorMsg);
|
|
502
|
-
reject(errorMsg);
|
|
503
|
-
}
|
|
504
|
-
});
|
|
505
|
-
};
|
|
506
|
-
/**
|
|
507
|
-
* 文件上传方法
|
|
508
|
-
* 支持视频、文档、APK等文件类型
|
|
509
|
-
* @param option 上传选项
|
|
510
|
-
* @returns Promise
|
|
511
|
-
*/
|
|
512
|
-
ossUploadFile = async (option) => {
|
|
513
|
-
if (!(option.file instanceof File)) {
|
|
514
|
-
return Promise.reject('file is not instanceof File');
|
|
515
|
-
}
|
|
516
|
-
if (!option.businessType) {
|
|
517
|
-
return Promise.reject('businessType不能为空');
|
|
518
|
-
}
|
|
519
|
-
const file = option.file;
|
|
520
|
-
// 初始化回调函数
|
|
521
|
-
const callbacks = {
|
|
522
|
-
onError: option.onError || (() => { }),
|
|
523
|
-
onSuccess: option.onSuccess || (() => { }),
|
|
524
|
-
onProgress: option.onProgress || (() => { }),
|
|
525
|
-
};
|
|
526
|
-
option.onError = callbacks.onError;
|
|
527
|
-
option.onSuccess = callbacks.onSuccess;
|
|
528
|
-
option.onProgress = callbacks.onProgress;
|
|
529
|
-
return new Promise(async (resolve, reject) => {
|
|
530
|
-
try {
|
|
531
|
-
let businessType = option.businessType;
|
|
532
|
-
await this.upload({
|
|
533
|
-
file,
|
|
534
|
-
businessType,
|
|
535
|
-
resolve,
|
|
536
|
-
reject,
|
|
537
|
-
callbacks,
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
catch (error) {
|
|
541
|
-
const errorMsg = error.message || '上传异常';
|
|
542
|
-
callbacks.onError(errorMsg);
|
|
543
|
-
reject(errorMsg);
|
|
544
|
-
}
|
|
545
|
-
});
|
|
546
|
-
};
|
|
547
|
-
/**
|
|
548
|
-
* 纯图片上传 pureOssUploadImage
|
|
549
|
-
* 注释:图片上传,不压缩
|
|
550
|
-
* @param option 上传选项
|
|
551
|
-
* @returns Promise
|
|
552
|
-
*/
|
|
553
|
-
pureOssUploadImage = async (option) => {
|
|
554
|
-
return await this.shopDetailUpdate(option);
|
|
555
|
-
};
|
|
556
|
-
/**
|
|
557
|
-
* 商品详情图片上传(不建议直接使用,请使用 pureOssUploadImage)
|
|
558
|
-
* 注释:图片上传,不压缩
|
|
559
|
-
* @param option 上传选项
|
|
560
|
-
* @returns Promise
|
|
561
|
-
*/
|
|
562
|
-
shopDetailUpdate = async (option) => {
|
|
563
|
-
if (!(option.file instanceof File)) {
|
|
564
|
-
return Promise.reject('file is not instanceof File');
|
|
565
|
-
}
|
|
566
|
-
if (!option.businessType) {
|
|
567
|
-
return Promise.reject('businessType不能为空');
|
|
568
|
-
}
|
|
569
|
-
const file = option.file;
|
|
570
|
-
// 初始化回调函数
|
|
571
|
-
const callbacks = {
|
|
572
|
-
onError: option.onError || (() => { }),
|
|
573
|
-
onSuccess: option.onSuccess || (() => { }),
|
|
574
|
-
onProgress: option.onProgress || (() => { }),
|
|
575
|
-
};
|
|
576
|
-
option.onError = callbacks.onError;
|
|
577
|
-
option.onSuccess = callbacks.onSuccess;
|
|
578
|
-
option.onProgress = callbacks.onProgress;
|
|
579
|
-
// 文件类型验证
|
|
580
|
-
if (!isSupportedImageType(file.type)) {
|
|
581
|
-
callbacks.onError('');
|
|
582
|
-
this.apiConfig.messageWarning?.('上传失败,请上传后缀为png、gif、jpg的文件');
|
|
583
|
-
return Promise.reject('文件类型不支持');
|
|
584
|
-
}
|
|
585
|
-
return new Promise(async (resolve, reject) => {
|
|
586
|
-
try {
|
|
587
|
-
const businessType = option.businessType;
|
|
588
|
-
// 文件大小验证
|
|
589
|
-
if (getFileSizeInMB(file.size) > DEFAULT_FILE_SIZE_LIMIT) {
|
|
590
|
-
const errorMsg = `图片不能超过${DEFAULT_FILE_SIZE_LIMIT}M`;
|
|
591
|
-
callbacks.onError(errorMsg);
|
|
592
|
-
reject(errorMsg);
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
await this.upload({
|
|
596
|
-
file,
|
|
597
|
-
businessType,
|
|
598
|
-
resolve,
|
|
599
|
-
reject,
|
|
600
|
-
callbacks,
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
catch (error) {
|
|
604
|
-
const errorMsg = error.message || '上传异常';
|
|
605
|
-
callbacks.onError(errorMsg);
|
|
606
|
-
reject(errorMsg);
|
|
607
|
-
}
|
|
608
|
-
});
|
|
609
|
-
};
|
|
610
|
-
/**
|
|
611
|
-
* 业务类型枚举
|
|
612
|
-
*/
|
|
613
|
-
businessType = BusinessType;
|
|
614
|
-
}
|
|
615
|
-
/**
|
|
616
|
-
* 创建阿里云OSS上传器(工厂函数,内部已实现单例)
|
|
617
|
-
* @param apiConfig API配置对象,包含业务相关的API函数
|
|
618
|
-
* @returns OSS上传器实例
|
|
619
|
-
*/
|
|
620
|
-
export function createAliOssUploader(apiConfig) {
|
|
621
|
-
return AliOssClass.getInstance(apiConfig);
|
|
622
|
-
}
|
|
623
|
-
/**
|
|
624
|
-
* 默认导出
|
|
625
|
-
*/
|
|
626
|
-
export default {
|
|
627
|
-
createAliOssUploader,
|
|
628
|
-
BusinessType,
|
|
629
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* @file 阿里云OSS上传统一工具类 v4
|
|
3
|
+
* @description 整合所有项目的aliOss业务逻辑,通过传参形式支持不同业务场景
|
|
4
|
+
* @Author: wanghui
|
|
5
|
+
* @createBy: @2025.11.17
|
|
6
|
+
*
|
|
7
|
+
* 使用示例:
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createAliOssUploader } from '@haluo/util'
|
|
10
|
+
*
|
|
11
|
+
* // 创建上传实例,传入业务相关的API函数
|
|
12
|
+
* const ossUploader = createAliOssUploader({
|
|
13
|
+
* getSts: (params) => api.getSts(params),
|
|
14
|
+
* darkWaterUploadImage: (params) => api.darkWaterUploadImage(params),
|
|
15
|
+
* multiTransferImage: (params) => api.multiTransferImage(params),
|
|
16
|
+
* generatePrePresignedUrl: (params) => api.generatePrePresignedUrl(params),
|
|
17
|
+
* messageWarning: (msg) => window.$message.warning(msg)
|
|
18
|
+
* })
|
|
19
|
+
*
|
|
20
|
+
* // 图片上传
|
|
21
|
+
* ossUploader.ossUploadImage({
|
|
22
|
+
* file: file,
|
|
23
|
+
* businessType: BusinessType.CONTENT, // 业务类型 BusinessType
|
|
24
|
+
* imageType: 'official', // nowater、official、panoram、forum、avatar、square、carport
|
|
25
|
+
* quality: 0.7, // 压缩质量
|
|
26
|
+
* batchTransfer: false, // 是否批量转换水印
|
|
27
|
+
* notCompress: false, // 是否不压缩
|
|
28
|
+
* idCard: false, // 是否身份证上传
|
|
29
|
+
* onProgress: (e) => console.log(e.percent),
|
|
30
|
+
* onSuccess: (val) => console.log(val),
|
|
31
|
+
* onError: (err) => console.error(err)
|
|
32
|
+
* })
|
|
33
|
+
*
|
|
34
|
+
* // 文件上传
|
|
35
|
+
* ossUploader.ossUploadFile({
|
|
36
|
+
* file: file,
|
|
37
|
+
* businessType: BusinessType.CONTENT, // 业务类型 BusinessType
|
|
38
|
+
* onProgress: (e) => console.log(e.percent),
|
|
39
|
+
* onSuccess: (val) => console.log(val),
|
|
40
|
+
* onError: (err) => console.error(err)
|
|
41
|
+
* })
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
import lrz from 'lrz';
|
|
45
|
+
// ==================== 常量定义 ====================
|
|
46
|
+
/** 支持的图片格式 */
|
|
47
|
+
const SUPPORTED_IMAGE_TYPES = [
|
|
48
|
+
'image/jpeg',
|
|
49
|
+
'image/png',
|
|
50
|
+
'image/gif',
|
|
51
|
+
'image/bmp',
|
|
52
|
+
'image/webp',
|
|
53
|
+
'image/svg+xml',
|
|
54
|
+
'image/tiff',
|
|
55
|
+
];
|
|
56
|
+
/** 支持的图片格式 ext */
|
|
57
|
+
const SUPPORTED_IMAGE_EXTS = [
|
|
58
|
+
'jpg',
|
|
59
|
+
'jpeg',
|
|
60
|
+
'png',
|
|
61
|
+
'gif',
|
|
62
|
+
'bmp',
|
|
63
|
+
'webp',
|
|
64
|
+
'svg',
|
|
65
|
+
'tiff',
|
|
66
|
+
];
|
|
67
|
+
/** 小图片阈值(KB) */
|
|
68
|
+
const SMALL_IMAGE_THRESHOLD = 100;
|
|
69
|
+
/** 长图宽高比阈值 */
|
|
70
|
+
const LONG_IMAGE_RATIO = 2;
|
|
71
|
+
/** 默认文件大小限制(MB) */
|
|
72
|
+
const DEFAULT_FILE_SIZE_LIMIT = 10;
|
|
73
|
+
/** 默认压缩质量 */
|
|
74
|
+
const DEFAULT_QUALITY = 0.7;
|
|
75
|
+
/** 预签名URL过期时间(毫秒) */
|
|
76
|
+
const PRESIGNED_URL_EXPIRE_TIME = 60000;
|
|
77
|
+
// ==================== 枚举定义 ====================
|
|
78
|
+
/**
|
|
79
|
+
* 业务类型枚举
|
|
80
|
+
*/
|
|
81
|
+
export var BusinessType;
|
|
82
|
+
(function (BusinessType) {
|
|
83
|
+
/** 客户端上传图片(跟用户相关) */
|
|
84
|
+
/** 隐私图片 */
|
|
85
|
+
BusinessType[BusinessType["CREDIT"] = 130541736] = "CREDIT";
|
|
86
|
+
/** 内容 */
|
|
87
|
+
BusinessType[BusinessType["CONTENT"] = 101300784] = "CONTENT";
|
|
88
|
+
/** 头像 */
|
|
89
|
+
BusinessType[BusinessType["AVATAR"] = 134539448] = "AVATAR";
|
|
90
|
+
/** 二手车 */
|
|
91
|
+
BusinessType[BusinessType["USEDCAR"] = 171978237] = "USEDCAR";
|
|
92
|
+
/** 商城 */
|
|
93
|
+
BusinessType[BusinessType["MALL"] = 115259415] = "MALL";
|
|
94
|
+
/** 魔友圈 */
|
|
95
|
+
BusinessType[BusinessType["HOOP"] = 155669648] = "HOOP";
|
|
96
|
+
/** 用户 */
|
|
97
|
+
BusinessType[BusinessType["USER"] = 170244368] = "USER";
|
|
98
|
+
/** 骑行 */
|
|
99
|
+
BusinessType[BusinessType["CYCLING"] = 165103952] = "CYCLING";
|
|
100
|
+
/** 玩车 */
|
|
101
|
+
BusinessType[BusinessType["MOTORCYCLE"] = 151637735] = "MOTORCYCLE";
|
|
102
|
+
/** 租车 */
|
|
103
|
+
BusinessType[BusinessType["RENTALCAR"] = 149095014] = "RENTALCAR";
|
|
104
|
+
/** 经销商 */
|
|
105
|
+
BusinessType[BusinessType["SHOP"] = 172655993] = "SHOP";
|
|
106
|
+
/** 财务 */
|
|
107
|
+
BusinessType[BusinessType["FINANCE"] = 188717541] = "FINANCE";
|
|
108
|
+
/** 反馈 */
|
|
109
|
+
BusinessType[BusinessType["FEEDBACK"] = 182411383] = "FEEDBACK";
|
|
110
|
+
/** 驾校 */
|
|
111
|
+
BusinessType[BusinessType["DRIVINGSCHOOL"] = 172137757] = "DRIVINGSCHOOL";
|
|
112
|
+
/** 评论图片 */
|
|
113
|
+
BusinessType[BusinessType["REPLY"] = 195929698] = "REPLY";
|
|
114
|
+
/** 厂家 */
|
|
115
|
+
BusinessType[BusinessType["FACTORY"] = 118733601] = "FACTORY";
|
|
116
|
+
/** 骑行数据 */
|
|
117
|
+
BusinessType[BusinessType["CYCLINGDATA"] = 148681294] = "CYCLINGDATA";
|
|
118
|
+
/** 动图 */
|
|
119
|
+
BusinessType[BusinessType["GIF"] = 191290831] = "GIF";
|
|
120
|
+
/** 其他(原则上不可以使用) */
|
|
121
|
+
BusinessType[BusinessType["OTHER"] = 100694193] = "OTHER";
|
|
122
|
+
/** 运营文件(跟用户无关) */
|
|
123
|
+
/** apk */
|
|
124
|
+
BusinessType[BusinessType["APK"] = 154033062] = "APK";
|
|
125
|
+
/** 证书 */
|
|
126
|
+
BusinessType[BusinessType["CERT"] = 142668067] = "CERT";
|
|
127
|
+
/** oss数据文件(xlxs等) */
|
|
128
|
+
BusinessType[BusinessType["OSSDATA"] = 105038102] = "OSSDATA";
|
|
129
|
+
/** 数据(题库等) */
|
|
130
|
+
BusinessType[BusinessType["DB"] = 180241065] = "DB";
|
|
131
|
+
/** PDF */
|
|
132
|
+
BusinessType[BusinessType["PDF"] = 198157532] = "PDF";
|
|
133
|
+
/** 视频 */
|
|
134
|
+
BusinessType[BusinessType["VIDEOOPS"] = 172023381] = "VIDEOOPS";
|
|
135
|
+
/** 运营图片(跟用户无关) */
|
|
136
|
+
/** 商城运营图片 */
|
|
137
|
+
BusinessType[BusinessType["MALLOPS"] = 175705586] = "MALLOPS";
|
|
138
|
+
/** 运营图片 */
|
|
139
|
+
BusinessType[BusinessType["OSSOPS"] = 157532694] = "OSSOPS";
|
|
140
|
+
/** 车库运营图片 */
|
|
141
|
+
BusinessType[BusinessType["CARPORT"] = 137563987] = "CARPORT";
|
|
142
|
+
})(BusinessType || (BusinessType = {}));
|
|
143
|
+
/**
|
|
144
|
+
* bucket 图片类型后缀枚举
|
|
145
|
+
*/
|
|
146
|
+
export const SuffixEnum = {
|
|
147
|
+
nowater: '!nowater',
|
|
148
|
+
official: '!official',
|
|
149
|
+
panoram: '!panoram',
|
|
150
|
+
forum: '!forum',
|
|
151
|
+
avatar: '!avatar',
|
|
152
|
+
square: '!square',
|
|
153
|
+
carport: '!carport', // 车库、经销商水印
|
|
154
|
+
};
|
|
155
|
+
// ==================== 工具函数 ====================
|
|
156
|
+
/**
|
|
157
|
+
* 检查文件类型是否支持
|
|
158
|
+
* @param fileType 文件类型
|
|
159
|
+
* @returns 是否支持
|
|
160
|
+
*/
|
|
161
|
+
function isSupportedImageType(fileType) {
|
|
162
|
+
return SUPPORTED_IMAGE_TYPES.includes(fileType);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* 计算文件大小(KB)
|
|
166
|
+
* @param size 文件大小(字节)
|
|
167
|
+
* @returns 文件大小(KB)
|
|
168
|
+
*/
|
|
169
|
+
function getFileSizeInKB(size) {
|
|
170
|
+
return Math.floor(size / 1024);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* 计算文件大小(MB)
|
|
174
|
+
* @param size 文件大小(字节)
|
|
175
|
+
* @returns 文件大小(MB)
|
|
176
|
+
*/
|
|
177
|
+
function getFileSizeInMB(size) {
|
|
178
|
+
return size / (1024 * 1024);
|
|
179
|
+
}
|
|
180
|
+
// ==================== 主类 ====================
|
|
181
|
+
/**
|
|
182
|
+
* 阿里云OSS上传类
|
|
183
|
+
* 整合所有项目的aliOss业务逻辑,通过依赖注入实现业务API解耦
|
|
184
|
+
*/
|
|
185
|
+
export class AliOssClass {
|
|
186
|
+
static instance = null;
|
|
187
|
+
apiConfig;
|
|
188
|
+
constructor(apiConfig) {
|
|
189
|
+
this.apiConfig = apiConfig;
|
|
190
|
+
}
|
|
191
|
+
static getInstance(apiConfig) {
|
|
192
|
+
if (!AliOssClass.instance) {
|
|
193
|
+
AliOssClass.instance = new AliOssClass(apiConfig);
|
|
194
|
+
}
|
|
195
|
+
return AliOssClass.instance;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* 判断文件是否为图片(支持常见图片格式)
|
|
199
|
+
* @param {File} file - 上传的File对象
|
|
200
|
+
* @returns {boolean} 是否为图片
|
|
201
|
+
*/
|
|
202
|
+
isImageFile(file) {
|
|
203
|
+
if (!file || !(file instanceof File))
|
|
204
|
+
return false;
|
|
205
|
+
// 1. 校验文件MIME类型(优先,更可靠)
|
|
206
|
+
if (SUPPORTED_IMAGE_TYPES.includes(file.type))
|
|
207
|
+
return true;
|
|
208
|
+
// 2. 校验文件后缀名(兜底,防止MIME类型缺失)
|
|
209
|
+
const fileExt = file.name.split('.').pop()?.toLowerCase() || '';
|
|
210
|
+
return SUPPORTED_IMAGE_EXTS.includes(fileExt);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* 通过V4签名上传
|
|
214
|
+
* @param file 上传的文件
|
|
215
|
+
* @param businessType 业务类型
|
|
216
|
+
* @param size 文件的尺寸
|
|
217
|
+
* @param resolve Promse
|
|
218
|
+
* @param reject Promse
|
|
219
|
+
* @returns 上传图片结果
|
|
220
|
+
*/
|
|
221
|
+
async upload(params) {
|
|
222
|
+
const { file, businessType, resolve, reject, callbacks } = params;
|
|
223
|
+
let { size } = params;
|
|
224
|
+
let result = {};
|
|
225
|
+
try {
|
|
226
|
+
if (!size && this.isImageFile(file)) {
|
|
227
|
+
const image = await this.getImageInfo(file);
|
|
228
|
+
size = `_${image?.width}_${image?.height}`;
|
|
229
|
+
}
|
|
230
|
+
// 步骤 1:请求后端获取所有签名字段
|
|
231
|
+
const signResponse = await this.apiConfig.getSts({
|
|
232
|
+
businessType: businessType,
|
|
233
|
+
docType: file.type?.split('/')[1] || file.type,
|
|
234
|
+
size: size, // _1024_567(可选)
|
|
235
|
+
});
|
|
236
|
+
if (signResponse.data.code !== 0) {
|
|
237
|
+
const errorMessage = `获取签名失败:${signResponse.data.message}`;
|
|
238
|
+
callbacks?.onError(errorMessage);
|
|
239
|
+
reject && reject(errorMessage);
|
|
240
|
+
console.error(errorMessage);
|
|
241
|
+
}
|
|
242
|
+
const signData = await signResponse.data.data;
|
|
243
|
+
// console.log('后端返回的签名字段:', signData)
|
|
244
|
+
// 步骤 2:构造 FormData(字段名必须和后端返回一致
|
|
245
|
+
const formData = new FormData();
|
|
246
|
+
// formData.append('autherid', signData['autherid'])
|
|
247
|
+
// formData.append('uid', signData['uid'])
|
|
248
|
+
formData.append('key', signData['key']);
|
|
249
|
+
formData.append('policy', signData['policy']);
|
|
250
|
+
formData.append('success_action_status', '200');
|
|
251
|
+
formData.append('x-oss-credential', signData['credential']);
|
|
252
|
+
formData.append('x-oss-date', signData['date']);
|
|
253
|
+
if (signData['securityToken']) {
|
|
254
|
+
formData.append('x-oss-security-token', signData['securityToken']);
|
|
255
|
+
}
|
|
256
|
+
formData.append('x-oss-signature', signData['signature']);
|
|
257
|
+
formData.append('x-oss-signature-version', signData['signatureVersion']);
|
|
258
|
+
formData.append('file', file);
|
|
259
|
+
// 步骤 3:发送 POST 请求到 OSS
|
|
260
|
+
const uploadResponse = await fetch(signData.uploadUrl, {
|
|
261
|
+
method: 'POST',
|
|
262
|
+
body: formData,
|
|
263
|
+
// 重点:不要手动设置 Content-Type!浏览器会自动处理为 multipart/form-data 并带边界符
|
|
264
|
+
headers: { Accept: '*/*' },
|
|
265
|
+
});
|
|
266
|
+
// 步骤 4:处理上传结果
|
|
267
|
+
const responseText = await uploadResponse.text();
|
|
268
|
+
if (uploadResponse.ok) {
|
|
269
|
+
result = {
|
|
270
|
+
url: signData['fileUrl'],
|
|
271
|
+
imgUrl: signData['fileUrl'],
|
|
272
|
+
imgOrgUrl: signData['fileUrl'],
|
|
273
|
+
name: file.name,
|
|
274
|
+
fileName: file.name,
|
|
275
|
+
};
|
|
276
|
+
callbacks?.onSuccess(result);
|
|
277
|
+
resolve && resolve(result);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
const errorMessage = `上传失败(OSS 返回 ${uploadResponse.status}):${responseText}`;
|
|
281
|
+
callbacks?.onError(errorMessage);
|
|
282
|
+
reject && reject(errorMessage);
|
|
283
|
+
console.error(errorMessage);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
callbacks?.onError(err.message);
|
|
288
|
+
reject && reject(err.message);
|
|
289
|
+
console.error('上传异常:', err.message);
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* 预加载图片获取尺寸
|
|
295
|
+
* @param file 文件对象
|
|
296
|
+
* @returns 图片对象
|
|
297
|
+
*/
|
|
298
|
+
getImageInfo(file) {
|
|
299
|
+
return new Promise((resolve, reject) => {
|
|
300
|
+
const URL = window.URL || window.webkitURL;
|
|
301
|
+
const image = new Image();
|
|
302
|
+
const objectUrl = URL.createObjectURL(file);
|
|
303
|
+
image.onload = () => {
|
|
304
|
+
URL.revokeObjectURL(objectUrl); // 释放内存
|
|
305
|
+
resolve(image);
|
|
306
|
+
};
|
|
307
|
+
image.onerror = () => {
|
|
308
|
+
URL.revokeObjectURL(objectUrl); // 释放内存
|
|
309
|
+
reject(new Error('图片加载失败'));
|
|
310
|
+
};
|
|
311
|
+
image.src = objectUrl;
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* 检查图片是否为长图
|
|
316
|
+
* @param File 图片
|
|
317
|
+
* @returns 是否为长图
|
|
318
|
+
*/
|
|
319
|
+
async isLongImage(file) {
|
|
320
|
+
// 预加载图片获取尺寸
|
|
321
|
+
const image = await this.getImageInfo(file);
|
|
322
|
+
const { width, height } = image;
|
|
323
|
+
const maxDimension = Math.max(width, height);
|
|
324
|
+
const minDimension = Math.min(width, height);
|
|
325
|
+
return maxDimension / minDimension > LONG_IMAGE_RATIO;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* 加载图片并返回结果
|
|
329
|
+
* 用于标准的图片上传场景
|
|
330
|
+
* @param params 加载参数
|
|
331
|
+
*/
|
|
332
|
+
loadImage(params) {
|
|
333
|
+
const { url, val, file, option, resolve, reject } = params;
|
|
334
|
+
let img = new Image();
|
|
335
|
+
img.src = url;
|
|
336
|
+
img.onload = function () {
|
|
337
|
+
if (!img)
|
|
338
|
+
return;
|
|
339
|
+
val.imgOrgUrl = `${url}?_${img.width}_${img.height}`;
|
|
340
|
+
val.imgUrl = val.imgOrgUrl;
|
|
341
|
+
val.name = file.name;
|
|
342
|
+
val.fileName = file.name;
|
|
343
|
+
option.onSuccess?.(val);
|
|
344
|
+
resolve(val);
|
|
345
|
+
img = null;
|
|
346
|
+
};
|
|
347
|
+
img.onerror = function () {
|
|
348
|
+
option.onError?.('图片加载失败');
|
|
349
|
+
reject('图片加载失败');
|
|
350
|
+
img = null;
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* 图片上传主方法
|
|
355
|
+
* 整合了所有项目的图片上传逻辑
|
|
356
|
+
* @param option 上传选项
|
|
357
|
+
* @returns Promise
|
|
358
|
+
*/
|
|
359
|
+
ossUploadImage = async (option) => {
|
|
360
|
+
if (!(option.file instanceof File)) {
|
|
361
|
+
return Promise.reject('file is not instanceof File');
|
|
362
|
+
}
|
|
363
|
+
if (!option.businessType) {
|
|
364
|
+
return Promise.reject('businessType不能为空');
|
|
365
|
+
}
|
|
366
|
+
const file = option.file;
|
|
367
|
+
// 初始化回调函数
|
|
368
|
+
const callbacks = {
|
|
369
|
+
onError: option.onError || (() => { }),
|
|
370
|
+
onSuccess: option.onSuccess || (() => { }),
|
|
371
|
+
onProgress: option.onProgress || (() => { }),
|
|
372
|
+
};
|
|
373
|
+
option.onError = callbacks.onError;
|
|
374
|
+
option.onSuccess = callbacks.onSuccess;
|
|
375
|
+
option.onProgress = callbacks.onProgress;
|
|
376
|
+
// 文件类型验证
|
|
377
|
+
if (!isSupportedImageType(file.type)) {
|
|
378
|
+
callbacks.onError('');
|
|
379
|
+
this.apiConfig.messageWarning?.('上传失败,请上传后缀为png、gif、jpg的文件');
|
|
380
|
+
return Promise.reject('文件类型不支持');
|
|
381
|
+
}
|
|
382
|
+
return new Promise(async (resolve, reject) => {
|
|
383
|
+
try {
|
|
384
|
+
// 注释:小图片(≤100KB)不压缩,quality设为1 (MAIN-2481)
|
|
385
|
+
const isSmallImage = getFileSizeInKB(file.size) <= SMALL_IMAGE_THRESHOLD;
|
|
386
|
+
let quality = isSmallImage ? 1 : option.quality || DEFAULT_QUALITY;
|
|
387
|
+
const businessType = option.businessType;
|
|
388
|
+
if (businessType === BusinessType.CREDIT) {
|
|
389
|
+
return await this.upload({
|
|
390
|
+
file,
|
|
391
|
+
businessType,
|
|
392
|
+
resolve,
|
|
393
|
+
reject,
|
|
394
|
+
callbacks,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
// 注释:batchTransfer选项 - 批量转换水印(用于某些特殊业务场景)
|
|
398
|
+
if (option.batchTransfer && this.apiConfig.multiTransferImage) {
|
|
399
|
+
const res = await this.apiConfig.multiTransferImage({ file });
|
|
400
|
+
if (res.data.code === 0) {
|
|
401
|
+
// 盲水印兼容报错
|
|
402
|
+
if (res.data.data) {
|
|
403
|
+
this.loadImage({
|
|
404
|
+
url: res.data.data,
|
|
405
|
+
val: res.data,
|
|
406
|
+
option,
|
|
407
|
+
file,
|
|
408
|
+
resolve,
|
|
409
|
+
reject,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
callbacks.onError('上传失败');
|
|
415
|
+
reject('上传失败');
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
// 注释:carport类型需要打暗水印
|
|
420
|
+
if (option.imageType === 'carport' &&
|
|
421
|
+
this.apiConfig.darkWaterUploadImage) {
|
|
422
|
+
const res = await this.apiConfig.darkWaterUploadImage({ file });
|
|
423
|
+
if (res.data.code === 0) {
|
|
424
|
+
this.loadImage({
|
|
425
|
+
url: res.data.data,
|
|
426
|
+
val: res.data,
|
|
427
|
+
option,
|
|
428
|
+
file,
|
|
429
|
+
resolve,
|
|
430
|
+
reject,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
callbacks.onError('上传失败');
|
|
435
|
+
reject('上传失败');
|
|
436
|
+
}
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
// 图片压缩
|
|
440
|
+
const rst = await lrz(file, { quality });
|
|
441
|
+
// 注释:长图压缩有问题,长宽比>2:1的图片不压缩
|
|
442
|
+
const isLong = (await this.isLongImage(file)) || false;
|
|
443
|
+
// 确定最终上传的文件
|
|
444
|
+
let postFile;
|
|
445
|
+
if (option.notCompress || file.type === 'image/gif' || isLong) {
|
|
446
|
+
// 注释:GIF、notCompress选项或长图不压缩
|
|
447
|
+
postFile = file;
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
postFile = rst.file;
|
|
451
|
+
}
|
|
452
|
+
// 文件大小验证
|
|
453
|
+
const imgSize = postFile.size;
|
|
454
|
+
const maxSizeMB = DEFAULT_FILE_SIZE_LIMIT;
|
|
455
|
+
if (getFileSizeInMB(imgSize) > maxSizeMB) {
|
|
456
|
+
const errorMsg = `图片不能超过${maxSizeMB}M`;
|
|
457
|
+
callbacks.onError(errorMsg);
|
|
458
|
+
reject(errorMsg);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
// 设置文件名(确保压缩后的文件有名称)
|
|
462
|
+
if (!postFile.name) {
|
|
463
|
+
Object.defineProperty(postFile, 'name', {
|
|
464
|
+
value: file.name,
|
|
465
|
+
writable: false,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
const result = await this.upload({
|
|
469
|
+
file,
|
|
470
|
+
businessType,
|
|
471
|
+
resolve,
|
|
472
|
+
reject,
|
|
473
|
+
callbacks,
|
|
474
|
+
});
|
|
475
|
+
// 注释:idCard场景 - 生成预签名URL
|
|
476
|
+
if ((option.idCard || false) &&
|
|
477
|
+
this.apiConfig.generatePrePresignedUrl) {
|
|
478
|
+
const res = await this.apiConfig.generatePrePresignedUrl({
|
|
479
|
+
objectId: result.name,
|
|
480
|
+
expireMils: PRESIGNED_URL_EXPIRE_TIME,
|
|
481
|
+
});
|
|
482
|
+
if (res.data.code === 0) {
|
|
483
|
+
this.loadImage({
|
|
484
|
+
url: res.data.data,
|
|
485
|
+
val: result,
|
|
486
|
+
file: postFile,
|
|
487
|
+
option,
|
|
488
|
+
resolve,
|
|
489
|
+
reject,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
callbacks.onError('生成预签名URL失败');
|
|
494
|
+
reject('生成预签名URL失败');
|
|
495
|
+
}
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
const errorMsg = error.message || '上传异常';
|
|
501
|
+
callbacks.onError(errorMsg);
|
|
502
|
+
reject(errorMsg);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
};
|
|
506
|
+
/**
|
|
507
|
+
* 文件上传方法
|
|
508
|
+
* 支持视频、文档、APK等文件类型
|
|
509
|
+
* @param option 上传选项
|
|
510
|
+
* @returns Promise
|
|
511
|
+
*/
|
|
512
|
+
ossUploadFile = async (option) => {
|
|
513
|
+
if (!(option.file instanceof File)) {
|
|
514
|
+
return Promise.reject('file is not instanceof File');
|
|
515
|
+
}
|
|
516
|
+
if (!option.businessType) {
|
|
517
|
+
return Promise.reject('businessType不能为空');
|
|
518
|
+
}
|
|
519
|
+
const file = option.file;
|
|
520
|
+
// 初始化回调函数
|
|
521
|
+
const callbacks = {
|
|
522
|
+
onError: option.onError || (() => { }),
|
|
523
|
+
onSuccess: option.onSuccess || (() => { }),
|
|
524
|
+
onProgress: option.onProgress || (() => { }),
|
|
525
|
+
};
|
|
526
|
+
option.onError = callbacks.onError;
|
|
527
|
+
option.onSuccess = callbacks.onSuccess;
|
|
528
|
+
option.onProgress = callbacks.onProgress;
|
|
529
|
+
return new Promise(async (resolve, reject) => {
|
|
530
|
+
try {
|
|
531
|
+
let businessType = option.businessType;
|
|
532
|
+
await this.upload({
|
|
533
|
+
file,
|
|
534
|
+
businessType,
|
|
535
|
+
resolve,
|
|
536
|
+
reject,
|
|
537
|
+
callbacks,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
const errorMsg = error.message || '上传异常';
|
|
542
|
+
callbacks.onError(errorMsg);
|
|
543
|
+
reject(errorMsg);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
};
|
|
547
|
+
/**
|
|
548
|
+
* 纯图片上传 pureOssUploadImage
|
|
549
|
+
* 注释:图片上传,不压缩
|
|
550
|
+
* @param option 上传选项
|
|
551
|
+
* @returns Promise
|
|
552
|
+
*/
|
|
553
|
+
pureOssUploadImage = async (option) => {
|
|
554
|
+
return await this.shopDetailUpdate(option);
|
|
555
|
+
};
|
|
556
|
+
/**
|
|
557
|
+
* 商品详情图片上传(不建议直接使用,请使用 pureOssUploadImage)
|
|
558
|
+
* 注释:图片上传,不压缩
|
|
559
|
+
* @param option 上传选项
|
|
560
|
+
* @returns Promise
|
|
561
|
+
*/
|
|
562
|
+
shopDetailUpdate = async (option) => {
|
|
563
|
+
if (!(option.file instanceof File)) {
|
|
564
|
+
return Promise.reject('file is not instanceof File');
|
|
565
|
+
}
|
|
566
|
+
if (!option.businessType) {
|
|
567
|
+
return Promise.reject('businessType不能为空');
|
|
568
|
+
}
|
|
569
|
+
const file = option.file;
|
|
570
|
+
// 初始化回调函数
|
|
571
|
+
const callbacks = {
|
|
572
|
+
onError: option.onError || (() => { }),
|
|
573
|
+
onSuccess: option.onSuccess || (() => { }),
|
|
574
|
+
onProgress: option.onProgress || (() => { }),
|
|
575
|
+
};
|
|
576
|
+
option.onError = callbacks.onError;
|
|
577
|
+
option.onSuccess = callbacks.onSuccess;
|
|
578
|
+
option.onProgress = callbacks.onProgress;
|
|
579
|
+
// 文件类型验证
|
|
580
|
+
if (!isSupportedImageType(file.type)) {
|
|
581
|
+
callbacks.onError('');
|
|
582
|
+
this.apiConfig.messageWarning?.('上传失败,请上传后缀为png、gif、jpg的文件');
|
|
583
|
+
return Promise.reject('文件类型不支持');
|
|
584
|
+
}
|
|
585
|
+
return new Promise(async (resolve, reject) => {
|
|
586
|
+
try {
|
|
587
|
+
const businessType = option.businessType;
|
|
588
|
+
// 文件大小验证
|
|
589
|
+
if (getFileSizeInMB(file.size) > DEFAULT_FILE_SIZE_LIMIT) {
|
|
590
|
+
const errorMsg = `图片不能超过${DEFAULT_FILE_SIZE_LIMIT}M`;
|
|
591
|
+
callbacks.onError(errorMsg);
|
|
592
|
+
reject(errorMsg);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
await this.upload({
|
|
596
|
+
file,
|
|
597
|
+
businessType,
|
|
598
|
+
resolve,
|
|
599
|
+
reject,
|
|
600
|
+
callbacks,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
const errorMsg = error.message || '上传异常';
|
|
605
|
+
callbacks.onError(errorMsg);
|
|
606
|
+
reject(errorMsg);
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
};
|
|
610
|
+
/**
|
|
611
|
+
* 业务类型枚举
|
|
612
|
+
*/
|
|
613
|
+
businessType = BusinessType;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* 创建阿里云OSS上传器(工厂函数,内部已实现单例)
|
|
617
|
+
* @param apiConfig API配置对象,包含业务相关的API函数
|
|
618
|
+
* @returns OSS上传器实例
|
|
619
|
+
*/
|
|
620
|
+
export function createAliOssUploader(apiConfig) {
|
|
621
|
+
return AliOssClass.getInstance(apiConfig);
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* 默认导出
|
|
625
|
+
*/
|
|
626
|
+
export default {
|
|
627
|
+
createAliOssUploader,
|
|
628
|
+
BusinessType,
|
|
629
|
+
};
|