Skip to content
本页导读

Movable

组件类型:UxMovableComponentPublicInstance

支持宫格和列表布局,内置照片墙上传场景,支持多选、编辑、滑动删除、禁用

平台兼容性

UniApp X

AndroidiOSweb鸿蒙 Next小程序
x

UniApp Vue Nvue

AndroidiOSweb鸿蒙 Next小程序
xxxx

Props

属性名类型默认值说明
layoutStringgrid布局
columnNumber4列数
spacingNumber8间隔
aspectRatioNumber1item宽高比仅grid有效
multipleBooleanfalse多选
editableBooleanfalse可编辑
swipeBooleanfalse滑动删除
disabledBooleanfalse禁用

layout

说明
grid宫格
list列表

Events

事件名说明参数
init初始化时触发
change值改变时触发

示例代码

html
<template>
	<ux-page :stack="showDoc">
		<ux-navbar :title="title" :bold="true">
			<template v-slot:right>
				<!-- #ifndef MP -->
				<ux-button theme="text" icon="/static/tip.png" :icon-size="22" @click="onDoc()"></ux-button>
				<!-- #endif -->
			</template>
		</ux-navbar>
		
		<ux-scroll>
			<ux-card direction="column" icon="flag-filled" title="原生拖拽组件" :bold="true">
				<ux-text text="支持宫格和列表布局,内置照片墙上传场景,支持多选、编辑、滑动删除、禁用等功能"></ux-text>
			</ux-card>
			
			<!-- #ifndef MP -->
			<ux-card direction="column" icon="arrowright" title="照片墙" :bold="true">
				<ux-text text="内置照片墙上传场景,支持多选、删除" :mb="15"></ux-text>
				<view style="height: 180px;">
					<ux-movable ref="movableGridRef"
						layout="grid"
						:spacing="6"
						:aspect-ratio="0.75"
						:column="column"
						:multiple="multiple"
						:disabled="disabled"
						@init="initGrid"
						@change="gridChange">
					</ux-movable>
				</view>
				
				<ux-row>
					<ux-button text="列数 + 1" @click="onColumn(1)"></ux-button>
					<ux-button text="列数 - 1" :ml="10" @click="onColumn(-1)"></ux-button>
					<ux-button text="多选" :ml="10" @click="onMultiple()"></ux-button>
				</ux-row>
			</ux-card>
			
			<ux-card direction="column" icon="arrowright" title="列表式" :bold="true">
				<ux-text text="支持滑动删除" :mb="15"></ux-text>
				<view style="height: 180px;">
					<ux-movable ref="movableListRef"
						layout="list"
						:swipe="swipe"
						:disabled="disabled"
						@init="initList"
						@change="listChange">
					</ux-movable>
				</view>
			</ux-card>
			
			<ux-placeholder :height="300">
				<ux-row justify="center" align="center" style="height: 100%;">
					<ux-text prefix-icon="soapbubble-filled" text="真的没有了~"></ux-text>
				</ux-row>
			</ux-placeholder>
			<!-- #endif -->
			
			<!-- #ifdef MP -->
			<ux-placeholder :height="200">
				<ux-row justify="center" align="center" style="height: 100%;">
					<ux-text prefix-icon="soapbubble-filled" text="小程序和Web不支持"></ux-text>
				</ux-row>
			</ux-placeholder>
			<!-- #endif -->
		</ux-scroll>
	</ux-page>
