| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773 |
- <template>
- <view class="media-uploader">
- <view class="media-grid">
- <view class="media-item" v-for="(item, index) in mediaList" :key="index">
- <!-- 图片预览 -->
- <image v-if="item.type === 'image'" :src="item.url" mode="aspectFill" @tap="previewMedia(index)">
- </image>
- <!-- 视频预览 -->
- <video v-else-if="item.type === 'video'" :id="'video-' + index" :src="item.url" class="video-preview"
- controls show-center-play-btn enable-play-gesture show-fullscreen-btn object-fit="cover"
- :initial-time="0" :show-mute-btn="true" :enable-progress-gesture="true" :vslide-gesture="true"
- :vslide-gesture-in-fullscreen="true" @tap="previewVideo(index)" :poster="item.thumbnail"></video>
- <!-- 删除按钮 -->
- <view class="delete-btn" @tap.stop="deleteMedia(index)">
- <uni-icons type="close" size="12" color="#fff"></uni-icons>
- </view>
- </view>
- <!-- 上传按钮 -->
- <view class="upload-trigger" v-if="mediaList.length < maxCount">
- <view class="upload-box" @tap="chooseMedia">
- <uni-icons type="camera" size="24" color="#999"></uni-icons>
- <text class="upload-text">{{mediaList.length}}/{{maxCount}}</text>
- </view>
- </view>
- </view>
- <text class="upload-tip">最多上传{{maxCount}}个文件</text>
- </view>
- </template>
- <script>
- export default {
- name: 'MediaUploader',
- props: {
- // 最大上传数量
- maxCount: {
- type: Number,
- default: 9
- },
- // 已有的媒体列表
- value: {
- type: Array,
- default: () => []
- },
- // 上传地址
- uploadUrl: {
- type: String,
- required: true
- },
- // 认证token
- token: {
- type: String,
- required: true
- },
- // 媒体类型限制
- mediaType: {
- type: String,
- default: 'all', // 可选值:'all'、'image'、'video'
- validator: function(value) {
- return ['all', 'image', 'video'].includes(value)
- }
- },
- // 是否使用媒体模式(拍照/相册)
- media: {
- type: Boolean,
- default: false
- }
- },
- data() {
- return {
- mediaList: []
- }
- },
- watch: {
- value: {
- handler(newVal) {
- if (!Array.isArray(newVal)) {
- console.warn('MediaUploader: value must be an array');
- return;
- }
- this.mediaList = newVal.map(item => {
- // 如果已经是正确的格式,直接返回
- if (typeof item === 'object' && item.url && item.type) {
- return item;
- }
- // 如果是字符串,转换为对象格式
- const url = typeof item === 'object' ? item.url : item;
- return {
- url: url,
- type: this.getMediaType(item)
- }
- })
- },
- immediate: true
- }
- },
- methods: {
- // 选择媒体文件
- async chooseMedia() {
- try {
- // 检查是否达到最大数量
- if (this.mediaList.length >= this.maxCount) {
- uni.showToast({
- title: `最多只能上传${this.maxCount}个文件`,
- icon: 'none'
- });
- return;
- }
- // 获取平台信息
- const platform = uni.getSystemInfoSync().platform;
- const isH5 = platform === 'h5';
- // 如果是媒体模式
- if (this.media) {
- try {
- const res = await uni.showActionSheet({
- itemList: ['拍摄', '从相册选择']
- });
- if (res.tapIndex === 0) {
- // 拍摄模式
- if (this.mediaType === 'video') {
- this.captureVideo();
- } else {
- this.captureImage();
- }
- } else {
- // 从相册选择,支持图片和视频
- this.chooseFromAlbum();
- }
- return;
- } catch (error) {
- console.error('选择操作失败:', error);
- return;
- }
- }
- // 非媒体模式的原有逻辑
- if (this.mediaType === 'image') {
- this.chooseImage();
- } else if (this.mediaType === 'video') {
- if (isH5) {
- this.chooseVideoH5();
- } else {
- this.chooseVideoMini();
- }
- } else {
- // 如果是 'all',则显示选择菜单
- try {
- const res = await uni.showActionSheet({
- itemList: ['选择图片', '选择视频']
- });
- if (res.tapIndex === 0) {
- this.chooseImage();
- } else {
- if (isH5) {
- this.chooseVideoH5();
- } else {
- this.chooseVideoMini();
- }
- }
- } catch (error) {
- console.error('选择操作失败:', error);
- return;
- }
- }
- } catch (e) {
- console.error(e);
- }
- },
- // 选择图片
- async chooseImage() {
- try {
- const res = await uni.chooseImage({
- count: this.maxCount - this.mediaList.length,
- sizeType: ['original', 'compressed'],
- sourceType: ['album', 'camera']
- });
- // 上传图片
- for (let tempFilePath of res.tempFilePaths) {
- try {
- const uploadResult = await this.uploadFile(tempFilePath);
- const newMedia = {
- url: uploadResult.data.link,
- type: 'image'
- };
- this.updateMediaList([...this.mediaList, newMedia]);
- } catch (uploadError) {
- console.error('图片上传失败:', uploadError);
- uni.showToast({
- title: '图片上传失败',
- icon: 'none'
- });
- }
- }
- } catch (e) {
- console.error(e);
- }
- },
- // H5平台选择视频
- async chooseVideoH5() {
- try {
- const res = await uni.chooseVideo({
- sourceType: ['album', 'camera'],
- compressed: true,
- camera: 'back'
- });
- uni.showLoading({
- title: '处理中...'
- });
- try {
- // 上传视频文件
- const videoResult = await this.uploadFile(res.tempFilePath);
- const newMedia = {
- url: videoResult.data.link,
- type: 'video',
- duration: Math.round(res.duration),
- // H5暂时不支持视频预览图,可以后续在服务端处理
- thumbnail: ''
- };
- this.updateMediaList([...this.mediaList, newMedia]);
- uni.hideLoading();
- } catch (error) {
- uni.hideLoading();
- uni.showToast({
- title: '视频处理失败',
- icon: 'none'
- });
- console.error('视频处理失败:', error);
- }
- } catch (e) {
- console.error('选择视频失败:', e);
- uni.showToast({
- title: '选择视频失败',
- icon: 'none'
- });
- }
- },
- // 非H5平台选择视频(小程序和APP)
- async chooseVideoMini() {
- try {
- // #ifdef MP-WEIXIN
- // 微信小程序使用 chooseMedia
- wx.chooseMedia({
- count: 1,
- mediaType: ['video'],
- success: async (res) => {
- const tempFile = res.tempFiles[0];
- try {
- uni.showLoading({
- title: '处理中...'
- });
- // 上传视频文件
- const videoResult = await this.uploadFile(tempFile.tempFilePath);
- // 上传视频预览图
- const thumbResult = await this.uploadFile(tempFile.thumbTempFilePath);
- const newMedia = {
- url: videoResult.data.link,
- type: 'video',
- duration: Math.round(tempFile.duration),
- thumbnail: thumbResult.data.link
- };
- this.updateMediaList([...this.mediaList, newMedia]);
- uni.hideLoading();
- } catch (error) {
- uni.hideLoading();
- uni.showToast({
- title: '视频处理失败',
- icon: 'none'
- });
- console.error('视频处理失败:', error);
- }
- },
- fail: (err) => {
- console.error('选择视频失败:', err);
- uni.showToast({
- title: '选择视频失败',
- icon: 'none'
- });
- }
- });
- // #endif
- // #ifdef APP-PLUS
- // APP端使用 chooseVideo
- uni.chooseVideo({
- sourceType: ['camera', 'album'],
- compressed: true,
- success: async (res) => {
- try {
- uni.showLoading({
- title: '处理中...'
- });
- // 上传视频文件
- const videoResult = await this.uploadFile(res.tempFilePath);
- // 获取视频缩略图
- const thumbPath = await this.getVideoThumb(res.tempFilePath);
- let thumbnail = '';
- if (thumbPath) {
- try {
- const thumbResult = await this.uploadFile(thumbPath);
- thumbnail = thumbResult.data.link;
- } catch (error) {
- console.error('缩略图上传失败:', error);
- }
- }
- const newMedia = {
- url: videoResult.data.link,
- type: 'video',
- duration: Math.round(res.duration || 0),
- thumbnail: thumbnail || videoResult.data
- .link // 如果没有缩略图,使用视频地址作为预览图
- };
- this.updateMediaList([...this.mediaList, newMedia]);
- uni.hideLoading();
- } catch (error) {
- uni.hideLoading();
- uni.showToast({
- title: '视频处理失败',
- icon: 'none'
- });
- console.error('视频处理失败:', error);
- }
- },
- fail: (err) => {
- console.error('选择视频失败:', err);
- uni.showToast({
- title: '选择视频失败',
- icon: 'none'
- });
- }
- });
- // #endif
- // #ifdef H5
- // 如果在H5环境下不小心调用了这个方法,就调用H5的方法
- this.chooseVideoH5();
- // #endif
- } catch (e) {
- console.error('选择视频失败:', e);
- uni.showToast({
- title: '选择视频失败',
- icon: 'none'
- });
- }
- },
- // 拍摄视频
- async captureVideo() {
- try {
- const res = await uni.chooseVideo({
- sourceType: ['camera'],
- compressed: true,
- camera: 'back'
- });
- uni.showLoading({
- title: '处理中...'
- });
- try {
- // 上传视频文件
- const videoResult = await this.uploadFile(res.tempFilePath);
- const newMedia = {
- url: videoResult.data.link,
- type: 'video',
- duration: Math.round(res.duration),
- thumbnail: ''
- };
- this.updateMediaList([...this.mediaList, newMedia]);
- uni.hideLoading();
- } catch (error) {
- uni.hideLoading();
- uni.showToast({
- title: '视频处理失败',
- icon: 'none'
- });
- console.error('视频处理失败:', error);
- }
- } catch (e) {
- console.error('拍摄视频失败:', e);
- uni.showToast({
- title: '拍摄视频失败',
- icon: 'none'
- });
- }
- },
- // 拍摄图片
- async captureImage() {
- try {
- const res = await uni.chooseImage({
- count: 1,
- sizeType: ['original', 'compressed'],
- sourceType: ['camera']
- });
- // 上传图片
- for (let tempFilePath of res.tempFilePaths) {
- try {
- const uploadResult = await this.uploadFile(tempFilePath);
- const newMedia = {
- url: uploadResult.data.link,
- type: 'image'
- };
- this.updateMediaList([...this.mediaList, newMedia]);
- } catch (uploadError) {
- console.error('图片上传失败:', uploadError);
- uni.showToast({
- title: '图片上传失败',
- icon: 'none'
- });
- }
- }
- } catch (e) {
- console.error(e);
- }
- },
- // 从相册选择(支持图片和视频)
- async chooseFromAlbum() {
- // #ifdef MP-WEIXIN
- try {
- const res = await new Promise((resolve, reject) => {
- wx.chooseMedia({
- count: this.maxCount - this.mediaList.length,
- mediaType: ['image', 'video'],
- sourceType: ['album'],
- success: resolve,
- fail: reject
- });
- });
- for (let tempFile of res.tempFiles) {
- try {
- uni.showLoading({
- title: '处理中...'
- });
- // 上传文件
- const fileResult = await this.uploadFile(tempFile.tempFilePath);
- if (tempFile.fileType === 'video') {
- // 处理视频
- const thumbPath = tempFile.thumbTempFilePath;
- let thumbnail = '';
- if (thumbPath) {
- try {
- const thumbResult = await this.uploadFile(thumbPath);
- thumbnail = thumbResult.data.link;
- } catch (error) {
- console.error('缩略图上传失败:', error);
- }
- }
- const newMedia = {
- url: fileResult.data.link,
- type: 'video',
- duration: Math.round(tempFile.duration || 0),
- thumbnail: thumbnail || fileResult.data.link
- };
- this.updateMediaList([...this.mediaList, newMedia]);
- } else {
- // 处理图片
- const newMedia = {
- url: fileResult.data.link,
- type: 'image'
- };
- this.updateMediaList([...this.mediaList, newMedia]);
- }
- uni.hideLoading();
- } catch (error) {
- uni.hideLoading();
- uni.showToast({
- title: '上传失败',
- icon: 'none'
- });
- console.error('上传失败:', error);
- }
- }
- } catch (e) {
- console.error('选择失败:', e);
- uni.showToast({
- title: '选择失败',
- icon: 'none'
- });
- }
- // #endif
- // #ifdef APP-PLUS || H5
- // APP和H5使用系统相册
- try {
- const res = await uni.chooseImage({
- count: this.maxCount - this.mediaList.length,
- sizeType: ['original', 'compressed'],
- sourceType: ['album']
- });
- // 上传图片
- for (let tempFilePath of res.tempFilePaths) {
- try {
- const uploadResult = await this.uploadFile(tempFilePath);
- const newMedia = {
- url: uploadResult.data.link,
- type: 'image'
- };
- this.updateMediaList([...this.mediaList, newMedia]);
- } catch (uploadError) {
- console.error('上传失败:', uploadError);
- uni.showToast({
- title: '上传失败',
- icon: 'none'
- });
- }
- }
- } catch (e) {
- console.error(e);
- }
- // #endif
- },
- async getVideoThumb(videoPath) {
- return new Promise((resolve, reject) => {
- try {
- // #ifdef APP-PLUS
- // 先尝试使用 plus.io.getVideoInfo
- plus.io.getVideoInfo({
- filePath: videoPath,
- success: (info) => {
- if (info.thumbnail) {
- resolve(info.thumbnail);
- } else {
- // 如果获取不到缩略图,尝试使用 uni.getVideoInfo
- this.getVideoThumbUnified(videoPath).then(resolve).catch(() =>
- resolve(''));
- }
- },
- error: () => {
- // 如果 plus.io.getVideoInfo 失败,尝试使用 uni.getVideoInfo
- this.getVideoThumbUnified(videoPath).then(resolve).catch(() =>
- resolve(''));
- }
- });
- // #endif
- // #ifndef APP-PLUS
- // 非APP平台使用 uni.getVideoInfo
- this.getVideoThumbUnified(videoPath).then(resolve).catch(() => resolve(''));
- // #endif
- } catch (e) {
- console.error('获取视频缩略图失败:', e);
- resolve('');
- }
- });
- },
- // 使用 uni.getVideoInfo 获取视频缩略图
- async getVideoThumbUnified(videoPath) {
- return new Promise((resolve, reject) => {
- uni.getVideoInfo({
- src: videoPath,
- success: (res) => {
- if (res.thumbTempFilePath) {
- resolve(res.thumbTempFilePath);
- } else {
- reject(new Error('No thumbnail'));
- }
- },
- fail: reject
- });
- });
- },
- // 文件上传方法
- async uploadFile(filePath) {
- uni.showLoading({
- title: '上传中...',
- mask: true
- })
- return new Promise((resolve, reject) => {
- uni.uploadFile({
- url: this.uploadUrl,
- filePath: filePath,
- header: {
- "Blade-Auth": this.token
- },
- name: 'file',
- success: (res) => {
- resolve(JSON.parse(res.data));
- uni.hideLoading()
- },
- fail: reject
- });
- });
- },
- // 删除媒体
- deleteMedia(index) {
- const newList = [...this.mediaList];
- newList.splice(index, 1);
- this.updateMediaList(newList);
- },
- // 预览媒体
- previewMedia(index) {
- const item = this.mediaList[index];
- if (item.type === 'image') {
- const imageUrls = this.mediaList
- .filter(item => item.type === 'image')
- .map(item => item.url);
- const currentIndex = imageUrls.indexOf(item.url);
- uni.previewImage({
- urls: imageUrls,
- current: currentIndex
- });
- }
- },
- // 预览视频
- previewVideo(index) {
- const item = this.mediaList[index];
- if (item.type === 'video') {
- // H5环境下不需要特殊处理,让视频组件自己处理全屏
- // #ifdef MP-WEIXIN
- const videoContext = uni.createVideoContext(`video-${index}`, this);
- if (videoContext) {
- videoContext.requestFullScreen();
- }
- // #endif
- }
- },
- // 更新媒体列表
- updateMediaList(list) {
- this.mediaList = list;
- this.$emit('input', list.map(item => item.url));
- this.$emit('change', list);
- },
- // 获取媒体类型
- getMediaType(url) {
- // 如果已经是对象格式,直接返回其类型
- if (typeof url === 'object' && url.type) {
- return url.type;
- }
- // 如果是字符串,则通过扩展名判断
- if (typeof url === 'string') {
- const videoExts = ['.mp4', '.mov', '.avi', '.wmv'];
- const ext = url.substring(url.lastIndexOf('.')).toLowerCase();
- return videoExts.includes(ext) ? 'video' : 'image';
- }
- // 默认返回图片类型
- return 'image';
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- .media-uploader {
- padding: 15px;
- .media-grid {
- display: flex;
- flex-wrap: wrap;
- gap: 10px;
- }
- .media-item {
- position: relative;
- width: calc((100% - 20px) / 3);
- height: 0;
- padding-bottom: calc((100% - 20px) / 3);
- background-color: #f8f8f8;
- border-radius: 8px;
- overflow: hidden;
- image,
- video {
- position: absolute;
- width: 100%;
- height: 100%;
- object-fit: cover;
- }
- .delete-btn {
- position: absolute;
- top: 5px;
- right: 5px;
- width: 20px;
- height: 20px;
- background: rgba(0, 0, 0, 0.5);
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 2;
- }
- .media-mask {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- height: 30px;
- background: linear-gradient(to top, rgba(0, 0, 0, 0.5), transparent);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 1;
- }
- }
- .upload-trigger {
- width: calc((100% - 20px) / 3);
- height: 0;
- padding-bottom: calc((100% - 20px) / 3);
- position: relative;
- .upload-box {
- position: absolute;
- width: 100%;
- height: 100%;
- background-color: #f8f8f8;
- border-radius: 8px;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- .upload-text {
- margin-top: 5px;
- font-size: 12px;
- color: #999;
- }
- }
- }
- .upload-tip {
- margin-top: 10px;
- font-size: 12px;
- color: #999;
- }
- }
- </style>
|