<script setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import request from '@/utils/request';
import { getFiles, getRandomFile, getFileArray } from '@/utils/yangUtils';
const imageSuffix = ['png', 'jpg', 'jpeg'];
const videoSuffix = ['mp4', 'avi', 'mov'];
const imageNum = ref(0);
const videoNum = ref(0);
// 获取父级传入的数据
const props = defineProps(["modelValue", "limit", "message", "isDelete", "isAdd"]);
// 定义 emits
const emit = defineEmits(['update:modelValue']);
// const listData = ref(["https://yxdjpw.oss-cn-beijing.aliyuncs.com/uploadDefault/20260417192542-d3ea46.png", "https://yxdjpw.oss-cn-beijing.aliyuncs.com/uploadDefault/20260417194449-bfb1ba.mp4"]);
const listData = ref([])
const onSelectFile = async () => {
let result;
// #ifdef APP || APP-PLUS || MP-WEIXIN || MP-TOUTIAO || MP-LARK || MP-JD || MP-HARMONY || MP-XHS
result = await uni.chooseMedia({
count: props.limit || 9,
mediaType: ['image', 'video'],
sourceType: ['album', 'camera'],
maxDuration: '30s'
})
// #endif
// #ifdef WEB || H5
result = await uni.chooseFile({
count: props.limit || 9,
type: 'all',
})
// #endif
let arr = [];
console.log(result);
for (let filePath of result.tempFiles) {
// 判断文件是否超出10M
if (filePath.size > 10 * 1024 * 1024) {
uni.showModal({
title: '提示',
content: '文件大小不能超过10M!',
showCancel: true,
})
return;
}
if (imageSuffix.includes(filePath.name.replaceAll('"', '').split('.').pop()?.toLowerCase())) {
imageNum.value++;
} else if (videoSuffix.includes(filePath.name.replaceAll('"', '').split('.').pop()?.toLowerCase())) {
// 判断之前有没有上传过视频或者图片
if (imageNum.value >= 1 || videoNum.value >= 1) {
uni.showModal({
title: '提示',
content: '图片和视频不能同时上传,并且视频只能上传一个!',
showCancel: true,
})
return;
}
videoNum.value++;
}
const data = await request("/upload/uploadFile", filePath.path, "post");
arr.push(getFileArray(data)[0]);
}
listData.value = listData.value.concat(arr);
// 将数据返回出去
emit("update:modelValue", listData.value.join(","));
}
// 删除
const onDelete = (index) => {
// 1. 先拿到要删除的项(必须在 splice 之前拿!)
const deletedItem = listData.value[index];
// 2. 判断类型,更新计数
const ext = deletedItem.split('.').pop()?.toLowerCase();
if (imageSuffix.includes(ext)) {
imageNum.value--;
} else if (videoSuffix.includes(ext)) {
videoNum.value--;
}
// 3. 再删除元素
listData.value.splice(index, 1);
// 4. 更新双向绑定
emit("update:modelValue", listData.value.join(","));
}
// 查看
const onView = (item) => {
}
// 类型判断
const isSuffix = () => {
return listData.value.some(item => {
// 获取后缀(转小写,避免大小写问题)
const ext = item.split('.').pop()?.toLowerCase()
return videoSuffix.includes(ext)
})
}
</script>
<template>
<view>
<view class="header">
<view v-if="props.message" class="message">{{ props.message }}</view>
<view class="numCount">{{ `${listData.length}/${props.limit || 9}` }}</view>
</view>
<view class="image-grid" :class="isSuffix() ? 'grid-video' : ''">
<template v-for="(item, index) in listData" :key="index">
<view class="grid-item grid-item-content" :class="isSuffix() ? 'grid-item-video' : ''" @click="onView(item)">
<image v-if="imageSuffix.includes(item.split('.').pop())" class="img" :src="item" mode="aspectFill" />
<video v-else-if="videoSuffix.includes(item.split('.').pop())" class="video" :src="item" />
<view class="grid-item-delete" v-if="(isAdd ?? true)" @click="onDelete(index)"><uni-icons class="icon-delete"
type="closeempty" color="#D3D4D6" size="24" /></view>
</view>
</template>
<view class="grid-item icon-add" v-if="(isAdd ?? true) && !isSuffix() && listData.length < (props.limit ?? 9)"
@click="onSelectFile()">
<uni-icons type="plusempty" size="60" color="#F1F1F1"></uni-icons>
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.image-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
/* 一行3个,自动均分 */
gap: 20rpx;
/* 格子间距 */
box-sizing: border-box;
}
.grid-item {
width: 240rpx;
height: 240rpx;
}
.grid-video {
grid-template-columns: repeat(1, 1fr) !important;
}
.grid-item-video {
width: 100%;
height: 400rpx;
}
.numCount {
display: flex;
flex-direction: row-reverse;
font-size: 26rpx;
}
.img,
.video {
width: 100%;
height: 100%;
}
.grid-item-content {
position: relative;
}
.icon-add {
background: #FFFFFF;
line-height: 240rpx;
text-align: center;
border: 1rpx solid #EEEEEE;
border-radius: 6rpx;
}
.grid-item-delete {
position: absolute;
top: 0rpx;
right: 0rpx;
z-index: 1;
}
</style>
方法解析
文件后端返回的是
/api/upload/xxx.png
- getFiles 将图片拼接成正常能访问的 比如http://域名.com/api/upload/xxx.png
- getRandomFile 是随机访问
- getFileArray 是拼接成数组
- limit → 最多上传数量 默认为9
- message → 提示 比如请上传视频 可不填
- isDelete → 是否可以删除重新上传 默认为true
- isAdd → 是否可以新增,比如有些地方可以直接回显 比如产品的图片,但是你不想让它显示新增的框 可以为false 默认为true
// 文件加载
export const getFiles = (url) => {
if (!url) {
return "";
}
if (url.startsWith("http://") || url.startsWith("https://")) {
return cleanString(url);
} else {
return baseURL + cleanString(url);
}
};
// 随机加载文件
export const getRandomFile = (url) => {
if (!url) {
return "";
}
const urls = url.split(",");
const randomIndex = Math.floor(Math.random() * urls.length);
const selectedUrl = urls[randomIndex];
if (
cleanString(selectedUrl).startsWith("http://") ||
cleanString(selectedUrl).startsWith("https://")
) {
return cleanString(selectedUrl);
} else {
return baseURL + cleanString(selectedUrl);
}
};
export const getFileArray = (url) => {
if (!url) {
return [];
}
let baseUrl = [];
url.split(",").forEach((v) => {
if (
cleanString(v).startsWith("http://") ||
cleanString(v).startsWith("https://")
) {
baseUrl.push(cleanString(v));
} else {
baseUrl.push(baseURL + cleanString(v));
}
});
console.log(baseUrl);
return baseUrl;
};
请求就是 uni.request的请求 你们按照你们自己的改
当前只支持H5 你们需要的话 我会再更新
如果上传了视频就直接独占一行 并且不能上传其他视频和图片 仿照的是朋友圈
其他页面调用就是 比如文件名叫uploadFile.vue吧
import UpLoadFile from '@/components/uploadFile.vue'
// 页面代码
<UpLoadFile v-model="form.media" />
第一次封装 还有些问题 我会持续更新的
1 个帖子 - 1 位参与者