dootix-developer/inertiajs-tables-laravel-query-builder

Inertia.js 前端组件,用于 Spatie 的 Laravel 查询构建器

v4.0.1 2024-06-05 13:25 UTC

This package is auto-updated.

Last update: 2024-09-19 08:52:28 UTC


README

Latest Version on NPM npm Latest Version on Packagist Software License

此包为 Inertia.js 提供类似 DataTables 的体验,支持搜索、过滤、排序、切换列和分页。它生成的 URL 可由 Spatie 的优秀 Laravel 查询构建器 包使用,无需额外逻辑。组件使用 Tailwind CSS 3.0 设计,但可以通过插槽进行完全自定义。数据刷新逻辑基于 Inertia 的 Ping CRM 示例

Inertia.js Table for Laravel Query Builder

功能

  • 自动填充:自动生成带有自定义单元格支持的 theadtbody
  • 全局搜索
  • 按字段搜索
  • 选择过滤器
  • 切换列
  • 排序列
  • 分页(支持 Eloquent/API 资源/Simple/Cursor)
  • 自动更新查询字符串(使用 Inertia 的 replace 功能)

兼容性

注意:目前使用 Vite 时存在 问题

安装

您需要安装服务器端包和客户端包。请注意,此包仅与 Laravel 10、Vue 3.0 兼容,并需要 Tailwind Forms 插件。

服务器端安装(Laravel)

您可以通过 composer 安装此包

composer require dootix-developer/inertiajs-tables-laravel-query-builder

该包将自动注册服务提供者,提供可在 Interia 响应中使用的 table 方法。

搜索字段

使用 searchInput 方法,您可以指定哪些属性是可搜索的。搜索查询作为 filter 传递到 URL 查询中。这无缝集成了 Laravel 查询构建器包的 过滤功能

虽然只需传递列键即可,但您可以指定自定义标签和默认值。

use ProtoneMedia\LaravelQueryBuilderInertiaJs\InertiaTable;

Inertia::render('Page/Index')->table(function (InertiaTable $table) {
    $table->searchInput('name');

    $table->searchInput(
        key: 'framework',
        label: 'Find your framework',
        defaultValue: 'Laravel'
    );
});

选择过滤器

选择过滤器类似于搜索字段,但使用 select 元素而不是 input 元素。这样,您可以向用户展示一组预定义的选项。在底层,这使用的是 Laravel 查询构建器包的相同过滤功能。

selectFilter 方法需要两个参数:键和一个包含选项的键值数组。

Inertia::render('Page/Index')->table(function (InertiaTable $table) {
    $table->selectFilter('language_code', [
        'en' => 'Engels',
        'nl' => 'Nederlands',
    ]);
});

默认情况下,selectFilter 将在数组中添加一个 无筛选 选项。您可以禁用此选项或为其指定一个自定义标签。

Inertia::render('Page/Index')->table(function (InertiaTable $table) {
    $table->selectFilter(
        key: 'language_code',
        options: $languages,
        label: 'Language',
        defaultValue: 'nl',
        noFilterOption: true,
        noFilterOptionLabel: 'All languages'
    );
});

布尔过滤器

这样,您可以向用户展示一个开关。在底层,这使用的是 Laravel 查询构建器包的相同过滤功能。

toggleFilter 方法需要一个参数:键。

Inertia::render('Page/Index')->table(function (InertiaTable $table) {
    $table->toggleFilter('is_verified');
});

您可以为此指定一个自定义标签和一个默认值。

Inertia::render('Page/Index')->table(function (InertiaTable $table) {
    $table->toggleFilter(
        key: 'is_verified',
        label: 'Is email verified',
        defaultValue: true,
    );
});

数字范围过滤器

这样,您可以向用户展示一个数字范围。

numberRangeFilter 方法需要两个参数:键和最大值。

Inertia::render('Page/Index')->table(function (InertiaTable $table) {
    $table->numberRangeFilter('invoice_recall_count', 5);
});

您可以指定一些其他参数。

Inertia::render('Page/Index')->table(function (InertiaTable $table) {
    $table->toggleFilter(
        key: 'invoice_recall_count',
        max: 5,
        min: 0,
        prefix: '',
        suffix: '',
        step: 1,
        label: 'Invoice recall count',
        defaultValue: [1,4],
    );
});

您需要为这个过滤器使用一个自定义的允许过滤器。

