laravelguru/laravel-filehandler

该 Laravel Inertia React 文件管理包通过 React 和 Inertia.js 无缝地将文件管理集成到您的 Laravel 应用程序中。它提供了预构建的文件输入组件和弹出文件对话框,简化了文件上传、存储、浏览和管理。


README

Latest Version on Packagist Total Downloads GitHub Actions

概述

该 Laravel Inertia React 文件管理包使用 React 和 Inertia.js 为您的 Laravel 应用程序提供无缝的文件管理功能。它包括预构建的文件输入组件和弹出文件对话框,方便文件上传、存储、浏览和管理。

功能

  • 无缝集成 React 和 Inertia.js
  • 使用 ShadCN 组件(对话框、按钮、滚动区域和选项卡)
  • 预构建的文件输入组件和弹出文件对话框
  • 平滑的单页应用程序(SPA)转换
  • 全面文件操作:上传、下载、删除、移动文件
  • 响应式文件浏览器
  • 适用于 CMS、电子商务平台、项目管理工具和个人作品集

安装

您可以通过 composer 安装此包

composer require laravelguru/laravel-filehandler

注册服务提供者

如果您使用的是 Laravel 11 或更新版本,您应该在 bootstrap/providers.php 中添加服务提供者

<?php

return [
    // Other Service Providers
    LaravelGuru\LaravelFilehandler\ServiceProvider::class,
];

发布资产

运行以下命令以发布包资产

  • 此包仅在应用程序以网络环境运行时加载,确保在命令行操作期间不会不必要地加载。通过检查应用程序是否在控制台运行使用 app()->runningInConsole();
    php artisan serve
    npm run dev
  • 通过仅在需要时激活,该包通过检查应用程序是否在控制台运行来优化性能,保持应用程序在 CLI 操作期间轻量级。

在发布资源之前,请了解用例

  • 如果您不使用预构建的组件,只需要迁移即可
  • 如果您不使用预构建的组件,则可能不需要迁移、模型、资源、组件的修改
  • 否则,您需要要求一切
php artisan vendor:publish --provider="LaravelGuru\LaravelFilehandler\LaravelFilehandlerServiceProvider"

or

php artisan vendor:publish --provider="LaravelGuru\LaravelFilehandler\LaravelFilehandlerServiceProvider" --tag=filehandler-config
php artisan vendor:publish --provider="LaravelGuru\LaravelFilehandler\LaravelFilehandlerServiceProvider" --tag=filehandler-migration
php artisan vendor:publish --provider="LaravelGuru\LaravelFilehandler\LaravelFilehandlerServiceProvider" --tag=filehandler-controller
php artisan vendor:publish --provider="LaravelGuru\LaravelFilehandler\LaravelFilehandlerServiceProvider" --tag=filehandler-resource
php artisan vendor:publish --provider="LaravelGuru\LaravelFilehandler\LaravelFilehandlerServiceProvider" --tag=filehandler-model
php artisan vendor:publish --provider="LaravelGuru\LaravelFilehandler\LaravelFilehandlerServiceProvider" --tag=filehandler-components
php artisan vendor:publish --provider="LaravelGuru\LaravelFilehandler\LaravelFilehandlerServiceProvider" --tag=filehandler-css

链接 Laravel 的存储

php artisan storage:link

安装 shadcn/ui 组件(仅当使用预定义组件时)

按照官方安装指南初始化 shadcn/ui 组件。使用以下命令添加所需组件

npx shadcn-ui@latest add button
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add tabs
npx shadcn-ui@latest add scroll-area

设置

  • 安装依赖项:确保您已在 Laravel 项目中设置 React 和 Inertia.js。
  • 集成组件:在您的应用程序中使用提供的 React 组件和 Inertia.js 中间件。
  • 自定义:根据需要修改组件和处理程序以符合您的需求。
  • 运行迁移:将迁移应用到您的数据库

运行迁移

当应用程序启动时,服务提供者将自动为 file_repos 表生成迁移。使用以下命令运行迁移

php artisan migrate

用法

虽然视图/组件对于 API 开发可能是可选的,但集成它们可以增强用户体验。此包专注于提供核心 API 路由,用于索引、显示、存储、更新和删除操作。如果您主要处理文件管理,此包可以作为坚实的基础。

    public function __construct(FileService $fileService)
  • 描述:构造函数方法,将 FileService 类注入到 FileController 类中,使其可用。
    public function index()

