- 新增 pages/course/course 页面,集成 web-view 显示线上课程列表 - 在 app.json 中注册课程页面并添加底部 tabbar 导航 - 优化打卡页面,添加学号显示和改进成员列表布局 - 重构计划页面,添加内容详情弹层和优化加载逻辑 - 添加阿里云 OSS 上传工具模块用于文件上传功能 - 统一姓名和学号在同一行显示,改善 UI 体验 - 修复多个页面中的绑定事件和样式问题
195 lines
6.5 KiB
JavaScript
195 lines
6.5 KiB
JavaScript
/**
|
||
* 阿里云 OSS 上传封装(demo)
|
||
*
|
||
* 提供两种模式,二选一即可:
|
||
* · uploadDirect() 模式B:向后端取签名 → 小程序直接传 OSS(省服务器带宽,推荐)
|
||
* · uploadViaServer() 模式A:文件先传后端,由后端转存 OSS(改动最小,兼容现有流程)
|
||
*
|
||
* 依赖:utils/api.js 里的 request / BASE_URL
|
||
*
|
||
* ⚠️ 用模式B之前,必须先把 OSS 域名加进微信后台「上传域名(uploadFile 合法域名)」,
|
||
* 否则真机上会直接 fail(errMsg: url not in domain list)。
|
||
*/
|
||
|
||
const api = require('./api');
|
||
|
||
const SIGN_API = '/api/v1/oss-demo/signature';
|
||
const CONFIRM_API = '/api/v1/oss-demo/confirm';
|
||
const SERVER_UPLOAD_API = '/api/v1/oss-demo/upload';
|
||
|
||
/** 支持的扩展名(与后端白名单保持一致) */
|
||
const ALLOWED_EXT = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'];
|
||
/** 单文件上限,需与后端 OSS_MAX_SIZE 一致,前端只是提前拦截省流量 */
|
||
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
||
|
||
function extOf(path) {
|
||
const m = /\.([a-zA-Z0-9]+)(?:\?.*)?$/.exec(path || '');
|
||
return m ? m[1].toLowerCase() : '';
|
||
}
|
||
|
||
function baseNameOf(path) {
|
||
const noQuery = String(path || '').split('?')[0];
|
||
const parts = noQuery.split('/');
|
||
return parts[parts.length - 1] || 'upload.jpg';
|
||
}
|
||
|
||
/**
|
||
* 前端先做一次类型/大小拦截,避免无谓的上传流量。
|
||
* @param {string} filePath
|
||
* @param {number} [size] wx.chooseMedia 返回的 size(字节),不传则跳过体积校验
|
||
*/
|
||
function checkFile(filePath, size) {
|
||
const ext = extOf(filePath);
|
||
if (ALLOWED_EXT.indexOf(ext) === -1) {
|
||
return { ok: false, msg: '仅支持 jpg/png/gif/webp/bmp 图片' };
|
||
}
|
||
if (size && size > MAX_FILE_SIZE) {
|
||
return { ok: false, msg: '单张图片不能超过 ' + Math.round(MAX_FILE_SIZE / 1024 / 1024) + 'MB' };
|
||
}
|
||
return { ok: true };
|
||
}
|
||
|
||
/**
|
||
* 模式B:取签名 → 直传 OSS → 回执后端
|
||
* @param {string} filePath wx.chooseMedia 返回的 tempFilePath
|
||
* @param {object} options { planType, periodKey, filename, onProgress }
|
||
* @returns {Promise<{key:string,url:string,signedUrl:string}>}
|
||
*/
|
||
function uploadDirect(filePath, options) {
|
||
const opts = options || {};
|
||
const filename = opts.filename || baseNameOf(filePath);
|
||
|
||
const guard = checkFile(filePath);
|
||
if (!guard.ok) return Promise.reject({ error: guard.msg });
|
||
|
||
const query = [
|
||
'plan_type=' + encodeURIComponent(opts.planType || ''),
|
||
'period_key=' + encodeURIComponent(opts.periodKey || ''),
|
||
'filename=' + encodeURIComponent(filename)
|
||
].join('&');
|
||
|
||
return api.request({ url: SIGN_API + '?' + query }).then((sign) => {
|
||
if (!sign || !sign.success) {
|
||
return Promise.reject({ error: (sign && sign.error) || '获取上传签名失败' });
|
||
}
|
||
|
||
// 注意:formData 的字段名由 OSS 约定,不能改名;
|
||
// `key` 决定对象落到哪个路径;`file` 必须是最后一个表单域(原生实现已保证)。
|
||
const formData = Object.assign({}, sign.form_data, {
|
||
key: sign.key,
|
||
'x-oss-content-type': sign.content_type || 'image/jpeg'
|
||
});
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const task = wx.uploadFile({
|
||
// 直传的地址是 OSS Bucket 域名,不是自家后端
|
||
url: sign.host,
|
||
filePath,
|
||
name: 'file',
|
||
formData,
|
||
timeout: 60000,
|
||
success(res) {
|
||
// success_action_status=200 时成功即 200,返回体为空
|
||
if (res.statusCode === 200 || res.statusCode === 204) {
|
||
resolve({ key: sign.key, host: sign.host });
|
||
} else {
|
||
reject({ error: '上传 OSS 失败(' + res.statusCode + ')', raw: res.data });
|
||
}
|
||
},
|
||
fail(err) {
|
||
const msg = (err && err.errMsg) || '';
|
||
if (/url not in domain list|domain not/i.test(msg)) {
|
||
reject({ error: 'OSS 域名未加入小程序上传域名白名单' });
|
||
return;
|
||
}
|
||
reject({ error: msg || '上传失败' });
|
||
}
|
||
});
|
||
|
||
if (opts.onProgress && task && task.onProgressUpdate) {
|
||
task.onProgressUpdate((p) => opts.onProgress(p.progress));
|
||
}
|
||
});
|
||
}).then((uploaded) => {
|
||
// 直传时后端不知情,必须回执一次:后端会校验对象确实存在,并写业务表
|
||
return api.request({
|
||
url: CONFIRM_API,
|
||
method: 'POST',
|
||
data: { key: uploaded.key, size: 0 }
|
||
}).then((res) => ({
|
||
key: uploaded.key,
|
||
url: (res && res.url) || '',
|
||
signedUrl: (res && res.signed_url) || ''
|
||
}));
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 模式B-批量:顺序上传(串行避免并发把弱网打爆)
|
||
* @returns {Promise<Array<{key:string,url:string}>>}
|
||
*/
|
||
function uploadDirectBatch(filePaths, options) {
|
||
const results = [];
|
||
return filePaths.reduce((chain, path) => {
|
||
return chain.then(() => uploadDirect(path, options)).then((r) => {
|
||
results.push(r);
|
||
if (options && options.onItemDone) options.onItemDone(r, results.length);
|
||
});
|
||
}, Promise.resolve()).then(() => results);
|
||
}
|
||
|
||
/**
|
||
* 模式A:走自家后端中转(与现有 plan-submit.js 的写法一致)
|
||
* 好处是不用配 OSS 域名白名单,改造时把 url 从 /plan/upload 换成本接口即可。
|
||
*/
|
||
function uploadViaServer(filePath, options) {
|
||
const opts = options || {};
|
||
const token = wx.getStorageSync('token');
|
||
|
||
return new Promise((resolve, reject) => {
|
||
wx.uploadFile({
|
||
url: api.BASE_URL + SERVER_UPLOAD_API,
|
||
filePath,
|
||
name: 'files',
|
||
header: token ? { Authorization: 'Bearer ' + token } : {},
|
||
formData: {
|
||
plan_type: opts.planType || 'weekly',
|
||
period_key: opts.periodKey || ''
|
||
},
|
||
success(res) {
|
||
let data = {};
|
||
try {
|
||
data = JSON.parse(res.data);
|
||
} catch (e) {
|
||
reject({ error: '返回格式异常' });
|
||
return;
|
||
}
|
||
if (res.statusCode >= 200 && res.statusCode < 300 && data.success) {
|
||
resolve({ key: (data.attachments || [])[0] || '', items: data.items || [] });
|
||
} else {
|
||
reject({ error: data.detail || data.error || '上传失败' });
|
||
}
|
||
},
|
||
fail: reject
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 私有 Bucket 取临时可读 URL(给 <image src> 用)
|
||
* key 必须是 attachments/{userId}/... 形式
|
||
*/
|
||
function getFileUrl(key) {
|
||
return api.request({
|
||
url: '/api/v1/oss-demo/file-url?key=' + encodeURIComponent(key)
|
||
}).then((res) => (res && res.url) || '');
|
||
}
|
||
|
||
module.exports = {
|
||
uploadDirect,
|
||
uploadDirectBatch,
|
||
uploadViaServer,
|
||
getFileUrl,
|
||
ALLOWED_EXT
|
||
};
|