ponchrobles/inertiajs-tables-laravel-query-builder

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

3.1.2 2024-08-28 17:55 UTC

This package is auto-updated.

Last update: 2024-09-28 18:10:51 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 功能)

兼容性

安装

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

服务器端安装(Laravel)

您可以通过 composer 安装此包

composer require ponchrobles/inertiajs-tables-laravel-query-builder

该包将自动注册 Service Provider,它提供可在 Inertia 响应中使用的 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,
	);
});

数字范围过滤器

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

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')]);

使用 column 方法,您可以指定哪些列可以切换、排序和搜索。您必须为每个列传递至少一个键或标签。

Inertia::render('Page/Index')->table(function (InertiaTable $table) {
	$table->column('name', 'User Name');

	$table->column(
		key: 'name',
		label: 'User Name',
		canBeHidden: true,
		hidden: false,
		sortable: true,
		searchable: true
	);
});

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 @ponchrobles_/inertiajs-tables-laravel-query-builder --save

yarn add @ponchrobles_/inertiajs-tables-laravel-query-builder

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

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

表格组件

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

<script setup>
import { Table } from "@ponchrobles_/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);
});

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

<script setup>
import { Table } from "@ponchrobles_/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 "@ponchrobles_/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",
				},
			},
		},
	},
}

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

const themeVariables = {
	inertia_table: {
		 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: {
			 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: {
			 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 组件的一部分,但应该导入。
  • 组件的模板和逻辑不再分离。使用插槽来注入自己的实现。

更新日志

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

贡献

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

安全性

如果你发现任何与安全相关的问题,请通过电子邮件 alfonsorodriguez@live.com.mx 联系,而不是使用问题跟踪器。

鸣谢

许可

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