396 lines
10 KiB
Vue
Raw Normal View History

2025-08-18 20:47:50 +08:00
<template>
<view class="dm-ps-component">
<!-- 现场拍照 -->
<view class="section-card mb-15">
<view class="section-title">{{ photoTitle || '现场拍照' }}</view>
<view class="photo-section">
<view class="photo-preview" v-if="photoList.length > 0">
<view
v-for="(photo, index) in photoList"
:key="index"
class="photo-item"
>
<image
:src="photo.url"
class="photo-image"
mode="aspectFill"
@click="previewPhoto(photo.url)"
/>
<view class="photo-actions">
<view class="action-btn delete-btn" @click="deletePhoto(index)">
<u-icon name="trash" size="16" color="#fff"></u-icon>
</view>
</view>
</view>
</view>
<view class="photo-upload" @click="takePhoto">
<u-icon name="camera" size="32" color="#999"></u-icon>
<text class="upload-text">{{ photoUploadText || '点击拍照' }}</text>
</view>
</view>
</view>
<!-- 现场视频 -->
<view class="section-card mb-15">
<view class="section-title">{{ videoTitle || '现场视频' }}</view>
<view class="video-section">
<view class="video-preview" v-if="videoList.length > 0">
<view
v-for="(video, index) in videoList"
:key="index"
class="video-item"
>
<video
:src="video.url"
class="video-player"
controls
show-center-play-btn
></video>
<view class="video-actions">
<view class="action-btn delete-btn" @click="deleteVideo(index)">
<u-icon name="trash" size="16" color="#fff"></u-icon>
</view>
</view>
</view>
</view>
<view class="video-upload" @click="recordVideo">
<u-icon name="videocam" size="32" color="#999"></u-icon>
<text class="upload-text">{{ videoUploadText || '点击录制' }}</text>
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import { attachmentUpload } from "@/api/system/upload";
import { showLoading, hideLoading, showToast } from "@/utils/uniapp";
// 定义组件属性
interface Props {
// 照片相关
photoTitle?: string;
photoUploadText?: string;
maxPhotoCount?: number;
// 视频相关
videoTitle?: string;
videoUploadText?: string;
maxVideoCount?: number;
maxVideoDuration?: number;
// 数据
modelValue?: {
photoList: Array<{url: string, path: string}>;
videoList: Array<{url: string, path: string}>;
};
}
// 定义组件事件
interface Emits {
(e: 'update:modelValue', value: {photoList: Array<{url: string, path: string}>, videoList: Array<{url: string, path: string}>}): void;
(e: 'photoChange', photoList: Array<{url: string, path: string}>): void;
(e: 'videoChange', videoList: Array<{url: string, path: string}>): void;
(e: 'photoAdd', photo: {url: string, path: string}): void;
(e: 'photoDelete', index: number): void;
(e: 'videoAdd', video: {url: string, path: string}): void;
(e: 'videoDelete', index: number): void;
(e: 'uploadComplete', result: {photoUrls: string, videoUrls: string}): void;
}
const props = withDefaults(defineProps<Props>(), {
photoTitle: '现场拍照',
photoUploadText: '点击拍照',
maxPhotoCount: 9,
videoTitle: '现场视频',
videoUploadText: '点击录制',
maxVideoCount: 3,
maxVideoDuration: 60
});
const emit = defineEmits<Emits>();
// 照片和视频列表
const photoList = ref<Array<{url: string, path: string}>>([]);
const videoList = ref<Array<{url: string, path: string}>>([]);
// 监听外部数据变化
watch(() => props.modelValue, (newValue) => {
if (newValue) {
photoList.value = newValue.photoList || [];
videoList.value = newValue.videoList || [];
}
}, { immediate: true, deep: true });
// 监听内部数据变化,向外部发送更新
watch([photoList, videoList], ([newPhotoList, newVideoList]) => {
const value = {
photoList: newPhotoList,
videoList: newVideoList
};
emit('update:modelValue', value);
emit('photoChange', newPhotoList);
emit('videoChange', newVideoList);
}, { deep: true });
// 拍照功能
const takePhoto = () => {
if (photoList.value.length >= props.maxPhotoCount) {
uni.showToast({
title: `最多只能拍摄${props.maxPhotoCount}张照片`,
icon: 'none'
});
return;
}
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['camera'],
success: (res) => {
const tempFilePath = res.tempFilePaths[0];
const photo = {
url: tempFilePath,
path: tempFilePath
};
photoList.value.push(photo);
emit('photoAdd', photo);
},
fail: (err) => {
console.error('拍照失败:', err);
uni.showToast({
title: '拍照失败',
icon: 'none'
});
}
});
};
// 录制视频功能
const recordVideo = () => {
if (videoList.value.length >= props.maxVideoCount) {
uni.showToast({
title: `最多只能录制${props.maxVideoCount}个视频`,
icon: 'none'
});
return;
}
uni.chooseVideo({
sourceType: ['camera'],
maxDuration: props.maxVideoDuration,
camera: 'back',
success: (res) => {
const tempFilePath = res.tempFilePath;
const video = {
url: tempFilePath,
path: tempFilePath
};
videoList.value.push(video);
emit('videoAdd', video);
},
fail: (err) => {
console.error('录制视频失败:', err);
uni.showToast({
title: '录制视频失败',
icon: 'none'
});
}
});
};
// 删除照片
const deletePhoto = (index: number) => {
const deletedPhoto = photoList.value[index];
photoList.value.splice(index, 1);
emit('photoDelete', index);
};
// 删除视频
const deleteVideo = (index: number) => {
const deletedVideo = videoList.value[index];
videoList.value.splice(index, 1);
emit('videoDelete', index);
};
// 预览照片
const previewPhoto = (url: string) => {
const urls = photoList.value.map(photo => photo.url);
uni.previewImage({
current: url,
urls: urls
});
};
// 上传媒体文件到服务器
const uploadMedia = async (): Promise<{photoUrls: string, videoUrls: string}> => {
const photoUrls: string[] = [];
const videoUrls: string[] = [];
try {
// 上传照片
if (photoList.value.length > 0) {
showLoading({ title: '正在上传照片...' });
for (let i = 0; i < photoList.value.length; i++) {
const photo = photoList.value[i];
try {
const res: any = await attachmentUpload(photo.path);
if (res && res.result && res.result[0]) {
photoUrls.push(res.result[0].filePath);
}
} catch (error) {
console.error('照片上传失败:', error);
throw new Error(`${i + 1}张照片上传失败`);
}
}
hideLoading();
}
// 上传视频
if (videoList.value.length > 0) {
showLoading({ title: '正在上传视频...' });
for (let i = 0; i < videoList.value.length; i++) {
const video = videoList.value[i];
try {
const res: any = await attachmentUpload(video.path);
if (res && res.result && res.result[0]) {
videoUrls.push(res.result[0].filePath);
}
} catch (error) {
console.error('视频上传失败:', error);
throw new Error(`${i + 1}个视频上传失败`);
}
}
hideLoading();
}
const result = {
photoUrls: photoUrls.join(','),
videoUrls: videoUrls.join(',')
};
// 发送上传完成事件
emit('uploadComplete', result);
return result;
} catch (error) {
hideLoading();
showToast({
title: error instanceof Error ? error.message : '上传失败',
icon: 'none'
});
throw error;
}
};
// 暴露方法给父组件
defineExpose({
photoList,
videoList,
takePhoto,
recordVideo,
deletePhoto,
deleteVideo,
uploadMedia,
clearAll: () => {
photoList.value = [];
videoList.value = [];
}
});
</script>
<style lang="scss" scoped>
.dm-ps-component {
.section-card {
background: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 15px;
padding-left: 8px;
border-left: 3px solid #007aff;
}
.photo-section, .video-section {
.photo-preview, .video-preview {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 15px;
}
.photo-item, .video-item {
position: relative;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.photo-image, .video-player {
width: 100%;
height: 100px;
border-radius: 8px;
}
.video-player {
background-color: #000;
}
.photo-actions, .video-actions {
position: absolute;
top: 5px;
right: 5px;
.action-btn {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&.delete-btn {
background-color: rgba(255, 0, 0, 0.8);
}
}
}
}
.photo-upload, .video-upload {
width: 100px;
height: 100px;
border: 2px dashed #ddd;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
border-color: #007aff;
background-color: rgba(0, 122, 255, 0.05);
}
.upload-text {
font-size: 12px;
color: #999;
margin-top: 5px;
}
}
}
}
// 工具类
.mb-15 { margin-bottom: 15px; }
</style>