Files
i-love/resources/views/upload.blade.php
2025-06-17 16:45:14 +08:00

385 lines
16 KiB
PHP

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片上传 | Laravel相册 - 暗色主题</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
.dropzone {
border: 2px dashed #4b5563;
border-radius: 0.5rem;
transition: all 0.3s ease;
}
.dropzone.active {
border-color: #818cf8;
background-color: #1e293b;
}
.sort-handle {
cursor: move;
transition: transform 0.2s;
}
.sort-handle:hover {
transform: scale(1.1);
}
.sortable-ghost {
opacity: 0.5;
background: #4f46e5;
}
</style>
</head>
<body class="bg-gray-900 min-h-screen text-gray-200">
<div id="app" class="max-w-4xl mx-auto p-4 sm:p-6">
<!-- 顶部导航 -->
<nav class="flex justify-between items-center mb-8">
<h1 class="text-2xl font-bold text-indigo-400">
<i class="fas fa-images mr-2"></i>图片上传
</h1>
<div>
<a href="#" class="bg-indigo-700 hover:bg-indigo-600 text-white px-4 py-2 rounded-lg transition">
返回相册
</a>
</div>
</nav>
<!-- 主卡片 -->
<div class="bg-gray-800 rounded-xl shadow-lg overflow-hidden border border-gray-700">
<!-- 标题区域 -->
<div class="bg-gradient-to-r from-indigo-800 to-purple-800 p-6 text-white">
<h2 class="text-xl font-bold">上传新图片</h2>
<p class="mt-1 opacity-90">选择相册并上传图片(支持拖拽排序)</p>
</div>
<!-- 表单区域 -->
<div class="p-6">
<!-- 全局加载指示器 -->
<div v-if="isLoading" class="fixed inset-0 bg-black bg-opacity-70 z-50 flex items-center justify-center">
<div class="text-center bg-gray-800 rounded-xl p-8 shadow-xl border border-gray-700">
<div class="animate-spin rounded-full h-16 w-16 border-b-2 border-indigo-600 mx-auto mb-4"></div>
<h3 class="text-xl font-medium text-gray-200"> @{{ current }} / @{{ totalUpload }}上传中...</h3>
<p class="mt-2 text-gray-400">请稍候,正在上传图片</p>
</div>
</div>
<!-- 相册选择 -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-300 mb-2">选择相册</label>
<select v-model="selectedAlbum" class="w-full p-3 bg-gray-700 border border-gray-600 rounded-lg focus:ring-indigo-500 focus:border-indigo-500">
<option value="" disabled selected>请选择相册</option>
<option v-for="album in albums" :value="album.id">@{{ album.title }}</option>
</select>
</div>
<!-- 文件上传区域 -->
<div class="mb-6">
<div
@dragover.prevent="dragActive = true"
@dragleave="dragActive = false"
@drop.prevent="handleDrop"
:class="{'active': dragActive}"
class="dropzone p-8 text-center cursor-pointer transition">
<input type="file" multiple @change="handleFileSelect" class="hidden" ref="fileInput">
<div v-if="!images.length">
<i class="fas fa-cloud-upload-alt text-5xl text-indigo-500 mb-4"></i>
<h3 class="text-lg font-medium text-gray-300 mb-2">拖放图片到这里</h3>
<p class="text-gray-500 mb-4"></p>
<button @click="$refs.fileInput.click()" class="bg-indigo-700 text-white px-6 py-2 rounded-lg hover:bg-indigo-600 transition">
选择图片
</button>
<p class="mt-3 text-gray-500 text-sm">支持 JPG, PNG 格式,最大 10MB</p>
</div>
<div v-else>
<button @click="$refs.fileInput.click()" class="bg-indigo-700 text-white px-6 py-2 rounded-lg hover:bg-indigo-600 transition mb-4">
<i class="fas fa-plus mr-2"></i>添加更多图片
</button>
</div>
</div>
</div>
<!-- 图片预览区域 -->
<div v-if="images.length" class="mb-6 transition-all">
<div class="flex justify-between items-center mb-4">
<h3 class="font-medium text-gray-300">已选图片 <span class="text-indigo-400">@{{ images.length }}</span> </h3>
<div>
<button @click="clearAll" class="text-red-400 hover:text-red-300 mr-3">
<i class="fas fa-trash mr-1"></i>清空
</button>
</div>
</div>
<div class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 gap-3" ref="gridContainer">
<div
v-for="(image, index) in images"
:key="image.id"
class="relative group overflow-hidden rounded-lg border border-gray-700 bg-gray-700 shadow-sm transition-transform duration-300 hover:shadow-md hover:-translate-y-0.5">
<!-- 排序手柄 -->
<div class="absolute top-2 left-2 text-indigo-400 sort-handle">
<i class="fas fa-arrows-alt"></i>
</div>
<!-- 删除按钮 -->
<button @click="removeImage(index)" class="absolute top-2 right-2 bg-gray-800 text-red-400 rounded-full w-6 h-6 flex items-center justify-center opacity-0 group-hover:opacity-100 transition">
<i class="fas fa-times"></i>
</button>
<!-- 缩略图 -->
<img :src="image.preview" class="w-full h-full object-cover">
<!-- 图片信息 -->
<div class="p-2 text-center">
<p class="text-xs truncate text-gray-300">@{{ image.file.name }}</p>
<p class="text-xs text-gray-500">@{{ formatFileSize(image.file.size) }}</p>
</div>
<!-- 图片编号 -->
<div class="absolute bottom-10 right-2 bg-indigo-600 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs">
@{{ index + 1 }}
</div>
</div>
</div>
<div class="mt-4 flex justify-between">
<div class="text-sm text-gray-500">
提示:拖拽图片左上角的 <i class="fas fa-arrows-alt text-indigo-400 mx-1"></i> 可以调整顺序
</div>
<button
@click="uploadImages"
:disabled="!selectedAlbum || !images.length || isLoading"
:class="{'opacity-70 cursor-not-allowed': !selectedAlbum || !images.length || isLoading, 'hover:bg-indigo-600': selectedAlbum && images.length}"
class="bg-indigo-700 text-white px-6 py-2 rounded-lg transition flex items-center">
<template v-if="isLoading">
<i class="fas fa-spinner animate-spin mr-2"></i>
上传中...
</template>
<template v-else>
<i class="fas fa-cloud-upload-alt mr-2"></i>
开始上传
</template>
</button>
</div>
</div>
</div>
</div>
<!-- 上传成功提示 -->
<transition name="fade">
<div v-if="showSuccess" class="fixed bottom-6 right-6 bg-green-700 text-white py-3 px-6 rounded-lg shadow-lg flex items-center z-50">
<i class="fas fa-check-circle text-xl mr-3"></i>
<div>
<div class="font-medium">上传成功!</div>
<div class="text-sm">成功上传 @{{ uploadedCount }} 张图片</div>
</div>
<button @click="showSuccess = false" class="ml-6 text-green-300 hover:text-white">
<i class="fas fa-times"></i>
</button>
</div>
</transition>
<!-- 页脚 -->
<footer class="mt-12 text-center text-gray-500 text-sm">
<p>Laravel 12.x 图片上传系统 &copy; 2023 | 暗色主题</p>
</footer>
</div>
<script>
const { createApp, ref, onMounted, nextTick } = Vue;
createApp({
setup() {
// 基础变量
const images = ref([]);
const fileInput = ref(null);
const dragActive = ref(false);
const isLoading = ref(false);
const selectedAlbum = ref('');
const showSuccess = ref(false);
const uploadedCount = ref(0);
const gridContainer = ref(null);
let sortableInstance = null;
const totalUpload = ref(0);
const current = ref(0);
// 相册数据(模拟)
const albums = <?php echo json_encode($photo_album); ?>;
// 处理文件选择
const handleFileSelect = (e) => {
addFiles(e.target.files);
// 重置文件输入以允许重复选择相同文件
e.target.value = null;
};
// 处理拖放文件
const handleDrop = (e) => {
dragActive.value = false;
if (e.dataTransfer.files.length) {
addFiles(e.dataTransfer.files);
}
};
// 添加文件到列表
const addFiles = (files) => {
for (let i = 0; i < files.length; i++) {
const file = files[i];
// 限制为图片格式
if (file && file.type.includes('image/')) {
images.value.push({
id: Date.now() + Math.random(),
file: file,
preview: URL.createObjectURL(file)
});
}
}
// 添加图片后重新初始化Sortable
initSortable();
};
// 移除图片
const removeImage = (index) => {
URL.revokeObjectURL(images.value[index].preview);
images.value.splice(index, 1);
// 移除图片后重新初始化Sortable
initSortable();
};
// 清空所有图片
const clearAll = () => {
images.value.forEach(img => URL.revokeObjectURL(img.preview));
images.value = [];
// 清空后销毁Sortable实例
destroySortable();
};
// 格式化文件大小
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
// 图片上传
const uploadImages = async () => {
// 调试代码可以保留,但正式使用时建议注释掉
console.log(images.value, '上传前的图片列表');
totalUpload.value = images.value.length;
// 检查条件:相册未选择、无图片、或正在上传中
if (!selectedAlbum.value || !images.value.length || isLoading.value) return;
isLoading.value = true;
let successCount = 0;
try {
// 使用 for...of 循环逐个上传
for (const image of images.value) {
successCount++;
current.value = successCount;
const formData = new FormData();
formData.append('pid', selectedAlbum.value);
formData.append('image', image.file);
// 等待当前图片上传完成
await axios.post('/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
console.log(`成功上传第 ${successCount} 张图片`);
}
// 全部上传完成后清空列表并显示成功消息
clearAll();
uploadedCount.value = successCount;
showSuccess.value = true;
// 5秒后自动隐藏成功消息
setTimeout(() => {
showSuccess.value = false;
}, 5000);
} catch (error) {
console.error('上传过程中出错:', error);
// 出错时不清空列表,允许用户重试
} finally {
isLoading.value = false; // 无论成功或失败,最终关闭加载状态
}
};
// 初始化Sortable拖拽功能
const initSortable = () => {
// 确保DOM已经更新
nextTick(() => {
// 销毁旧的Sortable实例
if (sortableInstance) {
sortableInstance.destroy();
sortableInstance = null;
}
// 确保容器存在且有图片
if (gridContainer.value && images.value.length > 0) {
sortableInstance = new Sortable(gridContainer.value, {
handle: '.sort-handle',
animation: 150,
ghostClass: 'sortable-ghost',
onEnd: (e) => {
// 更新数组顺序
const movedItem = images.value.splice(e.oldIndex, 1)[0];
images.value.splice(e.newIndex, 0, movedItem);
}
});
}
});
};
// 销毁Sortable实例
const destroySortable = () => {
if (sortableInstance) {
sortableInstance.destroy();
sortableInstance = null;
}
};
onMounted(() => {
// 初始加载完成后初始化Sortable
initSortable();
});
return {
images,
albums,
selectedAlbum,
fileInput,
dragActive,
isLoading,
showSuccess,
uploadedCount,
gridContainer,
handleFileSelect,
handleDrop,
removeImage,
clearAll,
formatFileSize,
uploadImages
};
}
}).mount('#app');
</script>
</body>
</html>