This commit is contained in:
2025-09-17 14:40:16 +08:00
parent 46c5682960
commit a3bbc0098a
94 changed files with 3549 additions and 105 deletions

View 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>