描述:检索并返回与已认证用户关联的文件列表,位于 'documents' 文件夹中。

    public function store(Request $request)

描述:处理新文件的上传。它从请求中接收文件,处理上传,并返回一个 JSON 响应,指示操作的成功或失败。

    private function upload($user_id, $files)

描述: 一个私有方法,用于执行文件上传到存储并更新数据库的操作,在事务中执行。它返回上传文件的详细信息。

    public function show(File $file)

描述: 获取并返回特定文件的详细信息。

    public function update(Request $request, File $file)

描述: 使用新数据更新现有文件。它处理文件修改并返回一个JSON响应,指示操作的成败。

    public function destroy(File $file)

描述: 删除指定的文件,并返回一个JSON响应,指示删除操作的成败。

利用预构建组件进行高效开发。

本包提供基于Shadcn UI构建的即用文件输入和多个文件处理组件。使用预设计的可定制元素简化开发流程。享受提升的用户体验和快速原型设计。

文件输入

处理多个文件

  • 验证规则配置
    $data = $request->validate([
        "cv_path" => ["nullable", "integer", "exists:files,id"],
        "brochures" => ["nullable", "array", "max:5"],
        "brochures.*" => ["nullable", "integer", "exists:files,id"],
    ])
  • 数据库迁移设置
    public function up(): void
    {
        Schema::create('courses', function (Blueprint $table) {
            $table->string('brochures')->nullable();
        });
    }
  • 创建函数的控制器方法(确保在视图中的文件同步时关注documents变量
public function create()
{
    $faculties = Faculty::query()->orderBy('name', 'asc')->get();
    $documents = File::query()->where('user_id', auth()->id())->where('folder', 'documents')->paginate(9)->onEachSide(1);

    return Inertia::render('Courses/Create', [
        'documents' => FileResource::collection($documents),
        'faculties' => FacultyResource::collection($faculties),
    ]);
}
  • 创建视图中使用文件输入进行文件同步的指南(确保在视图中的文件同步时关注documents变量 + brochures
export default function Create({ auth, documents, faculties }) {
    const { data, setData, post, processing, errors, reset } = useForm({
        brochures: null,
    });

    const [files, setFiles] = useState([]);
    const [submitTriggered, setSubmitTriggered] = useState(false);

    const onSubmit = (e) => {
        e.preventDefault();

        if (files.length === 0) {
        post(route("courses.store"), {
            onSuccess: () => {
                reset();
            },
            onError: () => {
                console.log("Error creating course");
            },
        });
        return;
        }

        setData(
            "brochures",
            files.map((file) => file.id)
        );

        setSubmitTriggered(true);
    };

    useEffect(() => {
        if (submitTriggered) {
        post(route("courses.store"), {
            onSuccess: () => {
                reset();
                setSubmitTriggered(false);
            },
            onError: () => {
                console.log("Error creating course");
                setSubmitTriggered(false);
            },
        });
        }
    }, [submitTriggered]);

    return (
        <div className="grid gap-2">
            <Label htmlFor="name">Brochures</Label>
            <div className="w-full overflow-x-auto">
                <FileInput
                    selectedFiles={files}
                    onFileChange={(files) => {
                        setFiles(files);
                    }}
                    apiUrl={route("files.store")}
                    multiple={true}
                    documents={documents}
                />
            </div>
        </div>
    )
}
  • 存储函数的控制器方法(确保在相应数据库表中存储文件时关注brochures变量
    public function store(StoreCourseRequest $request)
        {
            $data = $request->validated();

            if (isset($data['brochures'])) {
                $data['brochures'] = json_encode($data['brochures'], true);
            }

            $data['created_by'] = auth()->id();
            $data['updated_by'] = auth()->id();

            if (Gate::allows('create_course')) {
                Course::create($data);
                return redirect()->route('courses.index')->with('success', 'Course created successfully');
            } else {
                return redirect()->back()->with('error', 'You are not authorized to create a course');
            }
        }
  • 编辑函数的控制器方法(确保在视图中的文件同步时关注documents变量 + brochures
    public function edit(Course $course)
    {
        if (Gate::allows('update_course', $course)) {
            $faculties = Faculty::query()->orderBy('name', 'asc')->get();
            $documents = File::query()->where('user_id', auth()->id())->where('folder', 'documents')->paginate(9)->onEachSide(1);

            $array = json_decode($course->brochures) ?? [];
            $brochures = File::whereIn('id', $array)->get();

        return Inertia::render('Courses/Edit', [
            'course' => new CourseResource($course),
            'brochures' => FileResource::collection($brochures),
            'documents' => FileResource::collection($documents),
            'faculties' => FacultyResource::collection($faculties),
        ]);
        } else {
        return redirect()->back()->with('error', 'You are not authorized to edit this course');
        }
    }
  • 编辑视图中使用文件输入进行文件同步的指南(确保在视图中的文件同步时关注documents变量 + brochures
export default function Edit({auth, course, brochures, documents, faculties }){
    const { data, setData, put, processing, errors, reset } = useForm({
        brochures: null,
    })

    const [files, setFiles] = useState(brochures.data ?? []);

    const onSubmit = (e) => {
        e.preventDefault();
        if (files.length === 0) {
            put(route("courses.update", course.id), {
                preserveScroll: true,
                onSuccess: () => {
                reset();
                },
                onError: () => {
                console.log("Error updating course");
                },
            });
        return;
        }

    setData(
      "brochures",
      files.map((file) => file.id)
    );

    setSubmitTriggered(true);
  };

    useEffect(() => {
        if (submitTriggered) {
        put(route("courses.update", course.id), {
            onSuccess: () => {
            reset();
            setSubmitTriggered(false);
            },
            onError: () => {
            console.log("Error updating course");
            setSubmitTriggered(false);
            },
        });
        }
    }, [submitTriggered]);

    return(
        <div className="grid gap-2">
            <Label htmlFor="name">Brochures</Label>
            <div className="w-full overflow-x-auto">
                <FileInput
                selectedFiles={files}
                onFileChange={(files) => {
                    setFiles(files);
                }}
                apiUrl={route("files.store")}
                multiple={true}
                documents={documents}
                />
            </div>
        </div>
        )
    }
  • 更新函数的控制器方法(确保在相应数据库表中更新文件时关注brochures变量
    public function update(UpdateCourseRequest $request, Course $course)
    {
    //
        $data = $request->validated();

        if (isset($data['brochures'])) {
        $data['brochures'] = json_encode($data['brochures'], true);
        }

        $data['updated_by'] = auth()->id();

        if (Gate::allows('update_course', $course)) {
        $course->update($data);
        return redirect()->route('courses.index')->with('success', 'Course updated successfully');
        } else {
        return redirect()->back()->with('error', 'You are not authorized to update this course');
        }
    }

处理单个文件

  • 验证规则配置
    $data = $request->validate([
        "cv_path" => ["nullable", "integer", "exists:files,id"],
    ])
  • 数据库迁移设置
    public function up(): void
    {
        Schema::create('users', function (Blueprint $table) {
            $table->string('cv_path')->nullable();
        });
    }
  • 编辑函数的控制器方法(确保在视图中的文件同步时编辑文件时关注cv_path和documents变量
  public function edit(User $user)
  {
        $documents = File::query()->where('user_id', auth()->id())->where('folder', 'documents')->paginate(9)->onEachSide(1);

        $cv_array = [json_decode($user->cv_path)] ?? [];
        $cv_path = [];

        if (is_array($cv_array) && count($cv_array) > 0) {
            $cv_path = File::whereIn('id', $cv_array)->get();
        }

        return Inertia::render('User/Edit', [
          'user' => new UserResource($user),
          'cv_path' => FileResource::collection($cv_path),
          'documents' => FileResource::collection($documents),
        ]);
    }
  • 编辑视图中使用文件输入进行文件同步的指南(确保在视图中的文件同步时关注cv_path变量 + brochures
export default function EditStaff({ auth, user, cv_path, documents }) {
    const { data, setData, put, processing, errors, reset } = useForm({
        cv_path: null
    });

    const [cvFile, setCvFile] = useState(cv_path.data ?? []);

    return (
        <div className="grid gap-2">
            <Label htmlFor="cv_path">Upload CV</Label>
            <div className="w-full overflow-x-auto">
                <FileInput
                    selectedFiles={cvFile}
                    onFileChange={(files) => {
                        setCvFile(files);
                        setData("cv_path", files?.[0]?.id);
                    }}
                    apiUrl={route("files.store")}
                    multiple={false}
                    documents={documents}
                />
            </div>
        </div>
    )
}

显示文件处理

  • 显示函数的控制器方法(确保在视图中的文件同步时显示文件时关注brochures变量
    public function show(Course $course)
    {
        $modules = $course->modules()->orderBy('created_at', 'asc')
        ->paginate(10)
        ->onEachSide(1);

        $array = json_decode($course->brochures) ?? [];
        $brochures = File::whereIn('id', $array)->get();

        return Inertia::render('Courses/Show', [
            'course' => new CourseResource($course->loadCount('modules')),
            'brochures' => FileResource::collection($brochures),
            'modules' => ModuleResource::collection($modules),
        ]);
    }
  • 显示视图中使用文件输入进行文件同步的文件的指南(确保在视图中的文件同步时关注brochures变量
export default function Show({ auth, course, brochures, modules }) {
    return(
        <div>
        {
            brochures.data.map((brochure) => (
                <div className="flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6">
                    <div className="flex w-0 flex-1 items-center">
                    <svg
                        className="h-5 w-5 flex-shrink-0 text-gray-400"
                        viewBox="0 0 20 20"
                        fill="currentColor"
                        aria-hidden="true"
                    >
                        <path
                        fillRule="evenodd"
                        d="M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z"
                        clipRule="evenodd"
                        />
                    </svg>
                    <div className="ml-4 flex min-w-0 flex-1 justify-between">
                        <span className="truncate font-medium hover:underline">
                        <a
                            href={brochure.path}
                            target="_blank"
                            rel="noopener noreferrer"
                        >
                            {brochure.name}
                        </a>
                        </span>
                        <span className="flex-shrink-0 text-gray-400">
                        2.4MB
                        </span>
                    </div>
                    </div>
                    <div className="ml-4 flex-shrink-0">
                    <a
                        href={brochure.path}
                        download
                        className="font-medium text-indigo-600 hover:text-indigo-500"
                    >
                        Download
                    </a>
                    </div>
                </div>
            ))
        }
        </div>
    )
}

文件对话框

  • 验证规则配置
    $data = $request->validate([
        "image" => ["nullable", "string"],
    ])
  • 数据库迁移设置
    public function up(): void
    {
        Schema::create('courses', function (Blueprint $table) {
            $table->string('image')->nullable();
        });
    }
  • 前端实现
    <div className="relative h-full w-80 bg-gray-100 dark:bg-gray-900">
        <img
        src={data.image ?? "https://via.placeholder.com/150"}
        alt="Course Image"
        className="w-full h-full object-cover ring-1 ring-gray-700 dark:ring-gray-300 p-1 object-center rounded-md"
        />
        <div className="absolute -bottom-5 -right-5">
        <FileDialog
            selectedFiles={selectedFiles}
            setSelectedFiles={(files) => {
            if (!files?.length) setData("image", null);
            setSelectedFiles(files);
            setData("image", files?.[0]?.path);
            }}
            multiple={false}
            apiUrl={route("files.store")}
            documents={documents}
        >
            <Button
            variant="outline"
            className="w-10 h-10 rounded-full shadow-md dark:bg-gray-800 dark:border-gray-600 dark:hover:bg-gray-700 dark:text-gray-300"
            >
            <div>
                <Camera className="w-5 h-5 text-gray-500 dark:text-gray-400" />
            </div>
            </Button>
        </FileDialog>
        </div>
    </div>

使用文件对话框的模型函数存储和更新方法与处理Laravel中其他字符串变量的方式相似。有关详细说明,请参阅Laravel文档

测试

composer test

更新日志

有关最近更改的更多信息,请参阅更新日志

贡献

有关详细信息,请参阅贡献指南

安全

如果您发现任何与安全相关的问题,请通过insafnilam.2000@gmail.com发送电子邮件,而不是使用问题跟踪器。

鸣谢

许可证

MIT许可证(MIT)。有关更多信息,请参阅许可证文件

Laravel包模板

本包使用Laravel包模板生成。