改七牛直传
This commit is contained in:
parent
b903cabefb
commit
153e76f32a
@ -13,6 +13,7 @@
|
||||
- [一键复制](./一键复制.md)
|
||||
- [文件上传超时配置](./文件上传超时配置.md) - 大文件上传配置说明
|
||||
- [图片URL处理说明](./图片URL处理说明.md) - 图片URL拼接处理
|
||||
- [七牛云直传配置](./七牛云直传配置.md) - ⚡ 七牛云直传上传(推荐!)
|
||||
|
||||
### 存储配置功能
|
||||
存储配置功能的前端实现已完成,包括:
|
||||
|
||||
443
docs/七牛云直传配置.md
Normal file
443
docs/七牛云直传配置.md
Normal file
@ -0,0 +1,443 @@
|
||||
# 七牛云直传配置说明
|
||||
|
||||
## 概述
|
||||
|
||||
新的上传机制实现了前端直接上传到七牛云,不再通过后端中转,大幅提升大文件上传效率。
|
||||
|
||||
## 上传流程对比
|
||||
|
||||
### 旧流程(低效)
|
||||
```
|
||||
前端 → 后端服务器 → 七牛云
|
||||
(中转暂存)
|
||||
```
|
||||
|
||||
问题:
|
||||
- 大文件需要先上传到服务器,再由服务器上传到七牛云
|
||||
- 占用服务器带宽和磁盘空间
|
||||
- 上传时间翻倍
|
||||
- 服务器压力大
|
||||
|
||||
### 新流程(高效)
|
||||
```
|
||||
前端 → 七牛云(直传)
|
||||
后端 → 数据库(仅保存记录)
|
||||
```
|
||||
|
||||
优势:
|
||||
- 前端直接上传到七牛云,不经过服务器
|
||||
- 节省服务器资源
|
||||
- 上传速度快
|
||||
- 支持断点续传
|
||||
|
||||
## 安装依赖
|
||||
|
||||
### 前端安装七牛云 SDK
|
||||
|
||||
```bash
|
||||
cd platform
|
||||
npm install qiniu-js
|
||||
```
|
||||
|
||||
或使用 yarn:
|
||||
|
||||
```bash
|
||||
yarn add qiniu-js
|
||||
```
|
||||
|
||||
## 后端 API
|
||||
|
||||
### 1. 获取存储配置
|
||||
|
||||
**接口**: `GET /platform/storage/config`
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"storageType": "qiniu", // 或 "local"
|
||||
"qiniuDomain": "http://7colud.yunzer.cn",
|
||||
"qiniuRegion": "z0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 获取上传凭证
|
||||
|
||||
**接口**: `GET /platform/qiniu/token`
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"token": "七牛云上传token",
|
||||
"domain": "http://7colud.yunzer.cn",
|
||||
"bucket": "your-bucket",
|
||||
"region": "z0",
|
||||
"keyPrefix": "2026/04/09/1775722615052606500",
|
||||
"expires": 1712654400,
|
||||
"uploadUrl": "https://up-z0.qiniup.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 保存文件记录
|
||||
|
||||
**接口**: `POST /platform/qiniu/save`
|
||||
|
||||
**请求**:
|
||||
```json
|
||||
{
|
||||
"key": "2026/04/09/1775722615052606500.png",
|
||||
"hash": "FhGxwBzoLwO_RGws...",
|
||||
"size": 1024000,
|
||||
"name": "screenshot.png",
|
||||
"mimeType": "image/png",
|
||||
"cate": 0
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"url": "http://7colud.yunzer.cn/2026/04/09/1775722615052606500.png",
|
||||
"id": 123,
|
||||
"name": "screenshot.png",
|
||||
"key": "2026/04/09/1775722615052606500.png"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 前端使用
|
||||
|
||||
### 基础用法
|
||||
|
||||
```javascript
|
||||
import { smartUpload } from '@/utils/qiniuUpload';
|
||||
|
||||
// 自动选择上传方式(本地或七牛云)
|
||||
const result = await smartUpload(file, {
|
||||
cate: 0, // 文件分类
|
||||
onProgress: (progress) => {
|
||||
console.log('上传进度:', progress.percent + '%');
|
||||
console.log('已上传:', progress.loaded);
|
||||
console.log('总大小:', progress.total);
|
||||
},
|
||||
});
|
||||
|
||||
console.log('上传成功:', result);
|
||||
// { url: '...', id: 123, name: '...', key: '...' }
|
||||
```
|
||||
|
||||
### 在组件中使用
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-upload
|
||||
:http-request="handleUpload"
|
||||
:on-progress="handleProgress"
|
||||
>
|
||||
<el-button>上传文件</el-button>
|
||||
</el-upload>
|
||||
<el-progress v-if="uploading" :percentage="uploadPercent" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { smartUpload } from '@/utils/qiniuUpload';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const uploading = ref(false);
|
||||
const uploadPercent = ref(0);
|
||||
|
||||
async function handleUpload(options) {
|
||||
const file = options.file?.raw || options.file;
|
||||
uploading.value = true;
|
||||
|
||||
try {
|
||||
const result = await smartUpload(file, {
|
||||
cate: 0,
|
||||
onProgress: (progress) => {
|
||||
uploadPercent.value = progress.percent;
|
||||
},
|
||||
});
|
||||
|
||||
ElMessage.success('上传成功');
|
||||
options.onSuccess?.(result);
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '上传失败');
|
||||
options.onError?.(error);
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 批量上传
|
||||
|
||||
```javascript
|
||||
import { batchUpload } from '@/utils/qiniuUpload';
|
||||
|
||||
const files = [file1, file2, file3];
|
||||
|
||||
const results = await batchUpload(files, {
|
||||
cate: 0,
|
||||
onFileProgress: (file, progress) => {
|
||||
console.log(`${file.name}: ${progress.percent}%`);
|
||||
},
|
||||
onFileComplete: (file, result) => {
|
||||
console.log(`${file.name} 上传成功:`, result);
|
||||
},
|
||||
onFileError: (file, error) => {
|
||||
console.error(`${file.name} 上传失败:`, error);
|
||||
},
|
||||
});
|
||||
|
||||
console.log('所有文件上传完成:', results);
|
||||
```
|
||||
|
||||
## 工作原理
|
||||
|
||||
### 1. 智能选择上传方式
|
||||
|
||||
`smartUpload` 函数会自动检测后端配置:
|
||||
|
||||
```javascript
|
||||
// 1. 获取存储配置
|
||||
const config = await getStorageConfig();
|
||||
|
||||
// 2. 根据配置选择上传方式
|
||||
if (config.storageType === 'qiniu') {
|
||||
// 七牛云直传
|
||||
return uploadToQiniu(file, options);
|
||||
} else {
|
||||
// 本地上传(通过后端)
|
||||
return uploadToLocal(file, options);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 七牛云直传流程
|
||||
|
||||
```javascript
|
||||
// 1. 获取上传凭证
|
||||
const tokenRes = await getQiniuToken();
|
||||
const { token, keyPrefix } = tokenRes.data;
|
||||
|
||||
// 2. 生成文件 key
|
||||
const key = `${keyPrefix}.${ext}`;
|
||||
|
||||
// 3. 使用七牛云 SDK 直接上传
|
||||
const observable = qiniu.upload(file, key, token);
|
||||
|
||||
// 4. 监听上传进度
|
||||
observable.subscribe({
|
||||
next(res) {
|
||||
// 进度回调
|
||||
onProgress(res.total.percent);
|
||||
},
|
||||
complete(res) {
|
||||
// 上传完成,保存记录到数据库
|
||||
await saveFileRecord({
|
||||
key: res.key,
|
||||
hash: res.hash,
|
||||
size: file.size,
|
||||
name: file.name,
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 本地上传流程
|
||||
|
||||
```javascript
|
||||
// 通过后端中转(兼容本地存储)
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const res = await request({
|
||||
url: '/platform/uploadfile',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
onUploadProgress: (e) => {
|
||||
onProgress(e.loaded / e.total * 100);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 七牛云区域配置
|
||||
|
||||
| 区域代码 | 区域名称 | 上传地址 |
|
||||
|---------|---------|---------|
|
||||
| z0 | 华东 | https://up-z0.qiniup.com |
|
||||
| z1 | 华北 | https://up-z1.qiniup.com |
|
||||
| z2 | 华南 | https://up-z2.qiniup.com |
|
||||
| na0 | 北美 | https://up-na0.qiniup.com |
|
||||
| as0 | 新加坡 | https://up-as0.qiniup.com |
|
||||
| cn-east-2 | 华东-浙江2 | https://up-cn-east-2.qiniup.com |
|
||||
|
||||
### 上传策略配置
|
||||
|
||||
后端生成 token 时的策略:
|
||||
|
||||
```go
|
||||
putPolicy := storage.PutPolicy{
|
||||
Scope: cfg.QiniuBucket,
|
||||
ReturnBody: `{"key":"$(key)","hash":"$(etag)","size":$(fsize),"mimeType":"$(mimeType)"}`,
|
||||
Expires: 3600, // 1小时有效期
|
||||
}
|
||||
```
|
||||
|
||||
## 安全性
|
||||
|
||||
### 1. Token 有效期
|
||||
|
||||
上传 token 有效期为 1 小时,过期后需要重新获取。
|
||||
|
||||
### 2. 权限验证
|
||||
|
||||
- 获取 token 需要登录认证
|
||||
- 保存文件记录需要登录认证
|
||||
- 文件记录关联到当前用户和租户
|
||||
|
||||
### 3. 文件去重
|
||||
|
||||
通过 MD5 检查文件是否已存在,避免重复上传。
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. 断点续传
|
||||
|
||||
七牛云 SDK 支持断点续传(大文件自动分片):
|
||||
|
||||
```javascript
|
||||
const config = {
|
||||
useCdnDomain: true,
|
||||
region: qiniu.region.z0,
|
||||
chunkSize: 4, // 分片大小(MB)
|
||||
concurrentRequestLimit: 3, // 并发上传数
|
||||
};
|
||||
|
||||
const observable = qiniu.upload(file, key, token, putExtra, config);
|
||||
```
|
||||
|
||||
### 2. CDN 加速
|
||||
|
||||
启用 CDN 域名加速上传:
|
||||
|
||||
```javascript
|
||||
const config = {
|
||||
useCdnDomain: true, // 使用 CDN 加速
|
||||
};
|
||||
```
|
||||
|
||||
### 3. 并发上传
|
||||
|
||||
批量上传时可以控制并发数:
|
||||
|
||||
```javascript
|
||||
// 限制同时上传 3 个文件
|
||||
const concurrency = 3;
|
||||
const results = [];
|
||||
|
||||
for (let i = 0; i < files.length; i += concurrency) {
|
||||
const batch = files.slice(i, i + concurrency);
|
||||
const batchResults = await Promise.all(
|
||||
batch.map(file => smartUpload(file, options))
|
||||
);
|
||||
results.push(...batchResults);
|
||||
}
|
||||
```
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 1. 上传失败:获取 token 失败
|
||||
|
||||
**错误**: "当前未配置七牛云存储"
|
||||
|
||||
**解决**:
|
||||
- 检查数据库存储配置
|
||||
- 确认 `storage_type` 为 `'qiniu'`
|
||||
- 确认七牛云配置完整
|
||||
|
||||
### 2. 上传失败:token 无效
|
||||
|
||||
**错误**: "401 Unauthorized"
|
||||
|
||||
**解决**:
|
||||
- 检查 AccessKey 和 SecretKey 是否正确
|
||||
- 检查 token 是否过期(1小时有效期)
|
||||
- 重新获取 token
|
||||
|
||||
### 3. 上传失败:bucket 不存在
|
||||
|
||||
**错误**: "no such bucket"
|
||||
|
||||
**解决**:
|
||||
- 检查 bucket 名称是否正确
|
||||
- 检查 bucket 是否在对应区域
|
||||
- 登录七牛云控制台确认
|
||||
|
||||
### 4. 保存记录失败
|
||||
|
||||
**错误**: "保存文件记录失败"
|
||||
|
||||
**解决**:
|
||||
- 检查数据库连接
|
||||
- 检查文件信息是否完整
|
||||
- 查看后端日志
|
||||
|
||||
## 迁移指南
|
||||
|
||||
### 从旧版本迁移
|
||||
|
||||
1. **安装依赖**:
|
||||
```bash
|
||||
npm install qiniu-js
|
||||
```
|
||||
|
||||
2. **更新导入**:
|
||||
```javascript
|
||||
// 旧版本
|
||||
import { uploadFile } from '@/api/file';
|
||||
|
||||
// 新版本
|
||||
import { smartUpload } from '@/utils/qiniuUpload';
|
||||
```
|
||||
|
||||
3. **更新上传代码**:
|
||||
```javascript
|
||||
// 旧版本
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const res = await uploadFile(formData, { cate: 0 });
|
||||
|
||||
// 新版本
|
||||
const result = await smartUpload(file, { cate: 0 });
|
||||
```
|
||||
|
||||
4. **兼容性**:
|
||||
- `smartUpload` 会自动检测存储配置
|
||||
- 如果配置为本地存储,会自动使用旧的上传方式
|
||||
- 无需修改其他代码
|
||||
|
||||
## 相关文件
|
||||
|
||||
### 后端
|
||||
- `go/controllers/qiniu_upload.go` - 七牛云上传控制器
|
||||
- `go/routers/platform/platform.go` - 路由配置
|
||||
- `go/services/storage_service.go` - 存储服务
|
||||
|
||||
### 前端
|
||||
- `platform/src/utils/qiniuUpload.js` - 七牛云上传工具
|
||||
- `platform/src/views/platform/softwareupgrade/components/edit.vue` - 软件升级组件(示例)
|
||||
|
||||
## 更新日期
|
||||
|
||||
2026-04-09
|
||||
48
package-lock.json
generated
48
package-lock.json
generated
@ -20,6 +20,7 @@
|
||||
"marked": "^16.4.1",
|
||||
"os": "^0.1.2",
|
||||
"pinia": "^3.0.3",
|
||||
"qiniu-js": "^3.4.4",
|
||||
"vue": "^3.5.22",
|
||||
"vue-img-cutter": "^3.0.7",
|
||||
"vue-router": "^4.6.3",
|
||||
@ -77,6 +78,26 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime-corejs2": {
|
||||
"version": "7.29.2",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/runtime-corejs2/-/runtime-corejs2-7.29.2.tgz",
|
||||
"integrity": "sha512-+FqVkbqWaDleqS9fgzFypApKoPvmGFgk5X2lGXbL9wgz6tf88qt2HEUuEn9E3yBeLt7p8pIgODbJ5icVRALKhQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-js": "^2.6.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime-corejs2/node_modules/core-js": {
|
||||
"version": "2.6.12",
|
||||
"resolved": "https://registry.npmmirror.com/core-js/-/core-js-2.6.12.tgz",
|
||||
"integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==",
|
||||
"deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz",
|
||||
@ -3685,6 +3706,17 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/qiniu-js": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmmirror.com/qiniu-js/-/qiniu-js-3.4.4.tgz",
|
||||
"integrity": "sha512-S/ooashZjyFQIIbxrte+OfwRxZQz5/MfVv55xWewc9GoxogP/xMmWixCaBEbdqmyjPAmJ2+VtZiWUuP8teB/BA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime-corejs2": "^7.10.2",
|
||||
"querystring": "^0.2.1",
|
||||
"spark-md5": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/quansync": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmmirror.com/quansync/-/quansync-0.2.11.tgz",
|
||||
@ -3702,6 +3734,16 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/querystring": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/querystring/-/querystring-0.2.1.tgz",
|
||||
"integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==",
|
||||
"deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
@ -4112,6 +4154,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/spark-md5": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/spark-md5/-/spark-md5-3.0.2.tgz",
|
||||
"integrity": "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==",
|
||||
"license": "(WTFPL OR MIT)"
|
||||
},
|
||||
"node_modules/speakingurl": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz",
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
"marked": "^16.4.1",
|
||||
"os": "^0.1.2",
|
||||
"pinia": "^3.0.3",
|
||||
"qiniu-js": "^3.4.4",
|
||||
"vue": "^3.5.22",
|
||||
"vue-img-cutter": "^3.0.7",
|
||||
"vue-router": "^4.6.3",
|
||||
|
||||
225
src/utils/qiniuUpload.js
Normal file
225
src/utils/qiniuUpload.js
Normal file
@ -0,0 +1,225 @@
|
||||
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 } = 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',
|
||||
};
|
||||
|
||||
const config = {
|
||||
useCdnDomain: true,
|
||||
region: qiniu.region.z0, // 根据实际区域配置
|
||||
};
|
||||
|
||||
// 4. 创建 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;
|
||||
}
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user