</template>
<template>
	<ux-page :stack="showDoc">
		<ux-navbar :title="title" :bold="true">
			<template v-slot:right>
				<!-- #ifndef MP -->
				<ux-button theme="text" icon="/static/tip.png" :icon-size="22" @click="onDoc()"></ux-button>
				<!-- #endif -->
			</template>
		</ux-navbar>
		
		<ux-scroll>
			<ux-card direction="column" icon="flag-filled" title="原生拖拽组件" :bold="true">
				<ux-text text="支持宫格和列表布局,内置照片墙上传场景,支持多选、编辑、滑动删除、禁用等功能"></ux-text>
			</ux-card>
			
			<!-- #ifndef MP -->
			<ux-card direction="column" icon="arrowright" title="照片墙" :bold="true">
				<ux-text text="内置照片墙上传场景,支持多选、删除" :mb="15"></ux-text>
				<view style="height: 180px;">
					<ux-movable ref="movableGridRef"
						layout="grid"
						:spacing="6"
						:aspect-ratio="0.75"
						:column="column"
						:multiple="multiple"
						:disabled="disabled"
						@init="initGrid"
						@change="gridChange">
					</ux-movable>
				</view>
				
				<ux-row>
					<ux-button text="列数 + 1" @click="onColumn(1)"></ux-button>
					<ux-button text="列数 - 1" :ml="10" @click="onColumn(-1)"></ux-button>
					<ux-button text="多选" :ml="10" @click="onMultiple()"></ux-button>
				</ux-row>
			</ux-card>
			
			<ux-card direction="column" icon="arrowright" title="列表式" :bold="true">
				<ux-text text="支持滑动删除" :mb="15"></ux-text>
				<view style="height: 180px;">
					<ux-movable ref="movableListRef"
						layout="list"
						:swipe="swipe"
						:disabled="disabled"
						@init="initList"
						@change="listChange">
					</ux-movable>
				</view>
			</ux-card>
			
			<ux-placeholder :height="300">
				<ux-row justify="center" align="center" style="height: 100%;">
					<ux-text prefix-icon="soapbubble-filled" text="真的没有了~"></ux-text>
				</ux-row>
			</ux-placeholder>
			<!-- #endif -->
			
			<!-- #ifdef MP -->
			<ux-placeholder :height="200">
				<ux-row justify="center" align="center" style="height: 100%;">
					<ux-text prefix-icon="soapbubble-filled" text="小程序和Web不支持"></ux-text>
				</ux-row>
			</ux-placeholder>
			<!-- #endif -->
		</ux-scroll>
	</ux-page>
