| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656 | <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>		<upload-image v-if="fileMediatype === 'image' && showType === 'grid'" :readonly="readonly"			:image-styles="imageStyles" :files-list="filesList" :limit="limitLength" :disablePreview="disablePreview"			:delIcon="delIcon" @uploadFiles="uploadFiles" @choose="choose" @delFile="delFile">			<slot>				<view class="is-add">					<view class="icon-add"></view>					<view class="icon-add rotate"></view>				</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'	let fileInput = null	/**	 * FilePicker 文件选择上传	 * @description 文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间	 * @tutorial https://ext.dcloud.net.cn/plugin?id=4079	 * @property {Object|Array}	value	组件数据,通常用来回显 ,类型由return-type属性决定	 * @property {Boolean}	disabled = [true|false]	组件禁用	 * 	@value true 	禁用	 * 	@value false 	取消禁用	 * @property {Boolean}	readonly = [true|false]	组件只读,不可选择,不显示进度,不显示删除按钮	 * 	@value true 	只读	 * 	@value false 	取消只读	 * @property {String}	return-type = [array|object]	限制 value 格式,当为 object 时 ,组件只能单选,且会覆盖	 * 	@value array	规定 value 属性的类型为数组	 * 	@value object	规定 value 属性的类型为对象	 * @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: 'uniFilePicker',		components: {			uploadImage,			uploadFile		},		options: {			virtualHost: true		},		emits: ['select', 'success', 'fail', 'progress', 'delete', 'update:modelValue', 'input'],		props: {			// #ifdef VUE3			modelValue: {				type: [Array, Object],				default () {					return []				}			},			// #endif			// #ifndef VUE3			value: {				type: [Array, Object],				default () {					return []				}			},			// #endif			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			},			returnType: {				type: String,				default: 'array'			},			sizeType: {				type: Array,				default () {					return ['original', 'compressed']				}			}		},		data() {			return {				files: [],				localValue: []			}		},		watch: {			// #ifndef VUE3			value: {				handler(newVal, oldVal) {					this.setValue(newVal, oldVal)				},				immediate: true			},			// #endif			// #ifdef VUE3			modelValue: {				handler(newVal, oldVal) {					this.setValue(newVal, oldVal)				},				immediate: true			},			// #endif		},		computed: {			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() {			// TODO 兼容不开通服务空间的情况			if (!(uniCloud.config && uniCloud.config.provider)) {				this.noSpace = true				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: {			/**			 * 公开用户使用,清空文件			 * @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)			},			/**			 * 选择文件			 */			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 || this.noSpace) {					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) {				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 = []				if (this.returnType === 'object') {					data = this.backObject(this.files)[0]					this.localValue = data?data:null				} else {					data = this.backObject(this.files)					if (!this.localValue) {						this.localValue = []					}					this.localValue = [...data]				}				// #ifdef VUE3				this.$emit('update:modelValue', this.localValue)				// #endif				// #ifndef VUE3				this.$emit('input', this.localValue)				// #endif			},			/**			 * 处理返回参数			 * @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>	.uni-file-picker {		/* #ifndef APP-NVUE */		box-sizing: border-box;		overflow: hidden;		width: 100%;		/* #endif */		flex: 1;	}	.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;	}	.icon-add {		width: 50px;		height: 5px;		background-color: #f1f1f1;		border-radius: 2px;	}	.rotate {		position: absolute;		transform: rotate(90deg);	}</style>
 |