681 lines
18 KiB
Vue
681 lines
18 KiB
Vue
<template>
|
||
<view class="update">
|
||
<view class="upload-container">
|
||
<view class="image-list">
|
||
<!-- 已拍摄图片预览 - 不显示hidden的图片 -->
|
||
<view v-for="(file, index) in visibleFileList" :key="index" class="image-item" @click="previewImage(index)">
|
||
<image :src="file.url" class="preview-img" mode="aspectFill" />
|
||
<view class="delete-btn" @click.stop="handleDelete(index)">×</view>
|
||
<!-- 添加上传状态指示器 -->
|
||
<view v-if="file.status === 'uploading'" class="upload-status uploading">上传中...</view>
|
||
<view v-if="file.status === 'error'" class="upload-status error">上传失败</view>
|
||
</view>
|
||
<!-- 拍照图标,未达上限时显示 -->
|
||
<view v-if="fileList.length < maxCount" class="camera-icon" @click="handleOpenCamera"
|
||
hover-class="none">
|
||
<image src="/static/camera.png" class="icon-img" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<!-- bcode-camera组件 -->
|
||
<bcode-camera v-if="showCamera" :tipsText="tipsText" :currentCount="visibleFileList.length" :fileList="visibleFileList"
|
||
@onConfirm="handleCameraConfirm" @onCancel="handleCameraCancel" @onUpload="onUpload"
|
||
@onDeleteImage="handleDeleteImage"></bcode-camera>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
// 导入会员书籍数量检查工具
|
||
import { checkMemberBooksCount } from '@/components/MemberBookCheck.js';
|
||
|
||
export default {
|
||
name: 'CameraUpload',
|
||
props: {
|
||
// 初始文件列表
|
||
value: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
// 最大上传数量
|
||
maxCount: {
|
||
type: Number,
|
||
default: 9
|
||
},
|
||
uploadUrl: {
|
||
type: String,
|
||
required: true, // 必须传递有效 URL(如 https://xxx.com)
|
||
default: '' // 可选默认值
|
||
},
|
||
// 新增的ISBN和书名props
|
||
isbn: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
bookName: {
|
||
type: String,
|
||
default: ''
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
showCamera: false,
|
||
fileList: this.value,
|
||
uploadQueue: [], // 上传队列
|
||
isUploading: false, // 是否正在上传
|
||
processedUrls: new Set() // 新增:用于记录已处理的图片URL
|
||
};
|
||
},
|
||
computed: {
|
||
tipsText() {
|
||
return `已拍摄 ${this.fileList.length} 张,最多可拍 ${this.maxCount} 张`;
|
||
},
|
||
visibleFileList() {
|
||
return this.fileList.filter(file => !file.hidden);
|
||
}
|
||
},
|
||
watch: {
|
||
value(newVal) {
|
||
this.fileList = newVal;
|
||
},
|
||
// 新增:监听上传队列变化
|
||
uploadQueue: {
|
||
handler(newQueue) {
|
||
if (newQueue.length > 0 && !this.isUploading) {
|
||
this.processUploadQueue();
|
||
}
|
||
},
|
||
deep: true
|
||
},
|
||
// 监听文件列表变化,检查上传状态
|
||
fileList: {
|
||
handler(newFileList) {
|
||
// 检查是否有文件正在上传中
|
||
const isUploading = newFileList.some(file => file.status === 'uploading');
|
||
// 通知父组件上传状态
|
||
this.$emit('upload-status-change', isUploading);
|
||
},
|
||
deep: true
|
||
}
|
||
},
|
||
methods: {
|
||
// 新增:检查文件是否已处理
|
||
isProcessed(url) {
|
||
return this.processedUrls.has(url);
|
||
},
|
||
|
||
// 新增:标记文件为已处理
|
||
markAsProcessed(url) {
|
||
this.processedUrls.add(url);
|
||
},
|
||
|
||
// 新增:检查是否有文件正在上传中
|
||
checkUploadingStatus() {
|
||
const isUploading = this.fileList.some(file => file.status === 'uploading');
|
||
// 通知父组件上传状态
|
||
this.$emit('upload-status-change', isUploading);
|
||
return isUploading;
|
||
},
|
||
|
||
// 修改:处理新图片
|
||
async processNewImage(imageUrl) {
|
||
if (this.isProcessed(imageUrl)) {
|
||
console.log('图片已处理过,跳过:', imageUrl);
|
||
return;
|
||
}
|
||
|
||
// 获取已使用的编号(包括2,因为它预留给识图照片)
|
||
const usedNums = new Set(["2"]);
|
||
|
||
// 收集当前文件列表中已使用的编号,并确保不会重复使用已上传文件的编号
|
||
this.fileList.forEach(file => {
|
||
if (file.num) {
|
||
usedNums.add(file.num);
|
||
}
|
||
});
|
||
|
||
// 找到下一个可用的编号(从1开始)
|
||
let nextNum = 1;
|
||
while (usedNums.has(nextNum.toString())) {
|
||
nextNum++;
|
||
}
|
||
|
||
// 添加到文件列表
|
||
const newFile = {
|
||
url: imageUrl,
|
||
status: "ready",
|
||
message: "待上传",
|
||
num: nextNum.toString()
|
||
};
|
||
|
||
// 在添加新文件之前,检查是否存在相同编号的文件
|
||
const existingFileIndex = this.fileList.findIndex(f => f.num === nextNum.toString());
|
||
if (existingFileIndex !== -1) {
|
||
// 如果存在相同编号的文件,为新文件找一个新的编号
|
||
nextNum = Math.max(...Array.from(this.fileList.map(f => parseInt(f.num) || 0))) + 1;
|
||
newFile.num = nextNum.toString();
|
||
}
|
||
|
||
this.fileList.push(newFile);
|
||
|
||
// 重新排序文件列表,但保持识图照片的位置
|
||
this.fileList.sort((a, b) => {
|
||
// 如果其中一个是识图照片,保持其位置
|
||
if (a.name?.startsWith('识图-')) return 1;
|
||
if (b.name?.startsWith('识图-')) return -1;
|
||
|
||
const numA = parseInt(a.num) || 999;
|
||
const numB = parseInt(b.num) || 999;
|
||
return numA - numB;
|
||
});
|
||
|
||
// 添加到上传队列
|
||
await this.addToUploadQueue(newFile, this.fileList.indexOf(newFile));
|
||
|
||
// 如果这是第一张普通照片(num="1"),检查并上传识图照片
|
||
if (newFile.num === "1") {
|
||
const ocrPhoto = this.fileList.find(file =>
|
||
file.name?.startsWith('识图-') &&
|
||
file.status === 'ready' &&
|
||
file.url.startsWith('wxfile://tmp') || file.url.startsWith('http://tmp') // 检查是否为临时路径
|
||
);
|
||
|
||
if (ocrPhoto) {
|
||
console.log('发现待上传的识图照片,准备上传:', ocrPhoto);
|
||
// 更新识图照片的hidden属性为false
|
||
const ocrIndex = this.fileList.indexOf(ocrPhoto);
|
||
const updatedOcrPhoto = {
|
||
...ocrPhoto,
|
||
hidden: false,
|
||
num: "2" // 确保编号为2
|
||
};
|
||
|
||
this.fileList.splice(ocrIndex, 1, updatedOcrPhoto);
|
||
|
||
// 立即上传识图照片
|
||
await this.addToUploadQueue(updatedOcrPhoto, ocrIndex);
|
||
|
||
// 通知父组件更新
|
||
this.$emit('input', this.fileList);
|
||
console.log('已将识图照片设置为第二张并添加到上传队列');
|
||
}
|
||
}
|
||
|
||
// 标记为已处理
|
||
this.markAsProcessed(imageUrl);
|
||
|
||
// 通知父组件更新
|
||
this.$emit('input', this.fileList);
|
||
|
||
console.log(`新照片已添加,编号为: ${nextNum}`);
|
||
console.log('当前文件列表:', this.fileList.map(f => ({
|
||
url: f.url,
|
||
num: f.num,
|
||
name: f.name,
|
||
hidden: f.hidden,
|
||
status: f.status
|
||
})));
|
||
},
|
||
|
||
// 修改:onUpload 处理程序
|
||
async onUpload(data) {
|
||
if (!data || !data.url) {
|
||
console.error('无效的图片URL');
|
||
return;
|
||
}
|
||
|
||
console.log('接收到新拍摄的照片:', data);
|
||
await this.processNewImage(data.url);
|
||
},
|
||
|
||
// 修改:handleCameraConfirm 处理程序
|
||
async handleCameraConfirm(data) {
|
||
console.log('相机确认完成,接收到数据:', data);
|
||
|
||
// 关闭相机
|
||
this.showCamera = false;
|
||
this.$emit('camera-status-change', false);
|
||
|
||
// 检查是否有图片数据
|
||
if (!data || (!data.images && !data.url)) {
|
||
console.log('没有新的图片需要处理');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 处理图片数组
|
||
if (data.images && data.images.length > 0) {
|
||
console.log(`处理${data.images.length}张新图片`);
|
||
for (const imageUrl of data.images) {
|
||
await this.processNewImage(imageUrl);
|
||
}
|
||
}
|
||
// 处理单张图片
|
||
else if (data.url) {
|
||
await this.processNewImage(data.url);
|
||
|
||
// 检查是否有识图照片需要上传
|
||
const ocrPhoto = this.fileList.find(file => file.name?.startsWith('识图-') && file.status === 'ready');
|
||
if (ocrPhoto) {
|
||
console.log('发现待上传的识图照片,准备上传');
|
||
// 更新识图照片的hidden属性为false,使其显示出来
|
||
const ocrIndex = this.fileList.indexOf(ocrPhoto);
|
||
this.fileList.splice(ocrIndex, 1, {
|
||
...ocrPhoto,
|
||
hidden: false,
|
||
num: "2" // 设置为第二张照片
|
||
});
|
||
|
||
// 将识图照片添加到上传队列
|
||
this.addToUploadQueue(ocrPhoto, ocrIndex);
|
||
|
||
// 通知父组件更新
|
||
this.$emit('input', this.fileList);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('处理照片失败:', error);
|
||
uni.showToast({
|
||
title: '处理照片失败',
|
||
icon: 'none',
|
||
duration: 1500
|
||
});
|
||
}
|
||
},
|
||
|
||
// 修改:handleCameraCancel 处理程序(处理识图照片)
|
||
async handleCameraCancel(data) {
|
||
if (!data || !data.url) {
|
||
this.showCamera = false;
|
||
this.$emit('camera-status-change', false);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
console.log('收到识图照片数据:', data);
|
||
|
||
// 添加识图照片(hidden为true,等待第一张照片上传后再显示)
|
||
const file = {
|
||
url: data.url,
|
||
status: "ready",
|
||
message: "待上传",
|
||
name: `识图-${Date.now()}.jpg`,
|
||
num: "2", // 预设为第二张照片
|
||
hidden: true // 初始设置为隐藏
|
||
};
|
||
|
||
if (!this.isProcessed(data.url)) {
|
||
// 如果已经有识图照片,替换它
|
||
const existingOcrIndex = this.fileList.findIndex(f => f.name?.startsWith('识图-'));
|
||
if (existingOcrIndex !== -1) {
|
||
this.fileList.splice(existingOcrIndex, 1, file);
|
||
} else {
|
||
this.fileList.push(file);
|
||
}
|
||
|
||
// 标记为已处理
|
||
this.markAsProcessed(data.url);
|
||
|
||
// 通知父组件更新
|
||
this.$emit('input', this.fileList);
|
||
|
||
console.log('识图照片已保存,等待第一张照片上传后再显示');
|
||
}
|
||
} catch (error) {
|
||
console.error('处理识图照片失败:', error);
|
||
uni.showToast({
|
||
title: '处理照片失败',
|
||
icon: 'none',
|
||
duration: 1500
|
||
});
|
||
} finally {
|
||
this.showCamera = false;
|
||
this.$emit('camera-status-change', false);
|
||
}
|
||
},
|
||
|
||
// 新增:单个文件上传方法
|
||
async uploadSingleFile(file, index) {
|
||
const parentPage = this.getParentPage();
|
||
if (!parentPage || typeof parentPage.uploadFilePromise !== 'function') {
|
||
throw new Error('上传功能不可用');
|
||
}
|
||
|
||
try {
|
||
// 更新状态为上传中
|
||
this.updateFileStatus(index, 'uploading', '上传中');
|
||
|
||
// 上传文件
|
||
const result = await parentPage.uploadFilePromise(file.url, index);
|
||
|
||
// 更新状态为成功
|
||
this.updateFileStatus(index, 'success', '', result);
|
||
|
||
console.log(`图片 ${index + 1} 上传成功:`, result);
|
||
} catch (error) {
|
||
console.error(`图片 ${index + 1} 上传失败:`, error);
|
||
this.updateFileStatus(index, 'error', '上传失败');
|
||
throw error;
|
||
}
|
||
},
|
||
|
||
// 新增:更新文件状态的辅助方法
|
||
updateFileStatus(index, status, message, newUrl = null) {
|
||
const file = this.fileList[index];
|
||
if (file) {
|
||
const updatedFile = {
|
||
...file,
|
||
status: status,
|
||
message: message
|
||
};
|
||
if (newUrl) {
|
||
updatedFile.url = newUrl;
|
||
// 保留原有的num和name属性
|
||
updatedFile.num = file.num;
|
||
updatedFile.name = file.name;
|
||
}
|
||
this.fileList.splice(index, 1, updatedFile);
|
||
this.$emit('input', this.fileList);
|
||
}
|
||
},
|
||
|
||
// 新增:检查文件是否已存在
|
||
isFileExists(url) {
|
||
return this.fileList.some(file => file.url === url);
|
||
},
|
||
|
||
// 修改:添加到上传队列的方法
|
||
async addToUploadQueue(file, index) {
|
||
// 检查是否已在队列中
|
||
const isInQueue = this.uploadQueue.some(item => item.file.url === file.url);
|
||
if (!isInQueue) {
|
||
console.log('添加新文件到上传队列:', file.url);
|
||
|
||
// 获取父页面实例
|
||
const parentPage = this.getParentPage();
|
||
if (!parentPage || typeof parentPage.uploadFilePromise !== 'function') {
|
||
console.error('上传功能不可用');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 更新状态为上传中
|
||
const fileIndex = this.fileList.indexOf(file);
|
||
if (fileIndex !== -1) {
|
||
this.fileList.splice(fileIndex, 1, {
|
||
...file,
|
||
status: 'uploading',
|
||
message: '上传中'
|
||
});
|
||
this.$emit('input', this.fileList);
|
||
// 检查上传状态并通知父组件
|
||
this.checkUploadingStatus();
|
||
}
|
||
|
||
// 上传文件
|
||
const result = await parentPage.uploadFilePromise(file.url, index);
|
||
|
||
// 更新状态为成功
|
||
if (fileIndex !== -1) {
|
||
this.fileList.splice(fileIndex, 1, {
|
||
...file,
|
||
status: 'success',
|
||
message: '上传成功',
|
||
url: result, // 使用服务器返回的URL
|
||
num: file.num, // 保留原有编号
|
||
name: file.name // 保留原有名称
|
||
});
|
||
this.$emit('input', this.fileList);
|
||
// 检查上传状态并通知父组件
|
||
this.checkUploadingStatus();
|
||
|
||
// 如果是识图照片,确保保留name属性
|
||
if (file.name && file.name.startsWith('识图-')) {
|
||
console.log(`识图照片上传成功,保留name属性: ${file.name}`);
|
||
}
|
||
}
|
||
|
||
console.log(`文件上传成功: ${file.url} -> ${result}`);
|
||
} catch (error) {
|
||
console.error('文件上传失败:', error);
|
||
// 更新状态为失败
|
||
const fileIndex = this.fileList.indexOf(file);
|
||
if (fileIndex !== -1) {
|
||
this.fileList.splice(fileIndex, 1, {
|
||
...file,
|
||
status: 'error',
|
||
message: '上传失败'
|
||
});
|
||
this.$emit('input', this.fileList);
|
||
// 检查上传状态并通知父组件
|
||
this.checkUploadingStatus();
|
||
}
|
||
}
|
||
} else {
|
||
console.log('文件已在上传队列中,跳过:', file.url);
|
||
}
|
||
},
|
||
|
||
previewImage(index) {
|
||
// 获取可见文件的URL列表
|
||
const visibleUrls = this.visibleFileList.map(item => item.url);
|
||
|
||
uni.previewImage({
|
||
current: this.visibleFileList[index].url,
|
||
urls: visibleUrls,
|
||
indicator: 'number', // 显示数字指示器
|
||
loop: true
|
||
});
|
||
},
|
||
|
||
async handleOpenCamera() {
|
||
|
||
// 检查用户是否可以上传书籍
|
||
const canUpload = await checkMemberBooksCount();
|
||
if (!canUpload) {
|
||
// 如果不能上传,则直接返回,不继续执行
|
||
return;
|
||
}
|
||
|
||
// 检查ISBN和书名是否存在
|
||
if (!this.isbn || !this.bookName) {
|
||
uni.showToast({
|
||
title: '请先填写ISBN和书名',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
if (this.fileList.length >= this.maxCount) {
|
||
uni.showToast({
|
||
title: `最多只能上传 ${this.maxCount} 张照片`,
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
this.showCamera = true;
|
||
this.$emit('camera-status-change', true);
|
||
},
|
||
|
||
// 获取父组件实例的方法
|
||
getParentPage() {
|
||
let parent = this.$parent;
|
||
while (parent) {
|
||
if (parent.uploadFilePromise) {
|
||
return parent;
|
||
}
|
||
parent = parent.$parent;
|
||
}
|
||
return null;
|
||
},
|
||
|
||
handleDelete(index) {
|
||
// 获取要删除的可见文件
|
||
const fileToDelete = this.visibleFileList[index];
|
||
|
||
// 在完整fileList中找到对应索引并删除
|
||
const actualIndex = this.fileList.findIndex(file => file === fileToDelete);
|
||
if (actualIndex !== -1) {
|
||
this.fileList.splice(actualIndex, 1);
|
||
this.$emit('input', this.fileList);
|
||
}
|
||
},
|
||
|
||
handleDeleteImage(event) {
|
||
const index = event.index;
|
||
// 获取可见文件列表中的文件
|
||
if (index >= 0 && index < this.visibleFileList.length) {
|
||
const fileToDelete = this.visibleFileList[index];
|
||
|
||
// 在完整fileList中找到对应索引并删除
|
||
const actualIndex = this.fileList.findIndex(file => file === fileToDelete);
|
||
if (actualIndex !== -1) {
|
||
this.fileList.splice(actualIndex, 1);
|
||
this.$emit('input', this.fileList);
|
||
uni.showToast({
|
||
title: '已删除图片',
|
||
icon: 'none',
|
||
duration: 1000
|
||
});
|
||
}
|
||
}
|
||
},
|
||
|
||
// 添加一个新方法,专门用于处理文件变化
|
||
handleFileChange(newFileList) {
|
||
console.log('CameraUpload组件收到文件变化:', newFileList);
|
||
|
||
// 检查是否有识图照片
|
||
const ocrPhoto = newFileList.find(file => file.name && file.name.startsWith('识图-'));
|
||
|
||
if (ocrPhoto) {
|
||
console.log('发现识图照片:', ocrPhoto);
|
||
|
||
// 确保识图照片的num为2
|
||
const updatedOcrPhoto = {
|
||
...ocrPhoto,
|
||
num: "2"
|
||
};
|
||
|
||
// 找出识图照片在列表中的位置
|
||
const ocrIndex = newFileList.findIndex(file => file.name && file.name.startsWith('识图-'));
|
||
|
||
// 更新列表中的识图照片
|
||
if (ocrIndex !== -1) {
|
||
newFileList.splice(ocrIndex, 1, updatedOcrPhoto);
|
||
}
|
||
}
|
||
|
||
// 更新文件列表
|
||
this.fileList = [...newFileList];
|
||
|
||
// 触发输入事件
|
||
this.$emit('input', this.fileList);
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.update {
|
||
margin-top: 2rpx;
|
||
background-color: #3f51b530;
|
||
border-radius: 12rpx;
|
||
padding: 20rpx;
|
||
}
|
||
|
||
.image-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.image-item {
|
||
position: relative;
|
||
width: 150rpx;
|
||
height: 150rpx;
|
||
border-radius: 8rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.preview-img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.delete-btn {
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
color: #fff;
|
||
border-radius: 50%;
|
||
width: 50rpx;
|
||
height: 50rpx;
|
||
text-align: center;
|
||
line-height: 50rpx;
|
||
font-size: 50rpx;
|
||
z-index: 2;
|
||
}
|
||
|
||
.camera-icon {
|
||
width: 150rpx;
|
||
height: 150rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #f5f5f5;
|
||
border-radius: 8rpx;
|
||
cursor: pointer;
|
||
outline: none !important;
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
|
||
.icon-img {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
outline: none !important;
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
|
||
/* 新增:上传状态样式 */
|
||
.upload-status {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
padding: 4rpx;
|
||
font-size: 20rpx;
|
||
color: #fff;
|
||
text-align: center;
|
||
z-index: 2;
|
||
}
|
||
|
||
.uploading {
|
||
background-color: rgba(0, 122, 255, 0.7);
|
||
}
|
||
|
||
.error {
|
||
background-color: rgba(255, 59, 48, 0.7);
|
||
}
|
||
|
||
/* 添加提示文字样式 */
|
||
.photo-tip {
|
||
position: fixed;
|
||
top: 20rpx;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 10000;
|
||
text-align: center;
|
||
padding: 10rpx;
|
||
}
|
||
|
||
.photo-tip text {
|
||
background-color: rgba(0, 0, 0, 0.6);
|
||
color: #fff;
|
||
padding: 10rpx 30rpx;
|
||
border-radius: 30rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
</style> |