FRONTEND_EXAMPLES.md 17 KB

前端使用示例

本文档提供 ImageUpload 模块在前端(Shop)的使用示例。

目录

Vue 组件使用

1. 在 Blade 模板中使用

{{-- 在 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

2. 在 Vue SFC 中使用

<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>

JavaScript Fetch API

基础用法

// 获取 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);
    }
});

jQuery AJAX

// 单张图片上传
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);
    }
});

React 组件

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;

实际应用场景

1. 用户头像上传

// 在用户资料页面
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;
}

2. 商品评价图片上传

// 在评价表单中
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();
}

3. 客服聊天图片发送

// 在聊天界面
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
        })
    });
}

注意事项

  1. CSRF Token: 确保在所有请求中包含 CSRF token
  2. 文件大小限制: 前端验证文件大小,避免上传过大文件
  3. 错误处理: 妥善处理上传失败的情况
  4. 用户体验: 显示上传进度和状态反馈
  5. 图片预览: 上传前显示预览,提升用户体验
  6. 安全性: 验证文件类型,防止恶意文件上传

常见问题

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>