v1.2.1版本,优化调整了很多,整改验收阶段新加字段
This commit is contained in:
300
utils/upload.js
Normal file
300
utils/upload.js
Normal file
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* 统一文件上传:七牛云客户端直传
|
||||
* 流程:计算 MD5 + 后缀 → 向后端取凭证 → uni.uploadFile 直传七牛 → 返回完整 CDN URL
|
||||
*/
|
||||
import { toImageUrl } from '@/request/request.js';
|
||||
import { getQiniuUploadToken } from '@/request/api.js';
|
||||
import { addTimestampWatermark } from '@/utils/watermark.js';
|
||||
|
||||
const DEFAULT_QINIU_UPLOAD_URL = 'https://upload.qiniup.com';
|
||||
|
||||
/** 从本地临时路径解析后缀(不含点) */
|
||||
export function getFileSuffix(filePath) {
|
||||
if (!filePath) return '';
|
||||
const clean = String(filePath).split('?')[0];
|
||||
const ext = clean.split('.').pop()?.toLowerCase() || '';
|
||||
// 微信部分临时文件无后缀,按图片处理
|
||||
if (!ext || ext.length > 8 || clean.endsWith(ext) === false) {
|
||||
return 'jpg';
|
||||
}
|
||||
return ext;
|
||||
}
|
||||
|
||||
/** 计算文件 MD5(微信小程序支持;其他端回退为空字符串,需后端兼容) */
|
||||
export function getFileMd5(filePath) {
|
||||
return new Promise((resolve) => {
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.getFileInfo({
|
||||
filePath,
|
||||
digestAlgorithm: 'md5',
|
||||
success: (res) => resolve((res.digest || '').toLowerCase()),
|
||||
fail: (err) => {
|
||||
console.warn('getFileMd5 fail, use empty:', err);
|
||||
resolve('');
|
||||
}
|
||||
});
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
resolve('');
|
||||
// #endif
|
||||
});
|
||||
}
|
||||
|
||||
/** 向后端获取七牛直传凭证 */
|
||||
export async function fetchQiniuUploadCredential(filePath) {
|
||||
const suffix = getFileSuffix(filePath);
|
||||
const fileMd5 = await getFileMd5(filePath);
|
||||
const res = await getQiniuUploadToken({ fileMd5, suffix });
|
||||
return normalizeQiniuCredential(res.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析后端 /frontend/attachment/qiniu/token 返回的 data
|
||||
* 当前约定:token、key、uploadUrl、url(完整 CDN 地址)
|
||||
* 例:uploadUrl=https://upload-z2.qiniup.com,url=https://oss.hexieyun.com.cn/uploads/...
|
||||
*/
|
||||
function normalizeQiniuCredential(raw) {
|
||||
if (!raw) {
|
||||
throw new Error('七牛凭证为空');
|
||||
}
|
||||
const token = raw.token || raw.uploadToken || raw.uptoken;
|
||||
const key = raw.key || raw.fileKey || raw.objectKey;
|
||||
const uploadUrl = (raw.uploadUrl || raw.uploadHost || raw.host || DEFAULT_QINIU_UPLOAD_URL).replace(/\/$/, '');
|
||||
const presetUrl = String(raw.url || raw.fileUrl || raw.fullUrl || '').trim();
|
||||
|
||||
let cdnOrigin = (raw.domain || raw.cdnDomain || '').replace(/\/$/, '');
|
||||
if (!cdnOrigin && presetUrl) {
|
||||
const m = presetUrl.match(/^(https?:\/\/[^/]+)/i);
|
||||
if (m) cdnOrigin = m[1];
|
||||
}
|
||||
if (cdnOrigin && !cdnOrigin.startsWith('http')) {
|
||||
cdnOrigin = `https://${cdnOrigin}`;
|
||||
}
|
||||
|
||||
if (!token || !key) {
|
||||
throw new Error('七牛凭证缺少 token 或 key');
|
||||
}
|
||||
|
||||
return { token, key, uploadUrl, cdnOrigin, presetUrl };
|
||||
}
|
||||
|
||||
/** 七牛 upload 成功后的 JSON:{ hash, key },结合凭证得到最终访问 URL */
|
||||
function resolveUploadedFileUrl(credential, uploadResData) {
|
||||
if (credential.presetUrl) {
|
||||
return credential.presetUrl;
|
||||
}
|
||||
|
||||
let objectKey = credential.key;
|
||||
if (uploadResData) {
|
||||
try {
|
||||
const body =
|
||||
typeof uploadResData === 'string' ? JSON.parse(uploadResData) : uploadResData;
|
||||
if (body?.key) {
|
||||
objectKey = body.key;
|
||||
}
|
||||
} catch (e) {
|
||||
// 非 JSON 时沿用凭证里的 key
|
||||
}
|
||||
}
|
||||
|
||||
if (credential.cdnOrigin && objectKey) {
|
||||
return `${credential.cdnOrigin}/${String(objectKey).replace(/^\//, '')}`;
|
||||
}
|
||||
|
||||
return buildQiniuFileUrl(credential.cdnOrigin, objectKey);
|
||||
}
|
||||
|
||||
/** 拼接七牛文件完整访问地址 */
|
||||
export function buildQiniuFileUrl(domain, key) {
|
||||
if (!domain || !key) return '';
|
||||
const k = String(key).replace(/^\//, '');
|
||||
const d = String(domain).replace(/\/$/, '');
|
||||
if (d.startsWith('http://') || d.startsWith('https://')) {
|
||||
return `${d}/${k}`;
|
||||
}
|
||||
return `https://${d}/${k}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传单个文件到七牛云
|
||||
* @param {string} filePath 本地临时路径
|
||||
* @param {Object} [options]
|
||||
* @param {Function} [options.beforeUpload] 上传前处理(如水印),返回新的本地路径
|
||||
* @returns {Promise<{url:string,key:string,filePath:string,serverPath:string}>}
|
||||
*/
|
||||
export async function uploadToCloud(filePath, options = {}) {
|
||||
let localPath = filePath;
|
||||
if (options.beforeUpload) {
|
||||
localPath = await options.beforeUpload(filePath);
|
||||
}
|
||||
|
||||
const credential = await fetchQiniuUploadCredential(localPath);
|
||||
const { token, key, uploadUrl } = credential;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: uploadUrl,
|
||||
filePath: localPath,
|
||||
name: 'file',
|
||||
formData: {
|
||||
token,
|
||||
key
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode && res.statusCode >= 400) {
|
||||
reject(new Error(`七牛上传失败(${res.statusCode})`));
|
||||
return;
|
||||
}
|
||||
const fullUrl = resolveUploadedFileUrl(credential, res.data);
|
||||
if (!fullUrl) {
|
||||
reject(new Error('无法解析上传后的文件地址,请检查后端 url 或 CDN 配置'));
|
||||
return;
|
||||
}
|
||||
let respKey = key;
|
||||
try {
|
||||
const body =
|
||||
typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
|
||||
if (body?.key) respKey = body.key;
|
||||
} catch (e) {}
|
||||
resolve({
|
||||
url: fullUrl,
|
||||
key: respKey,
|
||||
filePath: fullUrl,
|
||||
serverPath: fullUrl
|
||||
});
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('七牛上传失败:', err);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单提交用:统一为完整 URL(兼容历史相对路径)
|
||||
*/
|
||||
export function toSubmitFileUrl(filePath) {
|
||||
if (!filePath) return '';
|
||||
const p = String(filePath);
|
||||
if (p.startsWith('http://') || p.startsWith('https://')) {
|
||||
return p;
|
||||
}
|
||||
return toImageUrl(p);
|
||||
}
|
||||
|
||||
/** 将后端附件记录转为 up-upload 列表项(编辑回显) */
|
||||
export function mapServerFileToUploadItem(att) {
|
||||
const filePath = toSubmitFileUrl(att.filePath || att.url || '');
|
||||
return {
|
||||
url: filePath,
|
||||
serverPath: filePath,
|
||||
status: 'success',
|
||||
message: '',
|
||||
name: att.fileName || att.name || '',
|
||||
type: att.fileType || 'image/jpeg',
|
||||
size: att.fileSize || 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 up-upload 列表项构建附件对象(提交给后端)
|
||||
*/
|
||||
export function buildAttachmentItem(file, defaults = {}) {
|
||||
const filePath = toSubmitFileUrl(
|
||||
file.serverPath || file.filePath || file.url || ''
|
||||
);
|
||||
const fileName =
|
||||
file.name ||
|
||||
(filePath ? filePath.split('/').pop()?.split('?')[0] : '') ||
|
||||
'';
|
||||
return {
|
||||
fileName: fileName || defaults.fileName || '',
|
||||
filePath,
|
||||
fileType: file.type || defaults.fileType || 'image/jpeg',
|
||||
fileSize: file.size || defaults.fileSize || 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 up-upload 组件创建 afterRead / deletePic 处理器
|
||||
* @param {import('vue').Ref} fileListRef
|
||||
* @param {Object} [options]
|
||||
* @param {Object} [options.watermark] 传入 addTimestampWatermark 的参数(不含 tempFilePath)
|
||||
*/
|
||||
export function createUploadListHandlers(fileListRef, options = {}) {
|
||||
const deletePic = (event) => {
|
||||
fileListRef.value.splice(event.index, 1);
|
||||
};
|
||||
|
||||
const afterRead = async (event) => {
|
||||
const lists = [].concat(event.file);
|
||||
let fileListLen = fileListRef.value.length;
|
||||
|
||||
lists.forEach((item) => {
|
||||
fileListRef.value.push({
|
||||
...item,
|
||||
status: 'uploading',
|
||||
message: '上传中'
|
||||
});
|
||||
});
|
||||
|
||||
for (let i = 0; i < lists.length; i++) {
|
||||
const listIndex = fileListLen;
|
||||
try {
|
||||
const beforeUpload = options.watermark
|
||||
? (tempFilePath) =>
|
||||
addTimestampWatermark({
|
||||
tempFilePath,
|
||||
...options.watermark
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const result = await uploadToCloud(lists[i].url, { beforeUpload });
|
||||
const item = fileListRef.value[listIndex];
|
||||
fileListRef.value.splice(listIndex, 1, {
|
||||
...item,
|
||||
status: 'success',
|
||||
message: '',
|
||||
url: result.url,
|
||||
serverPath: result.url
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('上传失败:', e);
|
||||
const item = fileListRef.value[listIndex];
|
||||
fileListRef.value.splice(listIndex, 1, {
|
||||
...item,
|
||||
status: 'failed',
|
||||
message: e?.msg || e?.message || '上传失败'
|
||||
});
|
||||
uni.showToast({
|
||||
title: e?.msg || e?.message || '上传失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
fileListLen++;
|
||||
}
|
||||
};
|
||||
|
||||
return { afterRead, deletePic };
|
||||
}
|
||||
|
||||
/**
|
||||
* 单文件上传(头像、证件照等),带 loading
|
||||
*/
|
||||
export function uploadSingleWithLoading(filePath, options = {}) {
|
||||
const loadingTitle = options.loadingTitle || '上传中...';
|
||||
uni.showLoading({ title: loadingTitle, mask: true });
|
||||
return uploadToCloud(filePath, options)
|
||||
.then((result) => {
|
||||
uni.hideLoading();
|
||||
return result;
|
||||
})
|
||||
.catch((err) => {
|
||||
uni.hideLoading();
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
/** @deprecated 请使用 uploadToCloud;保留别名便于渐进迁移 */
|
||||
export const uploadFilePromise = uploadToCloud;
|
||||
Reference in New Issue
Block a user