当前位置: 首页 > news >正文

博客项目 laravel vue mysql 第六章 文章功能

# 前言

前面章节没看过的朋友请先从第一章开始看 。这章主要写文章相关功能。这里很多功能以后还要扩展一下。我这里只实现了部分功能,大家可以先看看。

后端

创建文章迁移文件

php artisan make:migration create_articles_table

创建文章和分类关联迁移文件

php artisan make:migration create_article_tag_table

编辑文章迁移文件

Schema::create('articles', function (Blueprint $table) {$table->id(); // 主键,自增ID$table->string('title'); // 文章标题$table->string('slug')->unique(); // 文章别名,URL 友好,如"laravel-tutorial"$table->longText('content'); // 文章内容,存储 Markdown 或 HTML$table->text('summary')->nullable(); // 文章摘要,150-300 字,列表展示用$table->string('cover', 500)->nullable(); // 封面图片 URL,如"storage/covers/xxx.jpg"$table->foreignId('user_id')->nullable()->constrained()->onDelete('set null'); // 外键,关联用户表$table->foreignId('category_id')->nullable()->constrained()->onDelete('set null'); // 外键,关联分类表$table->enum('status', ['draft', 'published'])->default('draft'); // 文章状态:草稿或已发布$table->unsignedInteger('view_count')->default(0); // 阅读量统计$table->unsignedInteger('like_count')->default(0); // 点赞数统计$table->boolean('is_top')->default(false); // 是否置顶$table->string('seo_title')->nullable(); // SEO标题$table->text('seo_description')->nullable(); // SEO描述$table->text('seo_keywords')->nullable(); // SEO关键词$table->timestamp('published_at')->nullable(); // 发布时间,发布时设置$table->timestamps(); // 创建时间和更新时间$table->index('status'); // 索引,优化按状态查询$table->index('published_at'); // 索引,优化按发布时间排序$table->index('is_top'); // 索引,优化置顶查询
});

编辑关联表迁移文件

Schema::create('article_tag', function (Blueprint $table) {$table->id(); // 主键,自增ID$table->foreignId('article_id')->constrained()->onDelete('cascade'); // 外键,关联文章表$table->foreignId('tag_id')->constrained()->onDelete('cascade'); // 外键,关联标签表$table->timestamps(); // 创建时间和更新时间$table->unique(['article_id', 'tag_id']); // 联合唯一索引,防止重复关联
});

执行迁移命令

php artisan migrate

创建模型

php artisan make:model Article

编辑模型

protected $fillable = ['title', 'slug', 'content', 'summary', 'cover', 'user_id', 'category_id', 'status', 'view_count', 'like_count', 'is_top', 'seo_title', 'seo_description', 'seo_keywords', 'published_at'
];

创建控制器

php artisan make:controller ArticleController

编辑控制器

