9.17/1
This commit is contained in:
164
frontend/components/ImageUploader.vue
Normal file
164
frontend/components/ImageUploader.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user