|
|
@@ -1,99 +1,184 @@
|
|
|
<template>
|
|
|
- <div>
|
|
|
- <button @click="_openCamera">打开相机</button>
|
|
|
- <button @click="_openGallery">打开相册选择图片并返回路径</button>
|
|
|
- <button @click="_openImageEditor">打开图片编辑器</button>
|
|
|
-
|
|
|
- <!-- 显示图片的容器 -->
|
|
|
- <div v-if="imageUrl" class="image-preview">
|
|
|
- <img :src="imageUrl" alt="Selected Image" />
|
|
|
+ <div class="upload-container">
|
|
|
+ <!-- 上传组件 -->
|
|
|
+ <van-uploader
|
|
|
+ v-model="fileList"
|
|
|
+ :after-read="afterRead"
|
|
|
+ accept="image/*"
|
|
|
+ :max-count="5"
|
|
|
+ :max-size="5 * 1024 * 1024"
|
|
|
+ @oversize="onOversize"
|
|
|
+ multiple
|
|
|
+ >
|
|
|
+ </van-uploader>
|
|
|
+
|
|
|
+ <!-- 图片预览区域 -->
|
|
|
+ <div class="preview-container" v-if="fileList.length > 0">
|
|
|
+ <div class="preview-item" v-for="(item, index) in fileList" :key="index">
|
|
|
+ <img :src="item.content || item.url" class="preview-image" />
|
|
|
+ <div class="preview-status" :class="'status-' + item.status">
|
|
|
+ {{ item.message }}
|
|
|
+ </div>
|
|
|
+ <van-icon
|
|
|
+ name="close"
|
|
|
+ class="preview-remove"
|
|
|
+ @click="removeImage(index)"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
-<script setup>
|
|
|
-import { ref } from 'vue'
|
|
|
+<script>
|
|
|
+import { Uploader, Button, Icon, Toast } from 'vant';
|
|
|
|
|
|
-const imagePath = ref('')
|
|
|
-const imageUrl = ref('')
|
|
|
+export default {
|
|
|
+ components: {
|
|
|
+ [Uploader.name]: Uploader,
|
|
|
+ [Button.name]: Button,
|
|
|
+ [Icon.name]: Icon
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ fileList: [
|
|
|
+ // 可以预置已上传的图片
|
|
|
+ // { url: 'https://example.com/image1.jpg', status: 'done' }
|
|
|
+ ]
|
|
|
+ };
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ afterRead(file) {
|
|
|
+ if (!Array.isArray(file)) {
|
|
|
+ file = [file];
|
|
|
+ }
|
|
|
|
|
|
-const _openCamera = () => {
|
|
|
- if (!window.android) {
|
|
|
- alert('Android 接口不可用')
|
|
|
- return
|
|
|
- }
|
|
|
- window.android.openCamera()
|
|
|
-}
|
|
|
+ file.forEach(item => {
|
|
|
+ // 生成预览图
|
|
|
+ if (item.file) {
|
|
|
+ item.content = URL.createObjectURL(item.file);
|
|
|
+ }
|
|
|
|
|
|
-const _openGallery = () => {
|
|
|
- if (!window.android) {
|
|
|
- alert('Android 接口不可用')
|
|
|
- return
|
|
|
- }
|
|
|
- window.android.openGallery()
|
|
|
-}
|
|
|
+ // 设置上传状态
|
|
|
+ item.status = 'uploading';
|
|
|
+ item.message = '上传中...';
|
|
|
|
|
|
-const _openImageEditor = () => {
|
|
|
- if (!window.android) {
|
|
|
- alert('Android 接口不可用')
|
|
|
- return
|
|
|
- }
|
|
|
- if (!imagePath.value) {
|
|
|
- alert('请先选择图片')
|
|
|
- return
|
|
|
- }
|
|
|
- window.android.openImageEditor(imagePath.value)
|
|
|
-}
|
|
|
-
|
|
|
-// 这个函数应该由 Android 调用
|
|
|
-window.handleImageResult = (path) => {
|
|
|
- imagePath.value = path
|
|
|
- // 如果是文件路径,直接使用
|
|
|
- if (path.startsWith('file://') || path.startsWith('/storage')) {
|
|
|
- imageUrl.value = path
|
|
|
- }
|
|
|
- // 如果是 base64,直接使用
|
|
|
- else if (path.startsWith('data:image')) {
|
|
|
- imageUrl.value = path
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 处理 Blob 数据
|
|
|
-window.handleImageBlob = (base64) => {
|
|
|
- const blob = base64ToBlob(base64, 'image/jpeg')
|
|
|
- imageUrl.value = URL.createObjectURL(blob)
|
|
|
-}
|
|
|
-
|
|
|
-// 将 base64 转换为 Blob
|
|
|
-const base64ToBlob = (base64, mimeType) => {
|
|
|
- const byteCharacters = atob(base64.split(',')[1])
|
|
|
- const byteNumbers = new Array(byteCharacters.length)
|
|
|
- for (let i = 0; i < byteCharacters.length; i++) {
|
|
|
- byteNumbers[i] = byteCharacters.charCodeAt(i)
|
|
|
+ this.uploadImage(item).then(res => {
|
|
|
+ item.status = 'done';
|
|
|
+ item.message = '上传成功';
|
|
|
+ // 保存服务器返回的URL
|
|
|
+ // item.url = res.data.url;
|
|
|
+ }).catch(err => {
|
|
|
+ item.status = 'failed';
|
|
|
+ item.message = '上传失败';
|
|
|
+ Toast.fail('上传失败');
|
|
|
+ });
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ uploadImage(file) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ // 这里替换为真实的上传逻辑
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append('file', file.file);
|
|
|
+
|
|
|
+ // 示例:使用axios上传
|
|
|
+ // axios.post('/api/upload', formData, {
|
|
|
+ // headers: {
|
|
|
+ // 'Content-Type': 'multipart/form-data'
|
|
|
+ // }
|
|
|
+ // }).then(resolve).catch(reject)
|
|
|
+
|
|
|
+ // 模拟上传
|
|
|
+ setTimeout(() => {
|
|
|
+ resolve({
|
|
|
+ data: {
|
|
|
+ url: URL.createObjectURL(file.file)
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }, 1500);
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ onOversize(file) {
|
|
|
+ Toast.fail('文件大小不能超过5MB');
|
|
|
+ },
|
|
|
+
|
|
|
+ removeImage(index) {
|
|
|
+ this.fileList.splice(index, 1);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 组件销毁时释放内存
|
|
|
+ beforeDestroy() {
|
|
|
+ this.fileList.forEach(file => {
|
|
|
+ if (file.content) {
|
|
|
+ URL.revokeObjectURL(file.content);
|
|
|
+ }
|
|
|
+ });
|
|
|
}
|
|
|
- const byteArray = new Uint8Array(byteNumbers)
|
|
|
- return new Blob([byteArray], { type: mimeType })
|
|
|
-}
|
|
|
+};
|
|
|
</script>
|
|
|
|
|
|
+
|
|
|
<style scoped lang="sass">
|
|
|
-button
|
|
|
- margin: 10px
|
|
|
- padding: 8px 16px
|
|
|
- background: #42b983
|
|
|
- color: white
|
|
|
- border: none
|
|
|
+.upload-container
|
|
|
+ padding: 15px
|
|
|
+
|
|
|
+
|
|
|
+.preview-container
|
|
|
+ display: flex
|
|
|
+ flex-wrap: wrap
|
|
|
+ margin-top: 15px
|
|
|
+
|
|
|
+
|
|
|
+.preview-item
|
|
|
+ position: relative
|
|
|
+ width: 100px
|
|
|
+ height: 100px
|
|
|
+ margin-right: 10px
|
|
|
+ margin-bottom: 10px
|
|
|
+ border: 1px solid #eee
|
|
|
border-radius: 4px
|
|
|
- cursor: pointer
|
|
|
+ overflow: hidden
|
|
|
+
|
|
|
+
|
|
|
+.preview-image
|
|
|
+ width: 100%
|
|
|
+ height: 100%
|
|
|
+ object-fit: cover
|
|
|
+
|
|
|
+
|
|
|
+.preview-status
|
|
|
+ position: absolute
|
|
|
+ bottom: 0
|
|
|
+ left: 0
|
|
|
+ right: 0
|
|
|
+ padding: 2px 5px
|
|
|
+ font-size: 12px
|
|
|
+ color: white
|
|
|
+ background-color: rgba(0, 0, 0, 0.6)
|
|
|
+
|
|
|
+
|
|
|
+.status-uploading
|
|
|
+ background-color: #1989fa
|
|
|
+
|
|
|
|
|
|
+.status-done
|
|
|
+ background-color: #07c160
|
|
|
|
|
|
-.image-preview
|
|
|
- margin-top: 20px
|
|
|
- img
|
|
|
- max-width: 100%
|
|
|
- max-height: 400px
|
|
|
- border: 1px solid #ddd
|
|
|
- border-radius: 4px
|
|
|
+
|
|
|
+.status-failed
|
|
|
+ background-color: #ee0a24
|
|
|
+
|
|
|
+.preview-remove
|
|
|
+ position: absolute
|
|
|
+ top: 0
|
|
|
+ right: 0
|
|
|
+ color: white
|
|
|
+ background-color: rgba(0, 0, 0, 0.5)
|
|
|
+ padding: 4px
|
|
|
+ border-radius: 0 0 0 4px
|
|
|
+ cursor: pointer
|
|
|
|
|
|
|
|
|
</style>
|