<?phpnamespace App\Http\Controllers;use Illuminate\Http\Request;
use App\Models\Article;
use App\Models\Category;class ArticleController extends Controller
{/*** 文章列表* @param \Illuminate\Http\Request $request* @return mixed|\Illuminate\Http\JsonResponse*/public function index(Request $request){// 验证请求参数$validated = $request->validate(['per_page' => 'integer|min:1|max:50','page' => 'integer|min:1','category' => 'nullable|string|max:50','tags' => 'nullable|string', // 接受逗号分隔的字符串'keyword' => 'nullable|string|max:100','status' => 'nullable|in:draft,published','is_top' => 'nullable|boolean',], ['per_page.integer' => '每页数量必须为整数','per_page.min' => '每页数量至少为1','per_page.max' => '每页数量最多为50','page.integer' => '页码必须为整数','page.min' => '页码至少为1','category.max' => '分类名称或别名不能超过50个字符','keyword.max' => '搜索关键词不能超过100个字符','status.in' => '状态必须为草稿或已发布','is_top.boolean' => '置顶状态必须为布尔值',]);// 获取分页参数$perPage = $validated['per_page'] ?? 10;$page = $validated['page'] ?? 1;// 构建查询$query = Article::with(['category:id,name,slug', 'tags:id,name,slug'])->select(['id', 'title', 'slug', 'summary', 'cover', 'user_id', 'category_id','status', 'view_count', 'like_count', 'is_top', 'seo_title','seo_description', 'seo_keywords', 'published_at', 'created_at'])->orderBy('is_top', 'desc')->orderBy('created_at', 'desc');// 分类筛选if ($category = $validated['category'] ?? null) {$categoryId = Category::where('slug', $category)->orWhere('id', $category)->value('id');if (!$categoryId) {return response()->json(['data' => [],'meta' => ['current_page' => 1,'per_page' => $perPage,'total' => 0,'last_page' => 1,],'links' => ['prev' => null,'next' => null,],'message' => '无效的分类',], 200);}$query->where('category_id', $categoryId);}// 标签筛选if ($tags = $validated['tags'] ?? null) {$tags = array_filter(explode(',', $tags));if ($tags) {$tagIds = Tag::whereIn('slug', $tags)->pluck('id')->toArray();if (empty($tagIds)) {return response()->json(['data' => [],'meta' => ['current_page' => 1,'per_page' => $perPage,'total' => 0,'last_page' => 1,],'links' => ['prev' => null,'next' => null,],'message' => '未找到匹配的标签',], 200);}$query->whereHas('tags', fn($q) => $q->whereIn('tags.id', $tagIds));}}// 关键词搜索if ($keyword = $validated['keyword'] ?? null) {$keyword = trim($keyword);if ($keyword !== '') {$query->where(function ($q) use ($keyword) {$q->where('title', 'like', "%{$keyword}%")->orWhere('summary', 'like', "%{$keyword}%")->orWhere('content', 'like', "%{$keyword}%");});}}// 状态筛选if ($status = $validated['status'] ?? null) {$query->where('status', $status);}// 置顶筛选if (isset($validated['is_top'])) {$query->where('is_top', $validated['is_top']);}// 执行分页查询$articles = $query->paginate($perPage, ['*'], 'page', $page);// 返回jsonreturn response()->json(['data' => $articles->items(),'meta' => ['current_page' => $articles->currentPage(),'per_page' => $articles->perPage(),'total' => $articles->total(),'last_page' => $articles->lastPage(),],'links' => ['prev' => $articles->previousPageUrl(),'next' => $articles->nextPageUrl(),],'message' => $articles->isEmpty() ? '暂无文章' : '获取文章列表成功',], 200);}/*** 显示详细* @param \App\Models\Article $article* @return mixed|\Illuminate\Http\JsonResponse*/public function show(Article $article){// 预加载分类和标签$article = Article::with(['category:id,name,slug', 'tags:id,name,slug'])->select(['id', 'title', 'slug', 'content', 'summary', 'cover','user_id', 'category_id', 'view_count', 'like_count', 'is_top','seo_title', 'seo_description', 'seo_keywords', 'published_at','status', 'created_at'])->findOrFail($article->id);// 添加 cover_url 字段$article->cover_url = $article->cover ? asset('storage/' . $article->cover) : null;// 返回 JSONreturn response()->json(['data' => $article,'message' => '获取文章成功'], 200);}/*** 创建新文章*/public function store(Request $request){// 验证请求数据$validated = $request->validate(['title' => 'required|string|max:255','content' => 'required|string','summary' => 'nullable|string|max:500','cover' => 'nullable|string|max:500','category_id' => 'required|exists:categories,id','tags' => 'nullable|array','tags.*' => 'exists:tags,id','status' => 'nullable|in:draft,published','is_top' => 'nullable|boolean','seo_title' => 'nullable|string|max:255','seo_description' => 'nullable|string|max:500','seo_keywords' => 'nullable|string|max:500',], ['title.required' => '文章标题不能为空','title.max' => '文章标题不能超过255个字符','content.required' => '文章内容不能为空','summary.max' => '摘要不能超过500个字符','cover.max' => '封面路径不能超过500个字符','category_id.required' => '分类ID不能为空','category_id.exists' => '分类不存在','tags.*.exists' => '标签ID无效','status.in' => '状态必须为草稿或已发布','is_top.boolean' => '置顶状态必须为布尔值','seo_title.max' => 'SEO标题不能超过255个字符','seo_description.max' => 'SEO描述不能超过500个字符','seo_keywords.max' => 'SEO关键词不能超过500个字符',]);try {// 创建文章$article = Article::create(['title' => $validated['title'],'slug' => $this->generateUniqueSlug($validated['title']),'content' => $validated['content'],'summary' => $validated['summary'] ?? null,'cover' => $validated['cover'] ?? null,'category_id' => $validated['category_id'],'user_id' => auth('sanctum')->id(),'status' => $validated['status'] ?? 'draft','is_top' => $validated['is_top'] ?? false,'seo_title' => $validated['seo_title'] ?? null,'seo_description' => $validated['seo_description'] ?? null,'seo_keywords' => $validated['seo_keywords'] ?? null,'published_at' => ($validated['status'] ?? 'draft') === 'published' ? now() : null,]);// 关联标签if (!empty($validated['tags'])) {$article->tags()->sync($validated['tags']);}// 预加载关联数据$article->load(['category:id,name,slug', 'tags:id,name,slug']);return response()->json(['data' => ['id' => $article->id,'title' => $article->title,'slug' => $article->slug,'content' => $article->content,'summary' => $article->summary,'cover' => $article->cover,'cover_url' => $article->cover ? asset('storage/' . $article->cover) : null,'category_id' => $article->category_id,'user_id' => $article->user_id,'status' => $article->status,'is_top' => $article->is_top,'seo_title' => $article->seo_title,'seo_description' => $article->seo_description,'seo_keywords' => $article->seo_keywords,'published_at' => $article->published_at,'category' => $article->category,'tags' => $article->tags,],'message' => '创建文章成功'], 200);} catch (\Exception $e) {return response()->json(['message' => '创建文章失败: ' . $e->getMessage()], 500);}}/*** 更新文章*/public function update(Request $request, Article $article){// 验证用户权限if ($article->user_id !== auth('sanctum')->id()) {return response()->json(['message' => '无权更新此文章'], 403);}// 验证请求数据$validated = $request->validate(['title' => 'required|string|max:255','content' => 'required|string','summary' => 'nullable|string|max:500','cover' => 'nullable|string|max:500','category_id' => 'required|exists:categories,id','tags' => 'nullable|array','tags.*' => 'exists:tags,id','status' => 'nullable|in:draft,published','is_top' => 'nullable|boolean','seo_title' => 'nullable|string|max:255','seo_description' => 'nullable|string|max:500','seo_keywords' => 'nullable|string|max:500',], ['title.required' => '文章标题不能为空','title.max' => '文章标题不能超过255个字符','content.required' => '文章内容不能为空','summary.max' => '摘要不能超过500个字符','cover.max' => '封面路径不能超过500个字符','category_id.required' => '分类ID不能为空','category_id.exists' => '分类不存在','tags.*.exists' => '标签ID无效','status.in' => '状态必须为草稿或已发布','is_top.boolean' => '置顶状态必须为布尔值','seo_title.max' => 'SEO标题不能超过255个字符','seo_description.max' => 'SEO描述不能超过500个字符','seo_keywords.max' => 'SEO关键词不能超过500个字符',]);try {// 更新文章$updateData = ['title' => $validated['title'],'slug' => $this->generateUniqueSlug($validated['title'], $article->id),'content' => $validated['content'],'summary' => $validated['summary'] ?? null,'cover' => $validated['cover'] ?? null,'category_id' => $validated['category_id'],'status' => $validated['status'] ?? $article->status,'is_top' => $validated['is_top'] ?? $article->is_top,'seo_title' => $validated['seo_title'] ?? null,'seo_description' => $validated['seo_description'] ?? null,'seo_keywords' => $validated['seo_keywords'] ?? null,'published_at' => $article->status === 'draft' && ($validated['status'] ?? $article->status) === 'published' ? now() : $article->published_at,];$article->update($updateData);// 关联标签if (isset($validated['tags'])) {$article->tags()->sync($validated['tags']);}// 预加载关联数据$article->load(['category:id,name,slug', 'tags:id,name,slug']);return response()->json(['data' => ['id' => $article->id,'title' => $article->title,'slug' => $article->slug,'content' => $article->content,'summary' => $article->summary,'cover' => $article->cover,'cover_url' => $article->cover ? asset('storage/' . $article->cover) : null,'category_id' => $article->category_id,'user_id' => $article->user_id,'status' => $article->status,'is_top' => $article->is_top,'seo_title' => $article->seo_title,'seo_description' => $article->seo_description,'seo_keywords' => $article->seo_keywords,'published_at' => $article->published_at,'category' => $article->category,'tags' => $article->tags,],'message' => '更新文章成功'], 200);} catch (\Exception $e) {return response()->json(['message' => '更新文章失败: ' . $e->getMessage()], 500);}}/*** 删除文章*/public function destroy(Article $article){// 验证用户权限if ($article->user_id !== auth('sanctum')->id()) {return response()->json(['message' => '无权删除此文章'], 403);}try {// 删除关联标签$article->tags()->detach();$article->delete();return response()->json(['message' => '删除文章成功'], 200);} catch (\Exception $e) {return response()->json(['message' => '删除文章失败: ' . $e->getMessage()], 500);}}/*** 批量删除文章*/public function destroyBatch(Request $request){// 验证请求数据$validated = $request->validate(['ids' => 'required|array|min:1','ids.*' => 'integer|exists:articles,id',], ['ids.required' => '文章ID列表不能为空','ids.array' => '文章ID列表必须为数组','ids.min' => '文章ID列表不能为空','ids.*.integer' => '文章ID必须为整数','ids.*.exists' => '文章ID不存在',]);try {// 查询用户拥有的文章$articles = Article::whereIn('id', $validated['ids'])->where('user_id', auth('sanctum')->id())->get();if ($articles->isEmpty()) {return response()->json(['message' => '无权删除或文章不存在'], 403);}// 批量删除$deletedCount = 0;foreach ($articles as $article) {$article->tags()->detach();$article->delete();$deletedCount++;}return response()->json(['data' => ['deleted_count' => $deletedCount],'message' => '批量删除文章成功'], 200);} catch (\Exception $e) {return response()->json(['message' => '批量删除文章失败: ' . $e->getMessage()], 500);}}/*** 生成唯一 slug** @param string $title* @param int|null $excludeId* @return string*/protected function generateUniqueSlug(string $title, $excludeId = null){$slug = Str::slug($title);$originalSlug = $slug;$count = 1;while (Article::where('slug', $slug)->where('id', '!=', $excludeId)->exists()) {$slug = $originalSlug . '-' . $count++;}return $slug;}
}

前端

http://www.lryc.cn/news/588943.html

相关文章:

  • WPF中的ListBox详解
  • QTableView鼠标双击先触发单击信号
  • 3. ArrayList与LinkedList的区别
  • Redis的下载安装+基础操作+redis客户端的安装
  • Java :List,LinkedList,ArrayList
  • 23种设计模式--#1工厂模式
  • CodeRush AI 助手进驻 Visual Studio:AiGen/AiFind 亮相(一)
  • AI Agent 开发
  • 【Qt】 设计模式
  • SQLite技术架构解析,适用场景有哪些?
  • 设计模式之对象池模式
  • 深入理解设计模式:组合模式(Composite Pattern)
  • kotlin的自学笔记1
  • python deptry触发镜像构建失败
  • 20250715使用荣品RD-RK3588开发板在Android13下接入USB3.0接口的红外相机
  • 前端Vue.js面试题(4)
  • OSPFv3中LSA参数
  • Web3.0 学习方案
  • 前端开发数据缓存方案详解
  • 医疗资质OCR智能审核:让合规管理更高效、更精准
  • 2025-07-15通过边缘线检测图像里的主体有没有出血
  • 【Docker基础】Dockerfile构建与运行流程完全指南:从原理到实践优化
  • Spring MVC2
  • 操作系统——进程
  • 前端-CSS-day4
  • CSS 高阶使用指南
  • Python 函数:从“是什么”到“怎么用”的完整指南
  • QT 中各种坑
  • 【Qt】QWidget核心属性
  • Django基础(二)———URL与映射