</template>
ts
<script setup>
	import { UxMovableItem, UxMovablePlaceholder } from '@/uni_modules/ux-movable'
	import * as plus from '@/uni_modules/ux-plus'
	
	const column = ref(4)
	const disabled = ref(false)
	const multiple = ref(false)
	const swipe = ref(true)
	
	const movableGridRef = ref<ComponentPublicInstance | null>(null)
	const movableListRef = ref<ComponentPublicInstance | null>(null)
	
	const gridDatas = ref<UxMovableItem[]>([])
	const listDatas = ref<UxMovableItem[]>([])
	
	function getData(i: number, url: string): UxMovableItem {
		return {
			id: i,
			index: i + 1,
			label: 'Label' + i,
			url: url,
			radius: 6,
			rightTop: {
				url: '/static/close.png',
				width: 20,
				height: 20,
				margin: 4,
				click: (data: UxMovableItem) => {
					movableGridRef.value?.$callMethod('delData', data)
					
					uni.showToast({
						title: '删除成功',
						icon: 'none'
					})
				}
			},
			selected: {
				checked: false,
				selectedUrl: '/static/checked.png',
				unselectedUrl: '/static/unchecked.png',
				width: 20,
				height: 20,
				margin: 4,
			},
			loading: {
				show: true,
				progress: 0,
				waitingLabel: '等待上传...'
			},
			border: {
				type: 'solid',
				width: 1,
				color: '#f0f0f0'
			},
			click: (data: UxMovableItem) => {
				// 不要直接用data,ios事件会丢失
				let index = gridDatas.value.findIndex(e => e.id == data.id)
				if(multiple.value) {
					gridDatas.value[index].selected!.checked = !gridDatas.value[index].selected!.checked
					movableGridRef.value?.$callMethod('selectData', gridDatas.value[index])
				} else {
					uni.previewImage({
						urls: gridDatas.value.filter((e): boolean => (e.url ?? '') != '').map((e): string => e.url!),
						current: gridDatas.value[index].url
					})
				}
			}
		} as UxMovableItem
	}
	
	function startUpload() {
		// 上传时不能滑动
		disabled.value = true
		
		let index = 0
		let fun = () => {}
		fun = () => {
			if(index >= gridDatas.value.length) {
				disabled.value = false
				return
			}
			
			let e = gridDatas.value[index]
			
			if(e.upload != null) {
				// 忽略上传按钮
				index++
				fun()
				return
			}
			
			if(e.loading == null) {
				// 参数未配置
				index++
				fun()
				return
			}
			
			if(e.loading!.show == false) {
				// 已上传
				index++
				fun()
				return
			}
			
			e.loading!.show = true
			index++
			
			let up = () => {}
			up = () => {
				if(e.loading!.progress >= 100) {
					e.loading!.show = false
					movableGridRef.value?.$callMethod('upload', e)
					fun()
					return
				}
				
				e.loading!.progress += 5
				e.loading!.progress = Math.min(100, e.loading!.progress)
				
				requestAnimationFrame(() => {
					movableGridRef.value?.$callMethod('upload', e)
					up()
				})
			}
			up()
		}
		
		fun()
	}
	
	function onUpload(paths: string[]) {
		console.log(paths);
		// 新增等待上传
		for (let i = 0; i < paths.length; i++) {
			gridDatas.value.push(getData(gridDatas.value.length + i, paths[i]))
		}
		
		movableGridRef.value?.$callMethod('setData', gridDatas.value.map((e: UxMovableItem) => e))
		startUpload()
	}
	
	function getGridDatas() {
		let urls = [
			'https://q9.itc.cn/q_70/images03/20240607/100a8219bc9044e5a712e464525577c3.jpeg',
			'https://iknow-pic.cdn.bcebos.com/8cb1cb1349540923d860dce08058d109b3de4926',
			'https://iknow-pic.cdn.bcebos.com/0823dd54564e9258d93d39708e82d158ccbf4e26',
			'https://iknow-pic.cdn.bcebos.com/18d8bc3eb13533fa91bf2cb0bad3fd1f41345b26',
			'https://iknow-pic.cdn.bcebos.com/960a304e251f95cacdfc2d28db177f3e67095226',
		]
		
		let items = [{
			id: -1,
			index: 0,
			radius: 6,
			upload: {
				url: '/static/add.png',
				backgroundColor: '#f0f0f0',
			},
			click: (data: UxMovableItem) => {
				uni.chooseImage({
					count: 30,
					success: (res) => {
						onUpload(res.tempFilePaths)
					}
				})
			}
		}] as UxMovableItem[]
		
		for (var i = 0; i < urls.length; i++) {
			items.push(getData(i, urls[i]))
		}
		
		return items
	}
	
	function getListDatas() {
		let urls = [
			'https://q9.itc.cn/q_70/images03/20240607/100a8219bc9044e5a712e464525577c3.jpeg',
			'https://iknow-pic.cdn.bcebos.com/8cb1cb1349540923d860dce08058d109b3de4926',
			'https://iknow-pic.cdn.bcebos.com/0823dd54564e9258d93d39708e82d158ccbf4e26',
			'https://iknow-pic.cdn.bcebos.com/18d8bc3eb13533fa91bf2cb0bad3fd1f41345b26',
			'https://iknow-pic.cdn.bcebos.com/960a304e251f95cacdfc2d28db177f3e67095226',
		]
		
		let items = [] as UxMovableItem[]
		for (var i = 0; i < urls.length; i++) {
			items.push({
				id: i,
				index: i + 1,
				label: 'Label' + i,
				url: urls[i],
				radius: 6,
				border: {
					type: 'solid',
					width: 1,
					color: '#f0f0f0'
				}
			} as UxMovableItem)
		}
		
		return items
	}
	
	function initGrid() {
		gridDatas.value = getGridDatas()
		gridDatas.value.forEach((e, i) => {
			e.index = i
		})
		movableGridRef.value?.$callMethod('setData', gridDatas.value.map((e: UxMovableItem) => e))
		startUpload()
	}
	
	function initList() {
		listDatas.value = getListDatas()
		listDatas.value.forEach((e, i) => {
			e.index = i
		})
		movableListRef.value?.$callMethod('setData', listDatas.value.map((e: UxMovableItem) => e))
	}
	
	function gridChange(datas: UxMovableItem[]) {
		gridDatas.value = datas
		console.log(gridDatas.value);
	}
	
	function listChange(datas: UxMovableItem[]) {
		listDatas.value = datas
		console.log(listDatas.value);
	}
	
	function onColumn(n: number) {
		column.value += n
		column.value = Math.max(2, column.value)
		column.value = Math.min(5, column.value)
	}
	
	function onMultiple() {
		multiple.value = !multiple.value
	}
	
	function onSwipe() {
		swipe.value= !swipe.value
	}
	
	function onDisabled() {
		disabled.value = !disabled.value
	}
	
	const showDoc = ref(false)
	function onDoc() {
		plus.openWeb({
			title: '在线文档',
			url: 'https://www.uxframe.cn/component/blur.html',
			// blur: 1,
			success: () => {
				showDoc.value = true
			},
			complete: () => {
				showDoc.value = false
			}
		})
	}
	
	const title = ref('')
	onLoad((e: OnLoadOptions) => {
		title.value = e['title'] ?? ''
	})
