This commit is contained in:
2026-04-13 08:38:16 +08:00
10 changed files with 1756 additions and 20 deletions
+252
View File
@@ -0,0 +1,252 @@
import request from '@/utils/request';
import * as qiniu from 'qiniu-js';
/**
* 获取存储配置
* @returns {Promise<{storageType: string, qiniuDomain?: string, qiniuRegion?: string}>}
*/
export async function getStorageConfig() {
const res = await request({
url: '/platform/storage/config',
method: 'get',
});
if (res?.code === 200) {
return res.data || { storageType: 'local' };
}
return { storageType: 'local' };
}
/**
* 获取七牛云上传凭证
* @returns {Promise}
*/
export async function getQiniuToken() {
return request({
url: '/platform/qiniu/token',
method: 'get',
});
}
/**
* 保存文件记录到数据库
* @param {Object} data 文件信息
* @returns {Promise}
*/
export async function saveFileRecord(data) {
return request({
url: '/platform/qiniu/save',
method: 'post',
data,
});
}
/**
* 上传文件(自动选择本地或七牛云)
* @param {File} file 文件对象
* @param {Object} options 配置选项
* @param {number} [options.cate] 文件分类
* @param {Function} [options.onProgress] 进度回调
* @returns {Promise<{url: string, id: number, name: string, key?: string}>}
*/
export async function smartUpload(file, options = {}) {
// 获取存储配置
const config = await getStorageConfig();
if (config.storageType === 'qiniu') {
// 使用七牛云直传
return uploadToQiniu(file, options);
} else {
// 使用本地上传(通过后端)
return uploadToLocal(file, options);
}
}
/**
* 上传到七牛云(直传)
* @param {File} file 文件对象
* @param {Object} options 配置选项
* @returns {Promise}
*/
export async function uploadToQiniu(file, options = {}) {
// 1. 获取上传凭证
const tokenRes = await getQiniuToken();
if (tokenRes?.code !== 200) {
throw new Error(tokenRes?.msg || '获取上传凭证失败');
}
const { token, keyPrefix, domain, region, uploadUrl } = tokenRes.data;
// 2. 生成文件 key
const ext = file.name.split('.').pop();
const key = `${keyPrefix}.${ext}`;
// 3. 配置上传参数
const putExtra = {
fname: file.name,
mimeType: file.type || 'application/octet-stream',
};
// 4. 根据区域代码获取七牛云区域对象
const qiniuRegion = getQiniuRegion(region);
const config = {
useCdnDomain: true,
region: qiniuRegion,
};
// 5. 创建 observable 对象
const observable = qiniu.upload(file, key, token, putExtra, config);
// 5. 执行上传
return new Promise((resolve, reject) => {
const subscription = observable.subscribe({
next(res) {
// 进度回调
if (options.onProgress) {
options.onProgress({
loaded: res.total.loaded,
total: res.total.size,
percent: res.total.percent,
});
}
},
error(err) {
reject(new Error(err.message || '上传失败'));
},
async complete(res) {
try {
// 6. 保存文件记录到数据库
const saveRes = await saveFileRecord({
key: res.key,
hash: res.hash,
size: file.size,
name: file.name,
mimeType: file.type,
cate: options.cate || 0,
});
if (saveRes?.code === 200 || saveRes?.code === 201) {
resolve({
url: saveRes.data.url,
id: saveRes.data.id,
name: saveRes.data.name,
key: saveRes.data.key,
});
} else {
reject(new Error(saveRes?.msg || '保存文件记录失败'));
}
} catch (error) {
reject(error);
}
},
});
});
}
/**
* 上传到本地(通过后端中转)
* @param {File} file 文件对象
* @param {Object} options 配置选项
* @returns {Promise}
*/
export async function uploadToLocal(file, options = {}) {
const formData = new FormData();
formData.append('file', file);
if (options.cate !== undefined) {
formData.append('cate', String(options.cate));
}
const config = {
url: '/platform/uploadfile',
method: 'post',
data: formData,
timeout: 0, // 不设置超时
};
if (options.onProgress) {
config.onUploadProgress = (e) => {
options.onProgress({
loaded: e.loaded,
total: e.total || 0,
percent: e.total > 0 ? Math.round((e.loaded * 100) / e.total) : 0,
});
};
}
const res = await request(config);
if (res?.code === 200 || res?.code === 201) {
return {
url: res.data.url,
id: res.data.id,
name: res.data.name,
};
} else {
throw new Error(res?.msg || '上传失败');
}
}
/**
* 批量上传文件
* @param {File[]} files 文件数组
* @param {Object} options 配置选项
* @param {Function} [options.onFileProgress] 单个文件进度回调 (file, progress) => void
* @param {Function} [options.onFileComplete] 单个文件完成回调 (file, result) => void
* @param {Function} [options.onFileError] 单个文件错误回调 (file, error) => void
* @returns {Promise<Array>}
*/
export async function batchUpload(files, options = {}) {
const results = [];
for (const file of files) {
try {
const result = await smartUpload(file, {
...options,
onProgress: (progress) => {
if (options.onFileProgress) {
options.onFileProgress(file, progress);
}
},
});
results.push({ file, result, success: true });
if (options.onFileComplete) {
options.onFileComplete(file, result);
}
} catch (error) {
results.push({ file, error, success: false });
if (options.onFileError) {
options.onFileError(file, error);
}
}
}
return results;
}
/**
* 根据区域代码获取七牛云区域对象
* @param {string} regionCode 区域代码 (z0, z1, z2, na0, as0, cn-east-2)
* @returns {Object} 七牛云区域对象
*/
function getQiniuRegion(regionCode) {
switch (regionCode) {
case 'z0':
return qiniu.region.z0; // 华东
case 'z1':
return qiniu.region.z1; // 华北
case 'z2':
return qiniu.region.z2; // 华南
case 'na0':
return qiniu.region.na0; // 北美
case 'as0':
return qiniu.region.as0; // 新加坡
case 'cn-east-2':
return qiniu.region.cnEast2; // 华东-浙江2
default:
return qiniu.region.z0; // 默认华东
}
}
@@ -115,7 +115,8 @@ import {
createSoftwareUpgrade,
updateSoftwareUpgrade,
} from "@/api/softwareUpgrade";
import { getUserCate, createFileCate, uploadFile, getFileById } from "@/api/file";
import { getUserCate, createFileCate, getFileById } from "@/api/file";
import { smartUpload } from "@/utils/qiniuUpload";
const emit = defineEmits(["saved"]);
@@ -262,8 +263,7 @@ async function handlePackageUpload(options) {
options.onError?.(new Error("no category"));
return;
}
const fd = new FormData();
fd.append("file", file);
uploadXHRActive.value = true;
uploadPercent.value = 0;
uploadIndeterminate.value = false;
@@ -273,20 +273,24 @@ async function handlePackageUpload(options) {
let lastLoaded = 0;
let lastTick = xhrStart;
let emaBps = 0;
const res = await uploadFile(fd, {
// 使用智能上传(自动选择本地或七牛云)
const result = await smartUpload(file, {
cate: cateId,
onUploadProgress: (e) => {
onProgress: (progress) => {
const now = Date.now();
const loaded = e.loaded;
const total = e.total ?? 0;
const loaded = progress.loaded;
const total = progress.total || 0;
const elapsedSec = (now - xhrStart) / 1000;
const dt = (now - lastTick) / 1000;
if (dt >= 0.07 && loaded >= lastLoaded) {
const inst = (loaded - lastLoaded) / dt;
emaBps = emaBps > 0 ? emaBps * 0.72 + inst * 0.28 : inst;
lastLoaded = loaded;
lastTick = now;
}
const avgBps = elapsedSec > 0.12 ? loaded / elapsedSec : 0;
const showBps = emaBps > 0 ? emaBps : avgBps;
uploadSpeedText.value = showBps > 0 ? formatSpeed(showBps) : elapsedSec > 0.05 ? formatSpeed(avgBps) : "—";
@@ -301,19 +305,14 @@ async function handlePackageUpload(options) {
}
},
});
if (res?.code === 200 || res?.code === 201) {
const d = res.data || {};
form.fileId = d.id != null ? Number(d.id) : null;
const src = d.url || "";
form.downloadUrl = absoluteFromSrc(src);
uploadedLabel.value = d.name || file.name || "安装包";
displayFileList.value = [{ name: uploadedLabel.value, uid: `pkg-${form.fileId}-${Date.now()}` }];
ElMessage.success(res.code === 201 ? "文件已存在,已关联" : "上传成功");
options.onSuccess?.(res);
} else {
ElMessage.error(res?.msg || "上传失败");
options.onError?.(new Error(res?.msg));
}
// 上传成功
form.fileId = result.id;
form.downloadUrl = result.url;
uploadedLabel.value = result.name || file.name || "安装包";
displayFileList.value = [{ name: uploadedLabel.value, uid: `pkg-${form.fileId}-${Date.now()}` }];
ElMessage.success("上传成功");
options.onSuccess?.(result);
} catch (e) {
ElMessage.error(e?.message || "上传失败");
options.onError?.(e);