diff --git a/pages.json b/pages.json index 4cde9e2..521f428 100644 --- a/pages.json +++ b/pages.json @@ -277,7 +277,7 @@ } }, { - "path": "pages/feedback/index", + "path": "pages/feedback/evaluate", "style": { "navigationBarTitleText": "投诉建议" } diff --git a/pages/feedback/index.vue b/pages/feedback/evaluate.vue similarity index 97% rename from pages/feedback/index.vue rename to pages/feedback/evaluate.vue index 5f6ae75..8e69bd1 100644 --- a/pages/feedback/index.vue +++ b/pages/feedback/evaluate.vue @@ -28,7 +28,7 @@ 图片(选填) - + - - \ No newline at end of file diff --git a/pages/login.vue b/pages/login.vue index f8ffee0..d8affed 100644 --- a/pages/login.vue +++ b/pages/login.vue @@ -88,7 +88,7 @@ export default { } }, onShow() { - uni.reLaunch({ url: '/pages/feedback/index' }) + // uni.reLaunch({ url: '/pages/feedback/evaluate' }) // setTimeout(()=>{ if(Cookies.get('remember')){ this.remember = [Cookies.get('remember')] || []; diff --git a/pages/mine/index.vue b/pages/mine/index.vue index e6991f1..307aba7 100644 --- a/pages/mine/index.vue +++ b/pages/mine/index.vue @@ -41,7 +41,7 @@ - + diff --git a/pages/mine/me/bindingPhone.vue b/pages/mine/me/bindingPhone.vue index e3b6009..84303ba 100644 --- a/pages/mine/me/bindingPhone.vue +++ b/pages/mine/me/bindingPhone.vue @@ -42,14 +42,16 @@ + + + + + + diff --git a/uni_modules/xe-upload/package.json b/uni_modules/xe-upload/package.json new file mode 100644 index 0000000..b93f8b7 --- /dev/null +++ b/uni_modules/xe-upload/package.json @@ -0,0 +1,80 @@ +{ + "id": "xe-upload", + "displayName": "文件选择、文件上传组件(图片,视频,文件等)", + "version": "1.0.2", + "description": "H5、微信小程序、App端支持图片,视频,文件选择上传;其他端暂不支持文件选择上传", + "keywords": [ + "App、H5、微信小程序、图片,视频,文件上传" +], + "repository": "", +"engines": { + }, + "dcloudext": { + "type": "component-vue", + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "" + }, + "uni_modules": { + "dependencies": [], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "Vue": { + "vue2": "y", + "vue3": "u" + }, + "App": { + "app-vue": "y", + "app-nvue": "u" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "u", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y", + "钉钉": "y", + "快手": "y", + "飞书": "y", + "京东": "y" + }, + "快应用": { + "华为": "y", + "联盟": "y" + } + } + } + } +} \ No newline at end of file diff --git a/uni_modules/xe-upload/readme.md b/uni_modules/xe-upload/readme.md new file mode 100644 index 0000000..9002295 --- /dev/null +++ b/uni_modules/xe-upload/readme.md @@ -0,0 +1,78 @@ +# xe-upload + +## 说明 + +不占用页面位置的上传组件; + +H5、APP、微信小程序中可上传图片,视频和文件;其他端暂时只能上传图片和视频 + +> 上传图片通过[chooseMedia](https://uniapp.dcloud.net.cn/api/media/video.html#choosemedia)及[chooseImage](https://uniapp.dcloud.net.cn/api/media/image.html#chooseimage)实现 + +> 上传视频通过[chooseMedia](https://uniapp.dcloud.net.cn/api/media/video.html#choosemedia)及[chooseVideo](https://uniapp.dcloud.net.cn/api/media/video.html#choosevideo)实现 + +> H5端上传文件通过[chooseFile](https://uniapp.dcloud.net.cn/api/media/file.html#wx-choosemessagefile)实现 + +> APP上传文件通过[renderjs](https://uniapp.dcloud.net.cn/tutorial/renderjs.html#renderjs)实现 + +> 微信小程序上传文件通过[chooseMessageFile](https://developers.weixin.qq.com/miniprogram/dev/api/media/image/wx.chooseMessageFile.html)实现 + + +## 使用 + +Attributes + +| 参数 | 说明 | 类型 | 默认值 | +| ----------- | ----------- | ----------- | ----------- | +| options | 请求配置(参数与uni.uploadFile的参数一致) | object | { name: 'file' } | + +Events + +| 事件名 | 说明 | 参数 | +| ----------- | ----------- | ----------- | +| callback | 接收数据 | { type, data } | + +callback type + +| 参数 | 说明 | +| ----------- | ----------- | +| warning | 提示信息(下文称warning回调) | +| success | 上传成功(下文称success回调) | +| choose | 选择文件(下文称choose回调) | + +callback data + +``` +'callback.type === success' : [ + { + "size": 176579, // 选择的文件的大小 + "name": "Kafka.pdf", // 选择的文件的名称(小程序端可能会没有) + "type": "application/pdf", + "tempFilePath": "blob:http://192.168.137.1:8080/2585769b-3195-4f3d-b9f8-d9e99f55deec", // 临时路路径 + "fileType": "file", // 文件类型[image, video, file] + "response": { + "result": { + "fileName": "Kafka.pdf", + "filePath": `http://localhost:3000/upload/e51d814b649122fc64892d0bc6383d07.pdf`, + }, + "success": true, + }, // 上传返回的信息 + } +] + +'callback.type === choose' : [ + { + "size": 176579, // 选择的文件的大小 + "name": "Kafka.pdf", // 选择的文件的名称(小程序端可能会没有) + "type": "application/pdf", + "tempFilePath": "blob:http://192.168.137.1:8080/4204e460-f185-4fc9-9f4d-1bc50ab06981", // 文件临时路径 + "fileType": "file", // 文件类型[image, video, file] + } +] +``` + +## 注意事项 +#### 1、options入参中如果url为空,则choose回调的data列表中只有选择文件能得到的信息和临时路径,临时路径可用于自定义上传方法(APP除外);传入url选择文件后会自动上传到服务器,此时choose回调不会触发,而是执行success回调,success回调的data列表会包括选择文件能得到的信息 +#### 2、APP端文件建议直接上传到服务器,拿到文件上传后的地址再进行其他操作(目前测试APP端file转换后的Blob Url无法用于uni.uploadFile,所以建议APP文件直接上传) +#### 3、APP端文件暂时支持单个上传 +#### 4、当uni.chooseMedia可用时,会优先使用uni.chooseMedia +#### 5、具体使用可下载示例项目运行查看完整示例 diff --git a/uni_modules/xe-upload/tools/apis.js b/uni_modules/xe-upload/tools/apis.js new file mode 100644 index 0000000..6863c1a --- /dev/null +++ b/uni_modules/xe-upload/tools/apis.js @@ -0,0 +1,177 @@ +// eslint-disable +import { awaitWrap } from './tools'; +/** + * 从本地相册选择图片或使用相机拍照 + * @param {object} config 参数详情 => https://uniapp.dcloud.net.cn/api/media/image.html#chooseimage + * @returns + */ +export const chooseImage = (config) => { + return awaitWrap( + new Promise((r, j) => { + uni.chooseImage({ + ...config, + success: (res) => { + const tmpFiles = res?.tempFiles.map((e) => ({ + tempFilePath: e.path, + tempFile: e, + size: e.size, + name: e.name, + type: e.type, + fileType: 'image', + })); + return r({ type: 'image', ...res, tempFiles: tmpFiles }); + }, + fail: (err) => j({ mode: 'chooseImage', data: err }), + }); + }) + ); +}; + +/** + * 拍摄视频或从手机相册中选视频,返回视频的临时文件路径 + * @param {object} config 参数详情 => https://uniapp.dcloud.net.cn/api/media/video.html#choosevideo + * @returns + */ +export const chooseVideo = (config) => { + return awaitWrap( + new Promise((r, j) => { + uni.chooseVideo({ + ...config, + success: (res) => { + const tmpFiles = [{ + ...res, + tempFilePath: res.tempFilePath, + tempFile: res.tempFile ?? {}, + size: res.size, + name: res.name, + type: res.tempFile?.type, + fileType: 'video', + }]; + return r({ type: 'video', tempFiles: tmpFiles }); + }, + fail: (err) => j({ mode: 'chooseVideo', data: err }), + }); + }) + ); +}; + +/** + * 拍摄或从手机相册中选择图片或视频 + * @param {object} config 参数详情 => https://uniapp.dcloud.net.cn/api/media/video.html#choosemedia + * @returns + */ +export const chooseMedia = (type, config) => { + if (!type) return console.error('chooseMedia type cannot be empty'); + if (!uni.chooseMedia && type === 'image') return chooseImage(config); + if (!uni.chooseMedia && type === 'video') return chooseVideo(config); + return awaitWrap( + new Promise((r, j) => { + uni.chooseMedia({ + ...config, + mediaType: [type], + success: (res) => r(res), + fail: (err) => j({ mode: 'chooseMedia', data: err }), + }); + }) + ); +}; + +/** + * 从本地选择文件(h5) + * @param {object} config 参数详情 => https://uniapp.dcloud.net.cn/api/media/file.html#wx-choosemessagefile + * @returns + */ +export const chooseFile = (config) => { + return awaitWrap( + new Promise((r, j) => { + uni.chooseFile({ + ...config, + success: (res) => { + const tmpFiles = res?.tempFiles.map((e) => { + let tmpType = 'file'; + if (e.type.includes('image')) { + tmpType = 'image'; + } + if (e.type.includes('video')) { + tmpType = 'video'; + } + return { + tempFilePath: e.path, + tempFile: e, + size: e.size, + name: e.name, + type: e.type, + fileType: tmpType, + }; + }); + return r({ type: 'file', ...res, tempFiles: tmpFiles }); + }, + fail: (err) => j({ mode: 'chooseFile', data: err }), + }); + }) + ); +}; + +/** + * 从本地选择文件(微信小程序) + * @param {object} config 参数详情 => https://developers.weixin.qq.com/miniprogram/dev/api/media/image/wx.chooseMessageFile.html + * @returns + */ +export const chooseMessageFile = (config) => { + return awaitWrap( + new Promise((r, j) => { + wx.chooseMessageFile({ + ...config, + success: (res) => { + const tmpFiles = res?.tempFiles.map((e) => ({ + ...e, + tempFilePath: e.path, + fileType: e.type ?? 'file', + })); + return r({ type: 'file', ...res, tempFiles: tmpFiles }); + }, + fail: (err) => j({ mode: 'chooseMessageFile', data: err }), + }); + }) + ); +}; + +/** + * 上传 + * @param {object} config 参数详情 => https://uniapp.dcloud.net.cn/api/request/network-file.html#uploadfile + * @param {object} exts 选择的文件的数据 + * @returns {object} exts + response + */ +export const uploadFile = (config, exts = {}) => { + return new Promise((r, j) => { + uni.uploadFile({ + ...config, + success: (res) => r({ ...exts, response: JSON.parse(res.data) }), + fail: (err) => j({ mode: 'uploadFile', data: err }), + }); + }); +}; + +export const appUploadFile = (config, exts = {}, onprogress) => { + const { url, header, formData } = config; + return new Promise((r, j) => { + const xhr = new XMLHttpRequest(); + xhr.open('POST', url, true); + for (let key in header) { + xhr.setRequestHeader(key, header[key]); + } + if (onprogress) { + xhr.upload.onprogress = onprogress; + } + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + r({ ...exts, response: JSON.parse(xhr.responseText) }); + } else { + j({ mode: 'uploadFile', data: { data: xhr.responseText, errMsg: 'uploadFile fail.' } }); + } + } + } + xhr.send(formData); + }); +}; diff --git a/uni_modules/xe-upload/tools/tools.js b/uni_modules/xe-upload/tools/tools.js new file mode 100644 index 0000000..28009b7 --- /dev/null +++ b/uni_modules/xe-upload/tools/tools.js @@ -0,0 +1,180 @@ +// eslint-disable +export const isObject = (obj) => { + return obj + ? Object.prototype.toString.call(obj) === "[object Object]" + : false; +}; +export const isArray = (arr) => { + return arr ? Array.isArray(arr) : false; +}; +/** + * handle async await + * @param {*} promise promise + */ +export const awaitWrap = (promise) => + promise.then((res) => [null, res]).catch((err) => [err, {}]); +/** + * 深拷贝 + * @param {*} source + */ +export const deepClone = (source) => { + if (!isObject(source) && !isArray(source)) return source; + const targetObj = isArray(source) ? [] : {}; // 判断复制的目标是数组还是对象 + for (let keys in source) { + // 遍历目标 + if (source.hasOwnProperty(keys)) { + if (source[keys] && typeof source[keys] === "object") { + // 如果值是对象,就递归一下 + targetObj[keys] = isArray(source[keys]) ? [] : {}; + targetObj[keys] = deepClone(source[keys]); + } else { + // 如果不是,就直接赋值 + targetObj[keys] = source[keys]; + } + } + } + return targetObj; +}; +/** + * @description JS对象深度合并 + * @param {object} target 需要拷贝的对象 + * @param {object} source 拷贝的来源对象 + * @returns {object|boolean} 深度合并后的对象或者false(入参有不是对象) + */ +export const deepMerge = (target = {}, source = {}) => { + target = deepClone(target); + if (typeof target !== "object" || typeof source !== "object") return false; + for (const prop in source) { + if (!source.hasOwnProperty(prop)) continue; + if (prop in target) { + if (typeof target[prop] !== "object") { + target[prop] = source[prop]; + } else if (typeof source[prop] !== "object") { + target[prop] = source[prop]; + } else if (target[prop].concat && source[prop].concat) { + target[prop] = target[prop].concat(source[prop]); + } else { + target[prop] = deepMerge(target[prop], source[prop]); + } + } else { + target[prop] = source[prop]; + } + } + return target; +}; +/** + * 将File对象转为 Blob Url + * @param {File} File对象 + * @returns Blob Url + */ +export const fileToBlob = (file) => { + if (!file) return; + const fileType = file.type; + const blob = new Blob([file], { type: fileType || 'application/*' }); + const blobUrl = window.URL.createObjectURL(blob); + return blobUrl; +}; +/** + * 将File对象转为 base64 + * @param {File} File对象 + * @returns base64 + */ +export const fileToBase64 = (file) => { + if (!file) return; + return new Promise((r, j) => { + const reader = new FileReader(); + reader.onloadend = () => { + const base64String = reader.result; + r(base64String); + }; + reader.onerror = () => { + j({ mode: 'fileToBase64', data: { errMsg: 'File to base64 fail.' } }); + }; + reader.readAsDataURL(file); + }); +}; +/** + * base64转临时路径(改自https://github.com/zhetengbiji/image-tools/blob/master/index.js) + * @param base64 + * @returns + */ +function dataUrlToBase64(str) { + var array = str.split(','); + return array[array.length - 1]; +}; +function biggerThan(v1, v2) { + var v1Array = v1.split('.'); + var v2Array = v2.split('.'); + var update = false; + for (var index = 0; index < v2Array.length; index++) { + var diff = v1Array[index] - v2Array[index]; + if (diff !== 0) { + update = diff > 0; + break; + } + } + return update; +}; +var index = 0; +function getNewFileId() { + return Date.now() + String(index++); +}; +export const base64ToPath = (base64, name = '') => { + return new Promise((r, j) => { + if (typeof plus !== 'object') { + return j(new Error('not support')); + } + var fileName = ''; + if (name) { + const names = name.split('.'); + const extName = names.splice(-1); + fileName = `${names.join('.')}-${getNewFileId()}.${extName}`; + } else { + const names = base64.split(',')[0].match(/data\:\S+\/(\S+);/); + if (!names) { + j(new Error('base64 error')); + } + const extName = names[1]; + fileName = `${getNewFileId()}.${extName}`; + } + var basePath = '_doc'; + var dirPath = 'uniapp_temp'; + var filePath = `${basePath}/${dirPath}/${fileName}`; + if (!biggerThan(plus.os.name === 'Android' ? '1.9.9.80627' : '1.9.9.80472', plus.runtime.innerVersion)) { + plus.io.resolveLocalFileSystemURL(basePath, function (entry) { + entry.getDirectory(dirPath, { + create: true, + exclusive: false, + }, function (entry) { + entry.getFile(fileName, { + create: true, + exclusive: false, + }, function (entry) { + entry.createWriter(function (writer) { + writer.onwrite = function () { + r(filePath); + } + writer.onerror = j; + writer.seek(0); + writer.writeAsBinary(dataUrlToBase64(base64)); + }, j) + }, j) + }, j) + }, j) + return; + } + var bitmap = new plus.nativeObj.Bitmap(fileName); + bitmap.loadBase64Data(base64, function () { + bitmap.save(filePath, {}, function () { + bitmap.clear(); + r(filePath); + }, function (error) { + bitmap.clear(); + j(error); + }); + }, function (error) { + bitmap.clear(); + j(error); + }); + }); +};