</script>
<script setup>
	import { UxMovableItem, UxMovablePlaceholder } from '@/uni_modules/ux-movable'
	import * as plus from '@/uni_modules/ux-plus'
	
	const column = ref(4)
	const disabled = ref(false)
	const multiple = ref(false)
	const swipe = ref(true)
	
	const movableGridRef = ref<ComponentPublicInstance | null>(null)
	const movableListRef = ref<ComponentPublicInstance | null>(null)
	
	const gridDatas = ref<UxMovableItem[]>([])
	const listDatas = ref<UxMovableItem[]>([])
	
	function getData(i: number, url: string): UxMovableItem {
		return {
			id: i,
			index: i + 1,
			label: 'Label' + i,
			url: url,
			radius: 6,
			rightTop: {
				url: '/static/close.png',
				width: 20,
				height: 20,
				margin: 4,
				click: (data: UxMovableItem) => {
					movableGridRef.value?.$callMethod('delData', data)
					
					uni.showToast({
						title: '删除成功',
						icon: 'none'
					})
				}
			},
			selected: {
				checked: false,
				selectedUrl: '/static/checked.png',
				unselectedUrl: '/static/unchecked.png',
				width: 20,
				height: 20,
				margin: 4,
			},
			loading: {
				show: true,
				progress: 0,
				waitingLabel: '等待上传...'
			},
			border: {
				type: 'solid',
				width: 1,
				color: '#f0f0f0'
			},
			click: (data: UxMovableItem) => {
				// 不要直接用data,ios事件会丢失
				let index = gridDatas.value.findIndex(e => e.id == data.id)
				if(multiple.value) {
					gridDatas.value[index].selected!.checked = !gridDatas.value[index].selected!.checked
					movableGridRef.value?.$callMethod('selectData', gridDatas.value[index])
				} else {
					uni.previewImage({
						urls: gridDatas.value.filter((e): boolean => (e.url ?? '') != '').map((e): string => e.url!),
						current: gridDatas.value[index].url
					})
				}
			}
		} as UxMovableItem
	}
	
	function startUpload() {
		// 上传时不能滑动
		disabled.value = true
		
		let index = 0
		let fun = () => {}
		fun = () => {
			if(index >= gridDatas.value.length) {
				disabled.value = false
				return
			}
			
			let e = gridDatas.value[index]
			
			if(e.upload != null) {
				// 忽略上传按钮
				index++
				fun()
				return
			}
			
			if(e.loading == null) {
				// 参数未配置
				index++
				fun()
				return
			}
			
			if(e.loading!.show == false) {
				// 已上传
				index++
				fun()
				return
			}
			
			e.loading!.show = true
			index++
			
			let up = () => {}
			up = () => {
				if(e.loading!.progress >= 100) {
					e.loading!.show = false
					movableGridRef.value?.$callMethod('upload', e)
					fun()
					return
				}
				
				e.loading!.progress += 5
				e.loading!.progress = Math.min(100, e.loading!.progress)
				
				requestAnimationFrame(() => {
					movableGridRef.value?.$callMethod('upload', e)
					up()
				})
			}
			up()
		}
		
		fun()
	}
	
	function onUpload(paths: string[]) {
		console.log(paths);
		// 新增等待上传
		for (let i = 0; i < paths.length; i++) {
			gridDatas.value.push(getData(gridDatas.value.length + i, paths[i]))
		}
		
		movableGridRef.value?.$callMethod('setData', gridDatas.value.map((e: UxMovableItem) => e))
		startUpload()
	}
	
	function getGridDatas() {
		let urls = [
			'https://q9.itc.cn/q_70/images03/20240607/100a8219bc9044e5a712e464525577c3.jpeg',
			'https://iknow-pic.cdn.bcebos.com/8cb1cb1349540923d860dce08058d109b3de4926',
			'https://iknow-pic.cdn.bcebos.com/0823dd54564e9258d93d39708e82d158ccbf4e26',
			'https://iknow-pic.cdn.bcebos.com/18d8bc3eb13533fa91bf2cb0bad3fd1f41345b26',
			'https://iknow-pic.cdn.bcebos.com/960a304e251f95cacdfc2d28db177f3e67095226',
		]
		
		let items = [{
			id: -1,
			index: 0,
			radius: 6,
			upload: {
				url: '/static/add.png',
				backgroundColor: '#f0f0f0',
			},
			click: (data: UxMovableItem) => {
				uni.chooseImage({
					count: 30,
					success: (res) => {
						onUpload(res.tempFilePaths)
					}
				})
			}
		}] as UxMovableItem[]
		
		for (var i = 0; i < urls.length; i++) {
			items.push(getData(i, urls[i]))
		}
		
		return items
	}
	
	function getListDatas() {
		let urls = [
			'https://q9.itc.cn/q_70/images03/20240607/100a8219bc9044e5a712e464525577c3.jpeg',
			'https://iknow-pic.cdn.bcebos.com/8cb1cb1349540923d860dce08058d109b3de4926',
			'https://iknow-pic.cdn.bcebos.com/0823dd54564e9258d93d39708e82d158ccbf4e26',
			'https://iknow-pic.cdn.bcebos.com/18d8bc3eb13533fa91bf2cb0bad3fd1f41345b26',
			'https://iknow-pic.cdn.bcebos.com/960a304e251f95cacdfc2d28db177f3e67095226',
		]
		
		let items = [] as UxMovableItem[]
		for (var i = 0; i < urls.length; i++) {
			items.push({
				id: i,
				index: i + 1,
				label: 'Label' + i,
				url: urls[i],
				radius: 6,
				border: {
					type: 'solid',
					width: 1,
					color: '#f0f0f0'
				}
			} as UxMovableItem)
		}
		
		return items
	}
	
	function initGrid() {
		gridDatas.value = getGridDatas()
		gridDatas.value.forEach((e, i) => {
			e.index = i
		})
		movableGridRef.value?.$callMethod('setData', gridDatas.value.map((e: UxMovableItem) => e))
		startUpload()
	}
	
	function initList() {
		listDatas.value = getListDatas()
		listDatas.value.forEach((e, i) => {
			e.index = i
		})
		movableListRef.value?.$callMethod('setData', listDatas.value.map((e: UxMovableItem) => e))
	}
	
	function gridChange(datas: UxMovableItem[]) {
		gridDatas.value = datas
		console.log(gridDatas.value);
	}
	
	function listChange(datas: UxMovableItem[]) {
		listDatas.value = datas
		console.log(listDatas.value);
	}
	
	function onColumn(n: number) {
		column.value += n
		column.value = Math.max(2, column.value)
		column.value = Math.min(5, column.value)
	}
	
	function onMultiple() {
		multiple.value = !multiple.value
	}
	
	function onSwipe() {
		swipe.value= !swipe.value
	}
	
	function onDisabled() {
		disabled.value = !disabled.value
	}
	
	const showDoc = ref(false)
	function onDoc() {
		plus.openWeb({
			title: '在线文档',
			url: 'https://www.uxframe.cn/component/blur.html',
			// blur: 1,
			success: () => {
				showDoc.value = true
			},
			complete: () => {
				showDoc.value = false
			}
		})
	}
	
	const title = ref('')
	onLoad((e: OnLoadOptions) => {
		title.value = e['title'] ?? ''
	})
</script>
css
<style lang="scss">
	.blur {
		position: absolute;
		top: 200px;
		width: 100%;
		height: 120px;
	}
</style>
<style lang="scss">
	.blur {
		position: absolute;
		top: 200px;
		width: 100%;
		height: 120px;
	}
</style>