$users = QueryBuilder::for(/*...*/)
            ->allowedFilters([NumberRangeFilter::getQueryBuilderFilter('invoice_recall_count')]);

自定义过滤器

这样,您可以向用户展示一个自定义过滤器。

customFilter 方法需要一个参数:键。

Inertia::render('Page/Index')->table(function (InertiaTable $table) {
    $table->customFilter('date_range');
});

您可以指定一些其他参数。

Inertia::render('Page/Index')->table(function (InertiaTable $table) {
    $table->toggleFilter(
        key: 'date_range',
        label: 'Date range',
        params: ['min_date' => '2022-01-01', 'max_date' => '2022-12-31']
        defaultValue: ['start' => '2022-01-01', 'end' => '2022-12-31'],
    );
});

您需要为这个过滤器使用一个自定义的允许过滤器。

$dateRangeFilter = AllowedFilter::custom('date_range', new DateRangeFilter());
$users = QueryBuilder::for(/*...*/)
            ->allowedFilters([$dateRangeFilter]);

对于前端,您可以使用 #custom_filter(<< your key >>) 插槽来添加您的自定义过滤器。您可以访问过滤器数据(如参数)、颜色和 onFilterChange 函数。onFilterChange 函数是一个函数,当您想要更新过滤器值时需要调用。您需要传递键和新值。

<Table>
    <template #custom_filter(date_range)="{ filter: filter, color: color, onFilterChange: onFilterChange }">
        <!--        Your custom filter        -->
    </template>
</Table>

searchable 选项是 searchInput 方法的快捷键。下面的示例实际上会调用 $table->searchInput('name', 'User Name')

全局搜索

您可以使用 withGlobalSearch 方法启用全局搜索,并可选地指定一个占位符。

Inertia::render('Page/Index')->table(function (InertiaTable $table) {
    $table->withGlobalSearch();

    $table->withGlobalSearch('Search through the data...');
});

如果您想要默认为每个表格启用全局搜索,您可以使用静态 defaultGlobalSearch 方法,例如在 AppServiceProvider 类中。

InertiaTable::defaultGlobalSearch();
InertiaTable::defaultGlobalSearch('Default custom placeholder');
InertiaTable::defaultGlobalSearch(false); // disable

示例控制器

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Support\Collection;
use Inertia\Inertia;
use ProtoneMedia\LaravelQueryBuilderInertiaJs\InertiaTable;
use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\QueryBuilder;

