本文档提供 ImageUpload 模块在前端(Shop)的使用示例。
{{-- 在 Blade 文件中引入 Vue 组件 --}}
@extends('shop::layouts.master')
@section('content')
<div id="app">
<h2>上传头像</h2>
{{-- 单张图片上传 --}}
<image-upload-component
:multiple="false"
label="点击或拖拽上传头像"
hint="支持 JPG, PNG, GIF 格式,最大 5MB"
directory="user/avatars"
@uploaded="onAvatarUploaded"
></image-upload-component>
<div v-if="avatarUrl">
<p>头像预览:</p>
<img :src="avatarUrl" alt="Avatar" style="max-width: 200px;">
</div>
<hr>
<h2>上传产品图片</h2>
{{-- 多张图片上传 --}}
<image-upload-component
:multiple="true"
label="点击或拖拽上传产品照片"
hint="最多上传 5 张图片"
:max-files="5"
directory="product/images"
@uploaded="onProductImagesUploaded"
></image-upload-component>
<div v-if="productImages.length > 0">
<p>已上传 {{ productImages.length }} 张图片:</p>
<div v-for="(img, index) in productImages" :key="index">
<img :src="img.url" :alt="'Image ' + (index + 1)" style="max-width: 150px; margin: 5px;">
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
// 注册全局组件
Vue.component('image-upload-component', {
template: `@include('imageupload::components.image-upload')`
});
new Vue({
el: '#app',
data: {
avatarUrl: null,
productImages: []
},
methods: {
onAvatarUploaded(data) {
console.log('头像上传成功:', data);
this.avatarUrl = data.url;
// 可以保存到数据库
this.saveAvatarToDatabase(data.path);
},
onProductImagesUploaded(results) {
console.log('产品图片上传成功:', results);
this.productImages = results.filter(r => r.success);
// 可以保存到数据库
this.saveProductImages(this.productImages);
},
async saveAvatarToDatabase(path) {
try {
const response = await fetch('/api/customer/profile/update-avatar', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ avatar_path: path })
});
const data = await response.json();
if (data.success) {
alert('头像保存成功');
}
} catch (error) {
console.error('保存失败:', error);
}
},
async saveProductImages(images) {
// 类似地保存到数据库
console.log('保存图片到数据库:', images);
}
}
});
</script>
@endpush
<template>
<div class="profile-page">
<h2>个人资料</h2>
<div class="avatar-section">
<label>头像</label>
<ImageUpload
:multiple="false"
directory="user/avatars"
@uploaded="handleAvatarUpload"
/>
<img v-if="avatar" :src="avatar" class="avatar-preview" />
</div>
<div class="gallery-section">
<label>相册</label>
<ImageUpload
:multiple="true"
:max-files="10"
directory="user/gallery"
@uploaded="handleGalleryUpload"
/>
<div class="gallery-grid">
<div v-for="img in gallery" :key="img.id" class="gallery-item">
<img :src="img.url" />
<button @click="deleteImage(img)">删除</button>
</div>
</div>
</div>
</div>
</template>
<script>
import ImageUpload from '@/components/ImageUpload.vue';
export default {
components: {
ImageUpload
},
data() {
return {
avatar: null,
gallery: []
};
},
methods: {
handleAvatarUpload(data) {
this.avatar = data.url;
// 保存到后端
axios.post('/api/customer/profile/avatar', {
path: data.path
}).then(response => {
this.$toast.success('头像更新成功');
});
},
handleGalleryUpload(results) {
const successful = results.filter(r => r.success);
this.gallery.push(...successful.map((img, index) => ({
id: Date.now() + index,
url: img.url,
path: img.path
})));
},
async deleteImage(image) {
if (!confirm('确定删除这张图片吗?')) return;
try {
const response = await fetch('/api/image-upload/delete', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({ path: image.path })
});
const data = await response.json();
if (data.success) {
this.gallery = this.gallery.filter(img => img.id !== image.id);
this.$toast.success('删除成功');
}
} catch (error) {
this.$toast.error('删除失败');
}
}
}
};
</script>
<style scoped>
.avatar-preview {
width: 150px;
height: 150px;
border-radius: 50%;
object-fit: cover;
margin-top: 10px;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
margin-top: 20px;
}
.gallery-item {
position: relative;
}
.gallery-item img {
width: 100%;
height: 150px;
object-fit: cover;
border-radius: 8px;
}
.gallery-item button {
position: absolute;
top: 5px;
right: 5px;
background: red;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
</style>
// 获取 CSRF Token
function getCsrfToken() {
return document.querySelector('meta[name="csrf-token"]')?.content;
}
// 上传单张图片
async function uploadSingleImage(file, directory = 'shop/uploads') {
const formData = new FormData();
formData.append('image', file);
formData.append('directory', directory);
try {
const response = await fetch('/api/image-upload/upload', {
method: 'POST',
body: formData,
headers: {
'X-CSRF-TOKEN': getCsrfToken()
}
});
const data = await response.json();
if (data.success) {
return data.data; // { url, path, ... }
} else {
throw new Error(data.message);
}
} catch (error) {
console.error('Upload failed:', error);
throw error;
}
}
// 上传多张图片
async function uploadMultipleImages(files, directory = 'shop/uploads') {
const formData = new FormData();
files.forEach(file => {
formData.append('images[]', file);
});
formData.append('directory', directory);
try {
const response = await fetch('/api/image-upload/upload-multiple', {
method: 'POST',
body: formData,
headers: {
'X-CSRF-TOKEN': getCsrfToken()
}
});
const data = await response.json();
if (data.success) {
return data.data; // Array of results
} else {
throw new Error(data.message);
}
} catch (error) {
console.error('Upload failed:', error);
throw error;
}
}
// 使用示例
document.getElementById('imageInput').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const result = await uploadSingleImage(file, 'user/photos');
console.log('上传成功:', result.url);
// 显示预览
const preview = document.getElementById('preview');
preview.src = result.url;
preview.style.display = 'block';
} catch (error) {
alert('上传失败: ' + error.message);
}
});
function uploadWithProgress(file, onProgress) {
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append('image', file);
formData.append('directory', 'shop/uploads');
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
onProgress(percentComplete);
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
if (data.success) {
resolve(data.data);
} else {
reject(new Error(data.message));
}
} else {
reject(new Error('Upload failed'));
}
});
xhr.addEventListener('error', () => {
reject(new Error('Network error'));
});
xhr.open('POST', '/api/image-upload/upload');
xhr.setRequestHeader('X-CSRF-TOKEN', getCsrfToken());
xhr.send(formData);
});
}
// 使用示例
const fileInput = document.getElementById('imageInput');
const progressBar = document.getElementById('progressBar');
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const result = await uploadWithProgress(file, (progress) => {
progressBar.value = progress;
progressBar.textContent = Math.round(progress) + '%';
});
console.log('上传完成:', result.url);
} catch (error) {
alert('上传失败: ' + error.message);
}
});
// 单张图片上传
function uploadImage(file) {
var formData = new FormData();
formData.append('image', file);
formData.append('directory', 'shop/uploads');
$.ajax({
url: '/api/image-upload/upload',
type: 'POST',
data: formData,
processData: false,
contentType: false,
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
},
success: function(response) {
if (response.success) {
console.log('上传成功:', response.data.url);
$('#preview').attr('src', response.data.url).show();
} else {
alert('上传失败: ' + response.message);
}
},
error: function(xhr) {
alert('请求失败');
}
});
}
// 绑定事件
$('#imageInput').on('change', function() {
var file = this.files[0];
if (file) {
uploadImage(file);
}
});
import React, { useState } from 'react';
function ImageUpload({ multiple = false, directory = 'shop/uploads', onUpload }) {
const [uploading, setUploading] = useState(false);
const [preview, setPreview] = useState(null);
const handleUpload = async (files) => {
setUploading(true);
const formData = new FormData();
if (multiple) {
Array.from(files).forEach(file => {
formData.append('images[]', file);
});
} else {
formData.append('image', files[0]);
}
formData.append('directory', directory);
try {
const response = await fetch(
multiple ? '/api/image-upload/upload-multiple' : '/api/image-upload/upload',
{
method: 'POST',
body: formData,
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content
}
}
);
const data = await response.json();
if (data.success) {
if (!multiple) {
setPreview(data.data.url);
}
onUpload(data.data);
} else {
alert('上传失败: ' + data.message);
}
} catch (error) {
alert('上传失败: ' + error.message);
} finally {
setUploading(false);
}
};
return (
<div className="image-upload">
<input
type="file"
accept="image/*"
multiple={multiple}
onChange={(e) => handleUpload(e.target.files)}
disabled={uploading}
/>
{uploading && <p>上传中...</p>}
{preview && !multiple && (
<img src={preview} alt="Preview" style={{ maxWidth: '300px' }} />
)}
</div>
);
}
export default ImageUpload;
// 在用户资料页面
async function updateAvatar(file) {
const result = await uploadSingleImage(file, 'user/avatars');
// 更新用户资料
await fetch('/api/customer/profile', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': getCsrfToken()
},
body: JSON.stringify({
avatar: result.path
})
});
// 更新页面显示
document.getElementById('userAvatar').src = result.url;
}
// 在评价表单中
async function submitReview(reviewData, images) {
let imagePaths = [];
if (images && images.length > 0) {
const uploadResults = await uploadMultipleImages(images, 'review/images');
imagePaths = uploadResults
.filter(r => r.success)
.map(r => r.path);
}
// 提交评价
const response = await fetch('/api/product/review', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': getCsrfToken()
},
body: JSON.stringify({
...reviewData,
images: imagePaths
})
});
return response.json();
}
// 在聊天界面
async function sendChatMessage(message, image) {
let imageUrl = null;
if (image) {
const result = await uploadSingleImage(image, 'chat/messages');
imageUrl = result.url;
}
// 发送消息
await fetch('/api/chat/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': getCsrfToken()
},
body: JSON.stringify({
message: message,
image_url: imageUrl
})
});
}
Q: 如何限制上传的文件类型?
A: 在 input 元素上使用 accept 属性:accept="image/jpeg,image/png,image/gif"
Q: 如何实现拖拽上传?
A: 监听 dragover, dragleave, drop 事件,参考 Vue 组件中的实现
Q: 上传大文件时超时怎么办?
A: 增加服务器超时时间,或实现分片上传
Q: 如何在移动端优化?
A: 使用 capture 属性直接调用相机:<input type="file" accept="image/*" capture>