adesin-fr/inertiajs-tables-laravel-query-builder

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

3.1.1 2024-09-24 06:54 UTC

This package is auto-updated.

Last update: 2024-09-24 07:06:45 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 示例

由于某些质量问题,我不满意 [PonchRobles/inertiajs-tables-laravel-query-builder],因此将其分支出来。

Inertia.js Table for Laravel Query Builder

功能

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

兼容性

安装

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

服务器端安装(Laravel)

您可以通过 Composer 安装此包

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

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

搜索字段

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

尽管传递列键就足够了,但您也可以指定自定义标签和默认值。

use AdesinFr\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', '用户名')

全局搜索

您可以使用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 AdesinFr\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 AdesinFr/inertiajs-tables-laravel-query-builder --save

yarn add AdesinFr/inertiajs-tables-laravel-query-builder

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

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

表格组件

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

<script setup>
import { Table } from "AdesinFr/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()

自定义列单元格

当使用自动填充时,您可能想要在保持其他列不受影响的情况下转换特定列显示的数据。为此,您可以使用单元格模板。此示例取自上面的示例控制器

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

自定义表头单元格

当使用自动填充时,您可能想要在保持其他列不受影响的情况下转换特定表头显示的数据。为此,您可以使用表头模板。此示例取自上面的示例控制器

<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 "AdesinFr/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 "AdesinFr/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有几个插槽,您可以使用它们来注入自己的实现。

每个插槽都提供了props来与父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 prop

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端到端测试套件。在这里您将找到一个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)。有关更多信息,请参阅许可证文件