165 lines
4.6 KiB
Vue
165 lines
4.6 KiB
Vue
<template>
|
||
<view class="uploader">
|
||
<view class="grid" :style="{height: areaHeight+'rpx'}">
|
||
<movable-area class="area" :style="{height: areaHeight+'rpx'}">
|
||
<movable-view
|
||
v-for="(img, index) in innerList"
|
||
:key="img.uid"
|
||
class="cell"
|
||
:style="cellStyle(index)"
|
||
:direction="'all'"
|
||
:damping="40"
|
||
:friction="2"
|
||
:x="img.x"
|
||
:y="img.y"
|
||
@change="onMoving(index, $event)"
|
||
@touchend="onMoveEnd(index)"
|
||
>
|
||
<image :src="img.url" mode="aspectFill" class="thumb" @click="preview(index)" />
|
||
<view class="remove" @click.stop="remove(index)">×</view>
|
||
</movable-view>
|
||
|
||
<view v-if="innerList.length < max" class="adder" @click="choose">
|
||
<text>+</text>
|
||
</view>
|
||
</movable-area>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { upload } from '../common/http.js'
|
||
|
||
const ITEM_SIZE = 210 // rpx
|
||
const GAP = 18 // rpx
|
||
const COLS = 3
|
||
|
||
function px(rpx) {
|
||
// 以 750 设计稿计算;此函数仅用于内部位置计算,不写入样式
|
||
return rpx
|
||
}
|
||
|
||
export default {
|
||
name: 'ImageUploader',
|
||
props: {
|
||
modelValue: { type: Array, default: () => [] },
|
||
max: { type: Number, default: 9 },
|
||
uploadPath: { type: String, default: '/api/attachments' },
|
||
uploadFieldName: { type: String, default: 'file' },
|
||
formData: { type: Object, default: () => ({ ownerType: 'product' }) }
|
||
},
|
||
data() {
|
||
return {
|
||
innerList: []
|
||
}
|
||
},
|
||
computed: {
|
||
areaHeight() {
|
||
const rows = Math.ceil((this.innerList.length + 1) / COLS) || 1
|
||
return rows * ITEM_SIZE + (rows - 1) * GAP
|
||
}
|
||
},
|
||
watch: {
|
||
modelValue: {
|
||
immediate: true,
|
||
handler(list) {
|
||
const mapped = (list || []).map((u, i) => ({
|
||
uid: String(i) + '_' + (u.id || u.url || Math.random().toString(36).slice(2)),
|
||
url: typeof u === 'string' ? u : (u.url || ''),
|
||
x: this.posOf(i).x,
|
||
y: this.posOf(i).y
|
||
}))
|
||
this.innerList = mapped
|
||
}
|
||
}
|
||
},
|
||
methods: {
|
||
posOf(index) {
|
||
const row = Math.floor(index / COLS)
|
||
const col = index % COLS
|
||
return { x: px(col * (ITEM_SIZE + GAP)), y: px(row * (ITEM_SIZE + GAP)) }
|
||
},
|
||
cellStyle(index) {
|
||
return {
|
||
width: ITEM_SIZE + 'rpx',
|
||
height: ITEM_SIZE + 'rpx'
|
||
}
|
||
},
|
||
preview(index) {
|
||
uni.previewImage({ urls: this.innerList.map(i => i.url), current: index })
|
||
},
|
||
remove(index) {
|
||
this.innerList.splice(index, 1)
|
||
this.reflow()
|
||
this.emit()
|
||
},
|
||
choose() {
|
||
const remain = this.max - this.innerList.length
|
||
if (remain <= 0) return
|
||
uni.chooseImage({ count: remain, success: async (res) => {
|
||
for (const path of res.tempFilePaths) {
|
||
await this.doUpload(path)
|
||
}
|
||
}})
|
||
},
|
||
async doUpload(filePath) {
|
||
try {
|
||
const resp = await upload(this.uploadPath, filePath, this.formData, this.uploadFieldName)
|
||
const url = resp?.url || resp?.data?.url || resp?.path || ''
|
||
if (!url) throw new Error('上传响应无 url')
|
||
this.innerList.push({ uid: Math.random().toString(36).slice(2), url, ...this.posOf(this.innerList.length) })
|
||
this.reflow()
|
||
this.emit()
|
||
} catch (e) {
|
||
uni.showToast({ title: '上传失败', icon: 'none' })
|
||
}
|
||
},
|
||
onMoving(index, e) {
|
||
// 实时更新移动中元素的位置
|
||
const { x, y } = e.detail
|
||
this.innerList[index].x = x
|
||
this.innerList[index].y = y
|
||
},
|
||
onMoveEnd(index) {
|
||
// 根据落点推算新的索引
|
||
const mv = this.innerList[index]
|
||
const col = Math.round(mv.x / (ITEM_SIZE + GAP))
|
||
const row = Math.round(mv.y / (ITEM_SIZE + GAP))
|
||
let newIndex = row * COLS + col
|
||
newIndex = Math.max(0, Math.min(newIndex, this.innerList.length - 1))
|
||
if (newIndex !== index) {
|
||
const moved = this.innerList.splice(index, 1)[0]
|
||
this.innerList.splice(newIndex, 0, moved)
|
||
}
|
||
this.reflow()
|
||
this.emit()
|
||
},
|
||
reflow() {
|
||
this.innerList.forEach((it, i) => {
|
||
const p = this.posOf(i)
|
||
it.x = p.x
|
||
it.y = p.y
|
||
})
|
||
},
|
||
emit() {
|
||
this.$emit('update:modelValue', this.innerList.map(i => i.url))
|
||
this.$emit('change', this.innerList.map(i => i.url))
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.uploader { padding: 12rpx; background: #fff; }
|
||
.grid { position: relative; }
|
||
.area { width: 100%; position: relative; }
|
||
.cell { position: absolute; border-radius: 12rpx; overflow: hidden; box-shadow: 0 0 1rpx rgba(0,0,0,0.08); }
|
||
.thumb { width: 100%; height: 100%; }
|
||
.remove { position: absolute; right: 6rpx; top: 6rpx; background: rgba(0,0,0,0.45); color: #fff; width: 40rpx; height: 40rpx; text-align: center; line-height: 40rpx; border-radius: 20rpx; font-size: 28rpx; }
|
||
.adder { width: 210rpx; height: 210rpx; border: 2rpx dashed #ccc; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; color: #999; position: absolute; left: 0; top: 0; }
|
||
</style>
|
||
|
||
|
||
|
||
|