Laravel 中创建排名表单并实现数据排序
在Web应用开发中,实现一个允许用户对项目(如产品、文章或候选人)进行主观排名的系统是常见的需求。Laravel框架以其优雅的语法和强大的功能,为构建此类功能提供了坚实的基础。本文将详细介绍如何在Laravel应用中创建一个排名表单,并实现后端的数据排序逻辑。
一、 数据库准备与模型创建
首先,我们需要一个数据表来存储待排名的项目。假设我们正在构建一个“最佳电影”排名系统。
1.1 创建数据迁移
使用Artisan命令生成迁移文件:
php artisan make:migration create_movies_table
编辑生成的迁移文件,定义表结构:
<?php
use IlluminateDatabaseMigrationsMigration;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateSupportFacadesSchema;
return new class extends Migration
{
public function up(): void
{
Schema::create('movies', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('description')->nullable();
// `rank` 字段将存储用户给出的排名,初始可为null
$table->integer('rank')->nullable()->unique();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('movies');
}
};运行迁移以创建表:
php artisan migrate
1.2 创建Eloquent模型
生成并编辑Movie模型:
php artisan make:model Movie
<?php
namespace AppModels;
use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;
class Movie extends Model
{
use HasFactory;
protected $fillable = ['title', 'description', 'rank'];
/**
* 根据排名升序获取电影的作用域。
*/
public function scopeOrderByRank($query)
{
return $query->orderBy('rank');
}
/**
* 获取尚未被排名的电影的作用域。
*/
public function scopeUnranked($query)
{
return $query->whereNull('rank');
}
}二、 创建排名表单视图
表单是用户交互的界面。我们将创建一个Blade视图,允许用户为一系列电影分配排名(例如,从1到5)。
2.1 基本表单结构
创建视图文件 resources/views/rankings/create.blade.php。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rank Movies</title>
<link href="https://www.ipipp.com/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h1>Rank Your Top Movies</h1>
<p>请为以下电影分配唯一的排名(数字越小,排名越高)。</p>
@if(session('success'))
<div class="alert alert-success">
{{ session('success') }}
</div>
@endif
<form action="{{ route('rankings.store') }}" method="POST">
@csrf
<div class="list-group mb-3">
@forelse($movies as $movie)
<div class="list-group-item">
<div class="row">
<div class="col-md-8">
<h5>{{ $movie->title }}</h5>
<p class="text-muted">{{ $movie->description }}</p>
</div>
<div class="col-md-4">
<label for="rank_{{ $movie->id }}" class="form-label">排名</label>
<input type="number"
name="ranks[{{ $movie->id }}]"
id="rank_{{ $movie->id }}"
class="form-control rank-input"
min="1"
max="{{ $movies->count() }}"
value="{{ old('ranks.' . $movie->id, $movie->rank) }}"
placeholder="输入1-{{ $movies->count() }}之间的数字">
@error('ranks.' . $movie->id)
<div class="text-danger small">{{ $message }}</div>
@enderror
</div>
</div>
</div>
@empty
<div class="list-group-item">暂无电影可供排名。</div>
@endforelse
</div>
<button type="submit" class="btn btn-primary">提交排名</button>
</form>
<hr>
<h3>当前排名榜</h3>
<ul class="list-group">
@foreach($rankedMovies as $movie)
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ $movie->title }}
<span class="badge bg-primary rounded-pill">第 {{ $movie->rank }} 名</span>
</li>
@endforeach
</ul>
</div>
<script src="https://www.ipipp.com/js/bootstrap.bundle.min.js"></script>
<script>
// 简单的客户端验证:确保排名值唯一
document.querySelector('form').addEventListener('submit', function(e) {
const inputs = document.querySelectorAll('.rank-input');
const values = Array.from(inputs).map(input => input.value).filter(v => v);
const uniqueValues = new Set(values);
if (values.length !== uniqueValues.size) {
e.preventDefault();
alert('错误:排名数字不能重复!');
}
});
</script>
</body>
</html>三、 构建控制器处理逻辑
控制器负责处理表单提交的请求,验证数据,并更新数据库。
3.1 生成控制器
php artisan make:controller RankingController
3.2 编写控制器方法
编辑 app/Http/Controllers/RankingController.php。
<?php
namespace AppHttpControllers;
use AppModelsMovie;
use IlluminateHttpRequest;
use IlluminateSupportFacadesDB;
use IlluminateValidationRule;
class RankingController extends Controller
{
/**
* 显示排名表单。
*/
public function create()
{
// 获取所有电影,已排名的和未排名的分开
$movies = Movie::orderBy('rank', 'asc')->get();
$rankedMovies = $movies->whereNotNull('rank');
$unrankedMovies = $movies->whereNull('rank');
// 为了表单显示,合并它们,但未排名的在后面
$moviesForForm = $rankedMovies->merge($unrankedMovies);
return view('rankings.create', [
'movies' => $moviesForForm,
'rankedMovies' => $rankedMovies
]);
}
/**
* 处理排名表单提交。
*/
public function store(Request $request)
{
// 1. 基础验证:确保提交的数据是数组且数量正确
$validated = $request->validate([
'ranks' => 'required|array',
'ranks.*' => [
'nullable',
'integer',
'min:1',
// 自定义规则验证排名唯一性
function ($attribute, $value, $fail) use ($request) {
if ($value !== null) {
$count = collect($request->input('ranks'))
->filter(fn($rank) => $rank == $value)
->count();
if ($count > 1) {
$fail("排名 {$value} 被重复使用了。");
}
}
},
],
]);
// 2. 使用数据库事务确保数据一致性
DB::transaction(function () use ($validated) {
// 首先,重置所有电影的排名(根据业务逻辑可选)
// Movie::query()->update(['rank' => null]);
// 然后,为每个提交了排名的电影更新排名
foreach ($validated['ranks'] as $movieId => $rank) {
if (!is_null($rank)) {
Movie::where('id', $movieId)->update(['rank' => (int) $rank]);
}
}
// 3. (可选)处理排名间隙
$this->compressRanks();
});
return redirect()->route('rankings.create')
->with('success', '排名已成功更新!');
}
/**
* 压缩排名,确保排名是连续的(例如:1,2,3...而不是1,3,4)。
* 这是一个可选的高级功能。
*/
private function compressRanks()
{
$rankedMovies = Movie::whereNotNull('rank')->orderBy('rank', 'asc')->get();
$expectedRank = 1;
foreach ($rankedMovies as $movie) {
if ($movie->rank != $expectedRank) {
$movie->update(['rank' => $expectedRank]);
}
$expectedRank++;
}
}
}四、 定义路由
在 routes/web.php 文件中添加以下路由。
<?php
use AppHttpControllersRankingController;
use IlluminateSupportFacadesRoute;
Route::get('/rankings', [RankingController::class, 'create'])->name('rankings.create');
Route::post('/rankings', [RankingController::class, 'store'])->name('rankings.store');五、 实现高级排序与查询
基本的排名存储完成后,我们经常需要根据排名进行数据检索和展示。
5.1 使用查询作用域
我们已经在Movie模型中定义了 scopeOrderByRank 作用域。现在可以方便地使用它:
// 获取按排名升序排列的所有电影
$movies = Movie::orderByRank()->get();
// 获取前三名
$topThree = Movie::whereNotNull('rank')
->orderBy('rank', 'asc')
->limit(3)
->get();
// 获取尚未排名的电影
$unranked = Movie::unranked()->get();5.2 在视图中展示排序结果
创建一个新的视图来展示最终的排名榜,例如 resources/views/rankings/index.blade.php。
<h2>最终电影排名榜</h2>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">名次</th>
<th scope="col">电影名称</th>
<th scope="col">描述</th>
</tr>
</thead>
<tbody>
@foreach($movies as $movie)
<tr>
<th scope="row">{{ $movie->rank }}</th>
<td><strong>{{ $movie->title }}</strong></td>
<td>{{ $movie->description }}</td>
</tr>
@endforeach
</tbody>
</table>六、 总结与扩展
通过以上步骤,我们在Laravel中完整实现了一个具备表单提交、数据验证、唯一性检查、数据库更新和结果展示的排名系统。核心要点包括:
使用
<input type="number">元素构建排名表单。通过Laravel的表单请求验证和自定义验证规则确保排名数据的有效性(如唯一性)。
利用数据库事务(
DB::transaction)保证多条排名更新操作的原子性。在Eloquent模型中使用查询作用域来封装常用的排序逻辑。
此系统可以轻松扩展:
多用户排名:将
rank字段移至一个名为user_movie_rankings的中间表,并关联用户ID。加权排名:增加一个“权重”或“投票数”字段,最终排名根据算法(如加权平均)计算得出。
动态排名更新:使用Livewire或Alpine.js实现拖拽排序,并通过AJAX实时保存排名,提升用户体验。
排名历史:创建排名历史记录表,追踪每次排名的变化。
通过灵活运用Laravel的MVC架构和Eloquent ORM,开发者可以高效地构建出复杂且健壮的排名与排序功能。