170 lines
3.7 KiB
Vue
Raw Permalink Normal View History

<template>
<view class="files-wrap">
2026-02-09 15:40:03 +08:00
<view class="files-label">
{{ name }}<text v-if="required" class="form-cell-required">*</text>
</view>
<view class="grid">
<view v-for="(f, idx) in files" :key="idx" class="item" @click="preview(idx)">
<image class="thumb" :src="f.url" mode="aspectFill" />
<view class="remove" @click.stop="remove(idx)">×</view>
</view>
<view class="add" @click="add">
<view class="plus">+</view>
</view>
</view>
</view>
</template>
<script setup>
import { computed } from 'vue';
2026-02-09 15:40:03 +08:00
import { chooseAndUploadImage, normalizeFileUrl } from '@/utils/file';
import { toast } from '@/utils/widget';
const emits = defineEmits(['change']);
const props = defineProps({
form: { type: Object, default: () => ({}) },
name: { default: '' },
required: { type: Boolean, default: false },
title: { default: '' },
disableChange: { type: Boolean, default: false },
max: { type: [Number, String], default: 9 },
});
const maxCount = computed(() => {
const n = Number(props.max);
return Number.isFinite(n) && n > 0 ? Math.min(n, 9) : 9;
});
const files = computed(() => {
const v = props.form?.[props.title];
if (Array.isArray(v)) {
return v
.map((i) => {
if (typeof i === 'string') return { url: normalizeFileUrl(i), name: getFileNameFromUrl(i) };
if (i && typeof i === 'object' && i.url) {
const url = normalizeFileUrl(String(i.url));
return {
...i,
url,
name: i.name || i.fileName || getFileNameFromUrl(url),
};
}
return null;
})
.filter(Boolean);
}
if (typeof v === 'string' && v) {
const url = normalizeFileUrl(v);
return [{ url, name: getFileNameFromUrl(url) }];
}
return [];
});
function emitValue(list) {
emits('change', { title: props.title, value: list });
}
function preview(idx) {
const urls = files.value.map((i) => i.url);
if (!urls.length) return;
uni.previewImage({ urls, current: urls[idx] });
}
function remove(idx) {
if (props.disableChange) return;
const next = files.value.filter((_, i) => i !== idx);
emitValue(next);
}
async function add() {
if (props.disableChange) return;
if (files.value.length >= maxCount.value) {
toast(`最多上传${maxCount.value}`);
return;
}
const url = await chooseAndUploadImage({ count: 1 });
if (!url) return;
emitValue([...files.value, { url, name: getFileNameFromUrl(url) }]);
}
function getFileNameFromUrl(url) {
const cleanUrl = String(url || '').split('?')[0].split('#')[0];
const rawName = cleanUrl.split('/').pop() || '';
if (!rawName) return '';
let fileName = rawName;
try {
fileName = decodeURIComponent(rawName);
} catch (error) {
fileName = rawName;
}
return fileName.replace(/^\d{10,}[-_]/, '');
}
</script>
<style lang="scss" scoped>
@import '../cell-style.css';
.files-wrap {
padding: 24rpx 30rpx;
border-bottom: 1px solid #eee;
background: #fff;
}
.files-label {
2026-05-26 11:27:19 +08:00
font-size: 30rpx;
line-height: 42rpx;
}
.grid {
margin-top: 18rpx;
display: flex;
flex-wrap: wrap;
gap: 18rpx;
}
.item {
width: 160rpx;
height: 160rpx;
border-radius: 10rpx;
overflow: hidden;
position: relative;
background: #f6f6f6;
}
.thumb {
width: 100%;
height: 100%;
}
.remove {
position: absolute;
top: 6rpx;
right: 6rpx;
width: 36rpx;
height: 36rpx;
line-height: 36rpx;
text-align: center;
background: rgba(0, 0, 0, 0.55);
color: #fff;
border-radius: 18rpx;
2026-05-26 11:27:19 +08:00
font-size: 30rpx;
}
.add {
width: 160rpx;
height: 160rpx;
border-radius: 10rpx;
border: 1px dashed #cfcfcf;
display: flex;
align-items: center;
justify-content: center;
color: #999;
}
.plus {
2026-05-26 11:27:19 +08:00
font-size: 58rpx;
line-height: 56rpx;
}
</style>