Files
threeonecheck_web/utils/upload.js

301 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 统一文件上传:七牛云客户端直传
* 流程:计算 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.comurl=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;