Files
PartsInquiry/frontend/components/ImageUploader.vue
2025-09-27 22:57:59 +08:00

183 lines
5.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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)" />
<image class="remove" src="/static/icons/icons8-close-48.png" mode="aspectFit" @click.stop="remove(index)" />
</movable-view>
<view v-if="innerList.length < max" class="adder" :style="adderStyle" @click="choose">
<text></text>
</view>
</movable-area>
</view>
</view>
</template>
<script>
import { upload } from '../common/http.js'
import { API_BASE_URL } from '../common/config.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
},
adderStyle() {
const index = this.innerList.length
const row = Math.floor(index / COLS)
const col = index % COLS
const x = px(col * (ITEM_SIZE + GAP))
const y = px(row * (ITEM_SIZE + GAP))
return { left: x + 'rpx', top: y + 'rpx' }
}
},
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: this.ensureAbsoluteUrl(typeof u === 'string' ? u : (u.url || '')),
x: this.posOf(i).x,
y: this.posOf(i).y
}))
this.innerList = mapped
}
}
},
methods: {
ensureAbsoluteUrl(u) {
if (!u) return ''
const s = String(u)
if (s.startsWith('http://') || s.startsWith('https://')) return s
return API_BASE_URL + (s.startsWith('/') ? s : '/' + s)
},
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 = this.ensureAbsoluteUrl(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; width: 42rpx; height: 42rpx; }
.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>