class UserIndexController
{
    public function __invoke()
    {
        $globalSearch = AllowedFilter::callback('global', function ($query, $value) {
            $query->where(function ($query) use ($value) {
                Collection::wrap($value)->each(function ($value) use ($query) {
                    $query
                        ->orWhere('name', 'LIKE', "%{$value}%")
                        ->orWhere('email', 'LIKE', "%{$value}%");
                });
            });
        });

        $users = QueryBuilder::for(User::class)
            ->defaultSort('name')
            ->allowedSorts(['name', 'email', 'language_code'])
            ->allowedFilters(['name', 'email', 'language_code', $globalSearch])
            ->paginate()
            ->withQueryString();

        return Inertia::render('Users/Index', [
            'users' => $users,
        ])->table(function (InertiaTable $table) {
            $table
              ->withGlobalSearch()
              ->defaultSort('name')
              ->column(key: 'name', searchable: true, sortable: true, canBeHidden: false)
              ->column(key: 'email', searchable: true, sortable: true)
              ->column(key: 'language_code', label: 'Language')
              ->column(label: 'Actions')
              ->selectFilter(key: 'language_code', label: 'Language', options: [
                  'en' => 'English',
                  'nl' => 'Dutch',
              ]);
    }
}

客户端安装(Inertia)

您可以通过 npmyarn 安装此包。

npm install @dootix-developer/inertiajs-tables-laravel-query-builder --save

yarn add @dootix-developer/inertiajs-tables-laravel-query-builder

将存储库路径添加到您的 Tailwind 配置文件content 数组中。这确保了样式在生产构建中也有效。

module.exports = {
  content: [
    './node_modules/@dootix-developer/inertiajs-tables-laravel-query-builder/**/*.{js,vue}',
  ]
}

表格组件

要使用 Table 组件及其所有相关功能,您必须导入 Table 组件并将 users 数据传递给组件。

<script setup>
import { Table } from "@dootix-developer/inertiajs-tables-laravel-query-builder";

defineProps(["users"])
</script>

<template>
  <Table :resource="users" />
</template>

resource 属性会自动检测数据和额外的分页元数据。您也可以通过 datameta 属性手动将此传递给组件。

<template>
  <Table :data="users.data" :meta="users.meta" />
</template>

如果您想要手动渲染表格,例如在包的 v1 版本中,您可以使用 headbody 插槽。此外,您仍然可以使用 meta 属性来渲染分页器。

<template>
  <Table :meta="users">
    <template #head>
      <tr>
        <th>User</th>
      </tr>
    </template>

    <template #body>
      <tr
        v-for="(user, key) in users.data"
        :key="key"
      >
        <td>{{ user.name }}</td>
      </tr>
    </template>
  </Table>
</template>

Table 有一些额外的属性可以调整其前端行为。

<template>
  <Table
    :striped="true"
    :prevent-overlapping-requests="false"
    :input-debounce-ms="1000"
    :preserve-scroll="true"
  />
</template>

Table 有一些事件您可以使用

  • rowClicked:当用户点击行时,会触发此事件。事件会提供以下信息:事件、项目、键。如果您在行内使用可点击元素(如操作按钮)时使用此事件,请注意使用 event.stopPropagation()

自定义列单元格

当使用 auto-fill 时,您可能希望在保留其他列不变的情况下转换特定列的显示数据。为此,您可以使用单元格模板。以下示例来自上面的 示例控制器

<template>
  <Table :resource="users">
    <template #cell(actions)="{ item: user }">
      <a :href="`/users/${user.id}/edit`">
        Edit
      </a>
    </template>
  </Table>
</template>

自定义表头单元格

当使用 auto-fill 时,您可能希望在保留其他列不变的情况下转换特定表头的显示数据。为此,您可以使用表头模板。以下示例来自上面的 示例控制器

<template>
  <Table :resource="users">
    <template #header(email)="{ label: label, column: column }">
      <span class="lowercase">{{ label }}</span>
    </template>
  </Table>
</template>

每页多个表格

您可能希望在一页中使用多个表格组件。显示数据很简单,但使用过滤、排序和分页等功能需要稍微不同的设置。例如,默认情况下,使用 page 查询键来分页数据集,但现在您想为每个表格使用两个不同的键。幸运的是,这个包会处理这个问题,甚至还提供了一个辅助方法来支持 Spatie 的查询包。为了使它正常工作,您需要为您的表格 命名

让我们看看 Spatie 的 QueryBuilder。在这个例子中,有一个公司表格和一个用户表格。我们相应地命名这些表格。所以首先,调用静态方法 updateQueryBuilderParameters 告诉包使用不同的查询参数集。现在,filter 变为 companies_filtercolumn 变为 companies_column 等等。其次,更改数据库分页器的 pageName

InertiaTable::updateQueryBuilderParameters('companies');

$companies = QueryBuilder::for(Company::query())
    ->defaultSort('name')
    ->allowedSorts(['name', 'email'])
    ->allowedFilters(['name', 'email'])
    ->paginate(pageName: 'companiesPage')
    ->withQueryString();

InertiaTable::updateQueryBuilderParameters('users');

$users = QueryBuilder::for(User::query())
    ->defaultSort('name')
    ->allowedSorts(['name', 'email'])
    ->allowedFilters(['name', 'email'])
    ->paginate(pageName: 'usersPage')
    ->withQueryString();

然后,我们需要将这些更改应用到 InertiaTable 类中。有一个 namepageName 方法可以做到这一点。

return Inertia::render('TwoTables', [
    'companies' => $companies,
    'users'     => $users,
])->table(function (InertiaTable $inertiaTable) {
    $inertiaTable
        ->name('users')
        ->pageName('usersPage')
        ->defaultSort('name')
        ->column(key: 'name', searchable: true)
        ->column(key: 'email', searchable: true);
})->table(function (InertiaTable $inertiaTable) {
    $inertiaTable
        ->name('companies')
        ->pageName('companiesPage')
        ->defaultSort('name')
        ->column(key: 'name', searchable: true)
        ->column(key: 'address', searchable: true);
});

最后,将正确的 name 属性传递给 Vue 模板中的每个表格。可选地,您可以将 preserve-scroll 属性设置为 table-top。这确保在新数据时滚动到表格的顶部。例如,当更改第二个表格的页面时,您希望滚动到表格的顶部,而不是页面的顶部。

<script setup>
import { Table } from "@dootix-developer/inertiajs-tables-laravel-query-builder";

defineProps(["companies", "users"])
</script>

<template>
  <Table
    :resource="companies"
    name="companies"
    preserve-scroll="table-top"
  />

  <Table
    :resource="users"
    name="users"
    preserve-scroll="table-top"
  />
</template>

分页翻译

您可以使用 setTranslations 方法覆盖默认的分页翻译。您可以在主 JavaScript 文件中这样做

import { setTranslations } from "@dootix-developer/inertiajs-tables-laravel-query-builder";

setTranslations({
  next: "Next",
  no_results_found: "No results found",
  of: "of",
  per_page: "per page",
  previous: "Previous",
  results: "results",
  to: "to",
  search: "Search",
  reset: "Reset",
  grouped_reset: "Reset",
  add_search_fields: "Add search field",
  show_hide_columns: "Show / Hide columns",
});

Table.vue 插槽

Table.vue 有几个插槽,您可以使用它们来注入自己的实现。

每个插槽都提供了与父 Table 组件交互的属性。

<template>
  <Table>
    <template v-slot:tableGlobalSearch="slotProps">
      <input
        placeholder="Custom Global Search Component..."
        @input="slotProps.onChange($event.target.value)"
      />
    </template>
  </Table>
</template>

可用的定制

您可以定制表格的一些部分。

app.js 文件中提供一个包含所需定制的对象,如下所示

const themeVariables = {
    inertia_table: {
        per_page_selector: {
            select: {
                primary: 'your classes',
            },
        },
    },
}

createInertiaApp({
    progress: {
        color: '#4B5563',
    },
    title: (title) => `${title} - ${appName}`,
    resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
    setup({ el, App, props, plugin }) {
        return createApp({ render: () => h(App, props) })
            // ...
            .provide('themeVariables', themeVariables)
            // ...
            .mount(el);
    },
})

您可以通过覆盖默认样式来自定义默认样式

const themeVariables = {
    inertia_table: {
        per_page_selector: {
            select: {
                base: "block min-w-max shadow-sm text-sm rounded-md",
                color: {
                    primary: "border-gray-300 focus:ring-yellow-500 focus:border-yellow-500",
                },
            },
        },
    },
}

或者您可以创建一个新的样式,并使用 color 属性在 Table.vue 中使用

const themeVariables = {
    inertia_table: {
        per_page_selector: {
            select: {
                base: "block min-w-max shadow-sm text-sm rounded-md",
                color: {
                    red_style: 'border-gray-300 focus:ring-red-500 focus:border-red-500',
                },
            },
        },
    },
}
<template>
  <Table color="red_style" />
</template>

可用的定制

const themeVariables = {
    inertia_table: {
        button_with_dropdown: {
            button: {
                base: "w-full border rounded-md shadow-sm px-4 py-2 inline-flex justify-center text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2",
                color: {
                    primary: "bg-white text-gray-700 hover:bg-gray-50 border-gray-300 focus:ring-indigo-500",
                    dootix: "bg-white text-gray-700 hover:bg-gray-50 border-gray-300 focus:ring-cyan-500",
                },
            },
        },
        per_page_selector: {
            select: {
                base: "block min-w-max shadow-sm text-sm rounded-md",
                color: {
                    primary: "border-gray-300 focus:ring-indigo-500 focus:border-indigo-500",
                    dootix: "border-gray-300 focus:ring-cyan-500 focus:border-blue-500",
                },
            },
        },
        table_filter: {
            select_filter: {
                select: {
                    base: "block w-full shadow-sm text-sm rounded-md",
                    color: {
                        primary: "border-gray-300 focus:ring-indigo-500 focus:border-indigo-500",
                        dootix: "border-gray-300 focus:ring-cyan-500 focus:border-blue-500",
                    },
                },
            },
            togle_filter: {
                toggle: {
                    base: "w-11 h-6 rounded-full after:border after:rounded-full after:h-5 after:w-5",
                    color: {
                        primary: "after:bg-white after:border-white peer-checked:bg-indigo-500 bg-red-500",
                        dootix: "after:bg-white after:border-white peer-checked:bg-gradient-to-r peer-checked:from-cyan-500 peer-checked:to-blue-600 bg-red-500",
                        disabled: "after:bg-white after:border-white bg-gray-200",
                    }
                },
                reset_button: {
                    base: "rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2",
                    color: {
                        primary: "text-gray-400 hover:text-gray-500 focus:ring-indigo-500",
                        dootix: "text-gray-400 hover:text-gray-500 focus:ring-cyan-500",
                    },
                },
            },
            number_range_filter: {
                main_bar: {
                    base: "h-2 rounded-full",
                    color: {
                        primary: "bg-gray-200",
                        dootix: "bg-gray-200",
                    },
                },
                selected_bar: {
                    base: "h-2 rounded-full",
                    color: {
                        primary: "bg-indigo-600",
                        dootix: "bg-gradient-to-r from-cyan-500 to-blue-600",
                    },
                },
                button: {
                    base: "h-4 w-4 rounded-full shadow border",
                    color: {
                        primary: "bg-white border-gray-300",
                        dootix: "bg-white border-gray-300",
                    },
                },
                popover: {
                    base: "truncate text-xs rounded py-1 px-4",
                    color: {
                        primary: "bg-gray-600 text-white",
                        dootix: "bg-gray-600 text-white",
                    },
                },
                popover_arrow: {
                    color: {
                        primary: "text-gray-600",
                        dootix: "text-gray-600",
                    },
                },
                text: {
                    color: {
                        primary: "text-gray-700",
                        dootix: "text-gray-700",
                    },
                },
            },
        },
        global_search: {
            input: {
                base: "block w-full pl-9 text-sm rounded-md shadow-sm",
                color: {
                    primary: "focus:ring-indigo-500 focus:border-indigo-500 border-gray-300",
                    dootix: "focus:ring-cyan-500 focus:border-blue-500 border-gray-300",
                },
            },
        },
        reset_button: {
            button: {
                base: "w-full border rounded-md shadow-sm px-4 py-2 inline-flex justify-center text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2",
                color: {
                    primary: "bg-white text-gray-700 hover:bg-gray-50 border-gray-300 focus:ring-indigo-500",
                    dootix: "bg-white text-gray-700 hover:bg-gray-50 border-gray-300 focus:ring-cyan-500",
                },
            },
        },
        table_search_rows: {
            input: {
                base: "flex-1 min-w-0 block w-full px-3 py-2 rounded-none rounded-r-md text-sm",
                color: {
                    primary: "border-gray-300 focus:ring-indigo-500 focus:border-indigo-500",
                    dootix: "border-gray-300 focus:ring-cyan-500 focus:border-blue-500",
                },
            },
            remove_button: {
                base: "rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2",
                color: {
                    primary: "text-gray-400 hover:text-gray-500 focus:ring-indigo-500",
                    dootix: "text-gray-400 hover:text-gray-500 focus:ring-cyan-500",
                },
            },
        },
    },
}

测试

app 目录中可以找到一个庞大的 Laravel Dusk E2E 测试套件。在这里,您将找到一个 Laravel + Inertia 应用程序。

cd app
cp .env.example .env
composer install
npm install
npm run production
touch database/database.sqlite
php artisan migrate:fresh --seed
php artisan dusk:chrome-driver
php artisan serve
php artisan dusk

从 v1 升级

服务器端

  • 方法 addColumn 已重命名为 column
  • 方法 addFilter 已重命名为 selectFilter
  • 方法 addSearch 已重命名为 searchInput
  • 对于所有重命名的方法,请查看参数,因为其中一些已更改。
  • 方法 addColumnsaddSearchRows 已删除。
  • 全局搜索不再默认启用。

客户端

  • 混合 InteractsWithQueryBuilder 已删除,不再需要。
  • Table 组件不再需要 filterssearchcolumnson-update 属性。
  • 当使用自定义的 theadtbody 插槽时,您需要手动提供 样式
  • 当使用自定义的 thead 时,方法 showColumn 已重命名为 show
  • 方法 setTranslations 不再是 Pagination 组件的一部分,但应该导入。
  • 组件的模板和逻辑不再分离。使用插槽来注入您的实现。

v2.1 路线图

  • 日期过滤器
  • 日期范围过滤器
  • 将演示应用程序切换到 Vite

变更日志

请参阅变更日志以获取更多关于最近更改的信息。

贡献

请参阅贡献指南以获取详细信息。

安全

如果您发现任何与安全相关的问题,请通过电子邮件联系pascal@protone.media,而不是使用问题跟踪器。

鸣谢

许可协议

MIT许可协议(MIT)。请参阅许可文件以获取更多信息。