123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693 |
- <!-- 文件上传,基于 upload-file 和 upload-image 实现 -->
- <template>
- <view class="uni-file-picker">
- <view v-if="title" class="uni-file-picker__header">
- <text class="file-title">{{ title }}</text>
- <text class="file-count">{{ filesList.length }}/{{ limitLength }}</text>
- </view>
- <view v-if="subtitle" class="file-subtitle">
- <view>{{ subtitle }}</view>
- </view>
- <upload-image
- v-if="fileMediatype === 'image' && showType === 'grid'"
- :readonly="readonly"
- :image-styles="imageStyles"
- :files-list="url"
- :limit="limitLength"
- :disablePreview="disablePreview"
- :delIcon="delIcon"
- @uploadFiles="uploadFiles"
- @choose="choose"
- @delFile="delFile"
- >
- <slot>
- <view class="is-add">
- <image :src="imgsrc" class="add-icon"></image>
- </view>
- </slot>
- </upload-image>
- <upload-file
- v-if="fileMediatype !== 'image' || showType !== 'grid'"
- :readonly="readonly"
- :list-styles="listStyles"
- :files-list="filesList"
- :showType="showType"
- :delIcon="delIcon"
- @uploadFiles="uploadFiles"
- @choose="choose"
- @delFile="delFile"
- >
- <slot><button type="primary" size="mini">选择文件</button></slot>
- </upload-file>
- </view>
- </template>
- <script>
- import { chooseAndUploadFile, uploadCloudFiles } from './choose-and-upload-file.js';
- import {
- get_file_ext,
- get_extname,
- get_files_and_is_max,
- get_file_info,
- get_file_data,
- } from './utils.js';
- import uploadImage from './upload-image.vue';
- import uploadFile from './upload-file.vue';
- import sheep from '@/sheep';
- let fileInput = null;
- /**
- * FilePicker 文件选择上传
- * @description 文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间
- * @tutorial https://ext.dcloud.net.cn/plugin?id=4079
- * @property {Object|Array} value 组件数据,通常用来回显 ,类型由return-type属性决定
- * @property {String|Array} url url数据
- * @property {Boolean} disabled = [true|false] 组件禁用
- * @value true 禁用
- * @value false 取消禁用
- * @property {Boolean} readonly = [true|false] 组件只读,不可选择,不显示进度,不显示删除按钮
- * @value true 只读
- * @value false 取消只读
- * @property {Boolean} disable-preview = [true|false] 禁用图片预览,仅 mode:grid 时生效
- * @value true 禁用图片预览
- * @value false 取消禁用图片预览
- * @property {Boolean} del-icon = [true|false] 是否显示删除按钮
- * @value true 显示删除按钮
- * @value false 不显示删除按钮
- * @property {Boolean} auto-upload = [true|false] 是否自动上传,值为true则只触发@select,可自行上传
- * @value true 自动上传
- * @value false 取消自动上传
- * @property {Number|String} limit 最大选择个数 ,h5 会自动忽略多选的部分
- * @property {String} title 组件标题,右侧显示上传计数
- * @property {String} mode = [list|grid] 选择文件后的文件列表样式
- * @value list 列表显示
- * @value grid 宫格显示
- * @property {String} file-mediatype = [image|video|all] 选择文件类型
- * @value image 只选择图片
- * @value video 只选择视频
- * @value all 选择所有文件
- * @property {Array} file-extname 选择文件后缀,根据 file-mediatype 属性而不同
- * @property {Object} list-style mode:list 时的样式
- * @property {Object} image-styles 选择文件后缀,根据 file-mediatype 属性而不同
- * @event {Function} select 选择文件后触发
- * @event {Function} progress 文件上传时触发
- * @event {Function} success 上传成功触发
- * @event {Function} fail 上传失败触发
- * @event {Function} delete 文件从列表移除时触发
- */
- export default {
- name: 'sUploader',
- components: {
- uploadImage,
- uploadFile,
- },
- options: {
- virtualHost: true,
- },
- emits: ['select', 'success', 'fail', 'progress', 'delete', 'update:modelValue', 'update:url'],
- props: {
- modelValue: {
- type: [Array, Object],
- default() {
- return [];
- },
- },
- url: {
- type: [Array, String],
- default() {
- return [];
- },
- },
- disabled: {
- type: Boolean,
- default: false,
- },
- disablePreview: {
- type: Boolean,
- default: false,
- },
- delIcon: {
- type: Boolean,
- default: true,
- },
- // 自动上传
- autoUpload: {
- type: Boolean,
- default: true,
- },
- // 最大选择个数 ,h5只能限制单选或是多选
- limit: {
- type: [Number, String],
- default: 9,
- },
- // 列表样式 grid | list | list-card
- mode: {
- type: String,
- default: 'grid',
- },
- // 选择文件类型 image/video/all
- fileMediatype: {
- type: String,
- default: 'image',
- },
- // 文件类型筛选
- fileExtname: {
- type: [Array, String],
- default() {
- return [];
- },
- },
- title: {
- type: String,
- default: '',
- },
- listStyles: {
- type: Object,
- default() {
- return {
- // 是否显示边框
- border: true,
- // 是否显示分隔线
- dividline: true,
- // 线条样式
- borderStyle: {},
- };
- },
- },
- imageStyles: {
- type: Object,
- default() {
- return {
- width: 'auto',
- height: 'auto',
- };
- },
- },
- readonly: {
- type: Boolean,
- default: false,
- },
- sizeType: {
- type: Array,
- default() {
- return ['original', 'compressed'];
- },
- },
- driver: {
- type: String,
- default: 'local', // local=本地 | oss | unicloud
- },
- subtitle: {
- type: String,
- default: '',
- },
- },
- data() {
- return {
- files: [],
- localValue: [],
- imgsrc: sheep.$url.static('/static/images/add.png'),
- };
- },
- watch: {
- modelValue: {
- handler(newVal, oldVal) {
- this.setValue(newVal, oldVal);
- },
- immediate: true,
- },
- url: {
- handler(newUrl) {
- this.updateFilesFromUrl(newUrl);
- },
- immediate: true,
- },
- },
- computed: {
- returnType() {
- if (this.limit > 1) {
- return 'array';
- }
- return 'object';
- },
- filesList() {
- let files = [];
- this.files.forEach((v) => {
- files.push(v);
- });
- return files;
- },
- showType() {
- if (this.fileMediatype === 'image') {
- return this.mode;
- }
- return 'list';
- },
- limitLength() {
- if (this.returnType === 'object') {
- return 1;
- }
- if (!this.limit) {
- return 1;
- }
- if (this.limit >= 9) {
- return 9;
- }
- return this.limit;
- },
- },
- created() {
- if (this.driver === 'local') {
- uniCloud.chooseAndUploadFile = chooseAndUploadFile;
- }
- this.form = this.getForm('uniForms');
- this.formItem = this.getForm('uniFormsItem');
- if (this.form && this.formItem) {
- if (this.formItem.name) {
- this.rename = this.formItem.name;
- this.form.inputChildrens.push(this);
- }
- }
- },
- methods: {
- updateFilesFromUrl(newUrl) {
- if (Array.isArray(newUrl)) {
- this.files = newUrl.map((url) => ({
- url,
- status: 'success',
- }));
- } else {
- this.files = newUrl ? [{ url: newUrl, status: 'success' }] : [];
- }
- },
- /**
- * 公开用户使用,清空文件
- * @param {Object} index
- */
- clearFiles(index) {
- if (index !== 0 && !index) {
- this.files = [];
- this.$nextTick(() => {
- this.setEmit();
- });
- } else {
- this.files.splice(index, 1);
- }
- this.$nextTick(() => {
- this.setEmit();
- });
- },
- /**
- * 公开用户使用,继续上传
- */
- upload() {
- let files = [];
- this.files.forEach((v, index) => {
- if (v.status === 'ready' || v.status === 'error') {
- files.push(Object.assign({}, v));
- }
- });
- return this.uploadFiles(files);
- },
- async setValue(newVal, oldVal) {
- const newData = async (v) => {
- const reg = /cloud:\/\/([\w.]+\/?)\S*/;
- let url = '';
- if (v.fileID) {
- url = v.fileID;
- } else {
- url = v.url;
- }
- if (reg.test(url)) {
- v.fileID = url;
- v.url = await this.getTempFileURL(url);
- }
- if (v.url) v.path = v.url;
- return v;
- };
- if (this.returnType === 'object') {
- if (newVal) {
- await newData(newVal);
- } else {
- newVal = {};
- }
- } else {
- if (!newVal) newVal = [];
- for (let i = 0; i < newVal.length; i++) {
- let v = newVal[i];
- await newData(v);
- }
- }
- this.localValue = newVal;
- if (this.form && this.formItem && !this.is_reset) {
- this.is_reset = false;
- this.formItem.setValue(this.localValue);
- }
- let filesData = Object.keys(newVal).length > 0 ? newVal : [];
- this.files = [].concat(filesData);
- this.updateFilesFromUrl(this.url); // 确保在值设置之后调用 updateFilesFromUrl 方法
- },
- /**
- * 选择文件
- */
- choose() {
- if (this.disabled) return;
- if (
- this.files.length >= Number(this.limitLength) &&
- this.showType !== 'grid' &&
- this.returnType === 'array'
- ) {
- uni.showToast({
- title: `您最多选择 ${this.limitLength} 个文件`,
- icon: 'none',
- });
- return;
- }
- this.chooseFiles();
- },
- /**
- * 选择文件并上传
- */
- chooseFiles() {
- const _extname = get_extname(this.fileExtname);
- // 获取后缀
- uniCloud
- .chooseAndUploadFile({
- type: this.fileMediatype,
- compressed: false,
- sizeType: this.sizeType,
- // TODO 如果为空,video 有问题
- extension: _extname.length > 0 ? _extname : undefined,
- count: this.limitLength - this.files.length, //默认9
- onChooseFile: this.chooseFileCallback,
- onUploadProgress: (progressEvent) => {
- this.setProgress(progressEvent, progressEvent.index);
- },
- })
- .then((result) => {
- this.setSuccessAndError(result.tempFiles);
- })
- .catch((err) => {
- console.log('选择失败', err);
- });
- },
- /**
- * 选择文件回调
- * @param {Object} res
- */
- async chooseFileCallback(res) {
- const _extname = get_extname(this.fileExtname);
- const is_one =
- (Number(this.limitLength) === 1 && this.disablePreview && !this.disabled) ||
- this.returnType === 'object';
- // 如果这有一个文件 ,需要清空本地缓存数据
- if (is_one) {
- this.files = [];
- }
- let { filePaths, files } = get_files_and_is_max(res, _extname);
- if (!(_extname && _extname.length > 0)) {
- filePaths = res.tempFilePaths;
- files = res.tempFiles;
- }
- let currentData = [];
- for (let i = 0; i < files.length; i++) {
- if (this.limitLength - this.files.length <= 0) break;
- files[i].uuid = Date.now();
- let filedata = await get_file_data(files[i], this.fileMediatype);
- filedata.progress = 0;
- filedata.status = 'ready';
- this.files.push(filedata);
- currentData.push({
- ...filedata,
- file: files[i],
- });
- }
- this.$emit('select', {
- tempFiles: currentData,
- tempFilePaths: filePaths,
- });
- res.tempFiles = files;
- // 停止自动上传
- if (!this.autoUpload) {
- res.tempFiles = [];
- }
- },
- /**
- * 批传
- * @param {Object} e
- */
- uploadFiles(files) {
- files = [].concat(files);
- return uploadCloudFiles
- .call(this, files, 5, (res) => {
- this.setProgress(res, res.index, true);
- })
- .then((result) => {
- this.setSuccessAndError(result);
- return result;
- })
- .catch((err) => {
- console.log(err);
- });
- },
- /**
- * 成功或失败
- */
- async setSuccessAndError(res, fn) {
- let successData = [];
- let errorData = [];
- let tempFilePath = [];
- let errorTempFilePath = [];
- for (let i = 0; i < res.length; i++) {
- const item = res[i];
- const index = item.uuid ? this.files.findIndex((p) => p.uuid === item.uuid) : item.index;
- if (index === -1 || !this.files) break;
- if (item.errMsg === 'request:fail') {
- this.files[index].url = item.path;
- this.files[index].status = 'error';
- this.files[index].errMsg = item.errMsg;
- // this.files[index].progress = -1
- errorData.push(this.files[index]);
- errorTempFilePath.push(this.files[index].url);
- } else {
- this.files[index].errMsg = '';
- this.files[index].fileID = item.url;
- const reg = /cloud:\/\/([\w.]+\/?)\S*/;
- if (reg.test(item.url)) {
- this.files[index].url = await this.getTempFileURL(item.url);
- } else {
- this.files[index].url = item.url;
- }
- this.files[index].status = 'success';
- this.files[index].progress += 1;
- successData.push(this.files[index]);
- tempFilePath.push(this.files[index].fileID);
- }
- }
- if (successData.length > 0) {
- this.setEmit();
- // 状态改变返回
- this.$emit('success', {
- tempFiles: this.backObject(successData),
- tempFilePaths: tempFilePath,
- });
- }
- if (errorData.length > 0) {
- this.$emit('fail', {
- tempFiles: this.backObject(errorData),
- tempFilePaths: errorTempFilePath,
- });
- }
- },
- /**
- * 获取进度
- * @param {Object} progressEvent
- * @param {Object} index
- * @param {Object} type
- */
- setProgress(progressEvent, index, type) {
- const fileLenth = this.files.length;
- const percentNum = (index / fileLenth) * 100;
- const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
- let idx = index;
- if (!type) {
- idx = this.files.findIndex((p) => p.uuid === progressEvent.tempFile.uuid);
- }
- if (idx === -1 || !this.files[idx]) return;
- // fix by mehaotian 100 就会消失,-1 是为了让进度条消失
- this.files[idx].progress = percentCompleted - 1;
- // 上传中
- this.$emit('progress', {
- index: idx,
- progress: parseInt(percentCompleted),
- tempFile: this.files[idx],
- });
- },
- /**
- * 删除文件
- * @param {Object} index
- */
- delFile(index) {
- console.log(this.files)
- this.$emit('delete', {
- tempFile: this.files[index],
- tempFilePath: this.files[index].url,
- });
- this.files.splice(index, 1);
- this.$nextTick(() => {
- this.setEmit();
- });
- },
- /**
- * 获取文件名和后缀
- * @param {Object} name
- */
- getFileExt(name) {
- const last_len = name.lastIndexOf('.');
- const len = name.length;
- return {
- name: name.substring(0, last_len),
- ext: name.substring(last_len + 1, len),
- };
- },
- /**
- * 处理返回事件
- */
- setEmit() {
- let data = [];
- let updateUrl = [];
- if (this.returnType === 'object') {
- data = this.backObject(this.files)[0];
- this.localValue = data ? data : null;
- updateUrl = data ? data.url : '';
- } else {
- data = this.backObject(this.files);
- if (!this.localValue) {
- this.localValue = [];
- }
- this.localValue = [...data];
- if (this.localValue.length > 0) {
- this.localValue.forEach((item) => {
- updateUrl.push(item.url);
- });
- }
- }
- this.$emit('update:modelValue', this.localValue);
- this.$emit('update:url', updateUrl);
- },
- /**
- * 处理返回参数
- * @param {Object} files
- */
- backObject(files) {
- let newFilesData = [];
- files.forEach((v) => {
- newFilesData.push({
- extname: v.extname,
- fileType: v.fileType,
- image: v.image,
- name: v.name,
- path: v.path,
- size: v.size,
- fileID: v.fileID,
- url: v.url,
- });
- });
- return newFilesData;
- },
- async getTempFileURL(fileList) {
- fileList = {
- fileList: [].concat(fileList),
- };
- const urls = await uniCloud.getTempFileURL(fileList);
- return urls.fileList[0].tempFileURL || '';
- },
- /**
- * 获取父元素实例
- */
- getForm(name = 'uniForms') {
- let parent = this.$parent;
- let parentName = parent.$options.name;
- while (parentName !== name) {
- parent = parent.$parent;
- if (!parent) return false;
- parentName = parent.$options.name;
- }
- return parent;
- },
- },
- };
- </script>
- <style lang="scss" scoped>
- .uni-file-picker {
- /* #ifndef APP-NVUE */
- box-sizing: border-box;
- overflow: hidden;
- /* width: 100%; */
- /* #endif */
- /* flex: 1; */
- position: relative;
- }
- .uni-file-picker__header {
- padding-top: 5px;
- padding-bottom: 10px;
- /* #ifndef APP-NVUE */
- display: flex;
- /* #endif */
- justify-content: space-between;
- }
- .file-title {
- font-size: 14px;
- color: #333;
- }
- .file-count {
- font-size: 14px;
- color: #999;
- }
- .is-add {
- /* #ifndef APP-NVUE */
- display: flex;
- /* #endif */
- align-items: center;
- justify-content: center;
- }
- .add-icon {
- width: 57rpx;
- height: 49rpx;
- }
- .file-subtitle {
- position: absolute;
- left: 50%;
- transform: translateX(-50%);
- bottom: 0;
- width: 140rpx;
- height: 36rpx;
- z-index: 1;
- display: flex;
- justify-content: center;
- color: #fff;
- font-weight: 500;
- background: rgba(#000, 0.3);
- font-size: 24rpx;
- }
- </style>
|