|
|
@@ -1,5 +1,259 @@
|
|
|
-import { View } from "@tarojs/components";
|
|
|
+import { useState, useRef, useEffect } from 'react'
|
|
|
+import { View, MovableArea, MovableView, Image } from '@tarojs/components'
|
|
|
+import { createSelectorQuery, chooseImage, showToast } from '@tarojs/taro'
|
|
|
+import './message.scss'
|
|
|
+
|
|
|
+interface ImageItem {
|
|
|
+ id: string
|
|
|
+ url: string
|
|
|
+}
|
|
|
+
|
|
|
+interface Position {
|
|
|
+ x: number
|
|
|
+ y: number
|
|
|
+}
|
|
|
+
|
|
|
+interface Props {
|
|
|
+ maxCount?: number
|
|
|
+ value?: ImageItem[]
|
|
|
+ onChange?: (images: ImageItem[]) => void
|
|
|
+}
|
|
|
+
|
|
|
+export default function SortableGrid({
|
|
|
+ maxCount = 9,
|
|
|
+ value = [],
|
|
|
+ onChange
|
|
|
+}: Props) {
|
|
|
+ const [images, setImages] = useState<ImageItem[]>(value)
|
|
|
+ const [currentItem, setCurrentItem] = useState<ImageItem | null>(null)
|
|
|
+ const [positions, setPositions] = useState<{[key: string]: Position}>({})
|
|
|
+ const [itemSize, setItemSize] = useState({ width: 0, height: 0 })
|
|
|
+ const [isDragging, setIsDragging] = useState(false)
|
|
|
+ const [draggedIndex, setDraggedIndex] = useState<number>(-1)
|
|
|
+ const [targetIndex, setTargetIndex] = useState<number>(-1)
|
|
|
+ const gridRef = useRef<any>(null)
|
|
|
+
|
|
|
+ // 更新图片列表
|
|
|
+ const updateImages = (newImages: ImageItem[]) => {
|
|
|
+ setImages(newImages)
|
|
|
+ onChange?.(newImages)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理图片上传
|
|
|
+ const handleUpload = async () => {
|
|
|
+ if (images.length >= maxCount) {
|
|
|
+ showToast({
|
|
|
+ title: `最多只能上传${maxCount}张图片`,
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await chooseImage({
|
|
|
+ count: maxCount - images.length,
|
|
|
+ sizeType: ['compressed'],
|
|
|
+ sourceType: ['album', 'camera']
|
|
|
+ })
|
|
|
+
|
|
|
+ const newImages = [
|
|
|
+ ...images,
|
|
|
+ ...res.tempFilePaths.map((url, index) => ({
|
|
|
+ id: Date.now() + index + '',
|
|
|
+ url
|
|
|
+ }))
|
|
|
+ ]
|
|
|
+
|
|
|
+ updateImages(newImages)
|
|
|
+ } catch (error) {
|
|
|
+ console.error('选择图片失败:', error)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 删除图片
|
|
|
+ const handleDelete = (item: ImageItem) => {
|
|
|
+ const newImages = images.filter(img => img.id !== item.id)
|
|
|
+ updateImages(newImages)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算网格项尺寸和位置
|
|
|
+ const calculateLayout = () => {
|
|
|
+ return new Promise<void>((resolve) => {
|
|
|
+ const query = createSelectorQuery()
|
|
|
+ query.select('.grid-container').boundingClientRect((rect: any) => {
|
|
|
+ if (rect) {
|
|
|
+ const width = rect.width / 3
|
|
|
+ setItemSize({ width, height: width })
|
|
|
+
|
|
|
+ const newPositions: {[key: string]: Position} = {}
|
|
|
+ const totalItems = images.length + (images.length < maxCount ? 1 : 0)
|
|
|
+
|
|
|
+ for (let i = 0; i < totalItems; i++) {
|
|
|
+ const row = Math.floor(i / 3)
|
|
|
+ const col = i % 3
|
|
|
+ newPositions[i] = {
|
|
|
+ x: col * width,
|
|
|
+ y: row * width
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ setPositions(newPositions)
|
|
|
+ resolve()
|
|
|
+ }
|
|
|
+ }).exec()
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取目标索引
|
|
|
+ const getTargetIndex = (x: number, y: number): number => {
|
|
|
+ if (!itemSize.width) return -1
|
|
|
+
|
|
|
+ const col = Math.floor((x + itemSize.width / 2) / itemSize.width)
|
|
|
+ const row = Math.floor((y + itemSize.height / 2) / itemSize.height)
|
|
|
+
|
|
|
+ if (col < 0 || col >= 3 || row < 0) return -1
|
|
|
+ const index = row * 3 + col
|
|
|
+ return index >= images.length ? -1 : index
|
|
|
+ }
|
|
|
+
|
|
|
+ // 长按开始拖动
|
|
|
+ const handleLongPress = (index: number) => {
|
|
|
+ setIsDragging(true)
|
|
|
+ setDraggedIndex(index)
|
|
|
+ setCurrentItem(images[index])
|
|
|
+ }
|
|
|
+
|
|
|
+ // 拖动中
|
|
|
+ const handleMove = (e: any) => {
|
|
|
+ if (!isDragging || draggedIndex === -1) return
|
|
|
+
|
|
|
+ const { x, y } = e.detail
|
|
|
+ const newTargetIndex = getTargetIndex(x, y)
|
|
|
+
|
|
|
+ if (newTargetIndex !== -1 && newTargetIndex !== draggedIndex && newTargetIndex < images.length) {
|
|
|
+ setTargetIndex(newTargetIndex)
|
|
|
+ } else {
|
|
|
+ setTargetIndex(-1)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 拖动结束
|
|
|
+ const handleTouchEnd = () => {
|
|
|
+ if (isDragging && targetIndex !== -1 && draggedIndex !== -1) {
|
|
|
+ const newImages = [...images]
|
|
|
+ const [draggedItem] = newImages.splice(draggedIndex, 1)
|
|
|
+ newImages.splice(targetIndex, 0, draggedItem)
|
|
|
+ updateImages(newImages)
|
|
|
+ }
|
|
|
+
|
|
|
+ setIsDragging(false)
|
|
|
+ setDraggedIndex(-1)
|
|
|
+ setTargetIndex(-1)
|
|
|
+ setCurrentItem(null)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取元素样式
|
|
|
+ const getItemStyle = (index: number) => {
|
|
|
+ if (!positions[index]) return {}
|
|
|
+
|
|
|
+ const baseStyle = {
|
|
|
+ transform: `translate3d(${positions[index].x}px, ${positions[index].y}px, 0)`,
|
|
|
+ transition: isDragging ? 'transform 0.3s ease' : 'transform 0.3s ease'
|
|
|
+ }
|
|
|
+
|
|
|
+ if (index === draggedIndex) {
|
|
|
+ return {
|
|
|
+ ...baseStyle,
|
|
|
+ transition: 'none',
|
|
|
+ zIndex: 100
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (targetIndex !== -1) {
|
|
|
+ if (index === targetIndex) {
|
|
|
+ const targetPos = positions[draggedIndex]
|
|
|
+ return {
|
|
|
+ ...baseStyle,
|
|
|
+ transform: `translate3d(${targetPos.x}px, ${targetPos.y}px, 0)`
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (index > targetIndex && index <= draggedIndex) {
|
|
|
+ const nextPos = positions[index - 1]
|
|
|
+ return {
|
|
|
+ ...baseStyle,
|
|
|
+ transform: `translate3d(${nextPos.x}px, ${nextPos.y}px, 0)`
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (index < targetIndex && index >= draggedIndex) {
|
|
|
+ const prevPos = positions[index + 1]
|
|
|
+ return {
|
|
|
+ ...baseStyle,
|
|
|
+ transform: `translate3d(${prevPos.x}px, ${prevPos.y}px, 0)`
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return baseStyle
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算容器高度
|
|
|
+ const getContainerHeight = () => {
|
|
|
+ const totalItems = images.length + (images.length < maxCount ? 1 : 0)
|
|
|
+ const rows = Math.ceil(totalItems / 3)
|
|
|
+ return `${rows * 33.33}vw`
|
|
|
+ }
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ calculateLayout()
|
|
|
+ }, [images.length])
|
|
|
+
|
|
|
+ return (
|
|
|
+ <View className="grid-wrapper" style={{ height: getContainerHeight() }}>
|
|
|
+ <MovableArea className="grid-container" ref={gridRef}>
|
|
|
+ {images.map((item, index) => (
|
|
|
+ <MovableView
|
|
|
+ key={item.id}
|
|
|
+ className={`grid-item ${draggedIndex === index ? 'active' : ''}`}
|
|
|
+ direction="all"
|
|
|
+ x={positions[index]?.x || 0}
|
|
|
+ y={positions[index]?.y || 0}
|
|
|
+ onLongPress={() => handleLongPress(index)}
|
|
|
+ onChange={handleMove}
|
|
|
+ onTouchEnd={handleTouchEnd}
|
|
|
+ damping={50}
|
|
|
+ friction={2}
|
|
|
+ >
|
|
|
+ <Image
|
|
|
+ className="grid-image"
|
|
|
+ src={item.url}
|
|
|
+ mode="aspectFill"
|
|
|
+ />
|
|
|
+ <View
|
|
|
+ className="delete-btn"
|
|
|
+ onClick={(e) => {
|
|
|
+ e.stopPropagation()
|
|
|
+ handleDelete(item)
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </MovableView>
|
|
|
+ ))}
|
|
|
+
|
|
|
+ {images.length < maxCount && (
|
|
|
+ <View
|
|
|
+ className="grid-item upload-item"
|
|
|
+ onClick={handleUpload}
|
|
|
+ style={{
|
|
|
+ transform: `translate3d(${positions[images.length]?.x}px, ${positions[images.length]?.y}px, 0)`,
|
|
|
+ transition: 'transform 0.3s ease'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <View className="upload-icon">+</View>
|
|
|
+ </View>
|
|
|
+ )}
|
|
|
+ </MovableArea>
|
|
|
+ </View>
|
|
|
+ )
|
|
|
+}
|
|
|
|
|
|
-export default function Message(){
|
|
|
- return <View></View>
|
|
|
-}
|