Integrate article and page management into the Laravel admin dashboard
to serve as a headless CMS for the Next.js frontend (usher-site).
Backend:
- 7 migrations: article_categories, article_tags, articles, pivots, attachments, pages
- 5 models with relationships: Article, ArticleCategory, ArticleTag, ArticleAttachment, Page
- 4 admin controllers: articles (with publish/archive/pin), categories, tags, pages
- Admin views with EasyMDE markdown editor, multi-select categories/tags
- Navigation section "官網管理" in admin sidebar
API (v1):
- GET /api/v1/articles (filtered by type, category, tag, search; paginated)
- GET /api/v1/articles/{slug} (with related articles)
- GET /api/v1/categories
- GET /api/v1/pages/{slug} (with children)
- GET /api/v1/homepage (aggregated homepage data)
- Attachment download endpoint
- CORS configured for usher.org.tw, vercel.app, localhost:3000
Content migration:
- ImportHugoContent command: imports Hugo markdown files as articles/pages
- Successfully imported 27 articles, 17 categories, 11 tags, 9 pages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
185 lines
14 KiB
PHP
185 lines
14 KiB
PHP
<x-app-layout>
|
|
<x-slot name="header">
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
|
官網文章管理
|
|
</h2>
|
|
<a href="{{ route('admin.articles.create') }}" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600">
|
|
+ 建立文章
|
|
</a>
|
|
</div>
|
|
</x-slot>
|
|
|
|
<div class="py-12">
|
|
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-6">
|
|
@if (session('status'))
|
|
<div class="rounded-md bg-green-50 dark:bg-green-900/50 p-4">
|
|
<p class="text-sm font-medium text-green-800 dark:text-green-200">{{ session('status') }}</p>
|
|
</div>
|
|
@endif
|
|
|
|
@if (session('error'))
|
|
<div class="rounded-md bg-red-50 dark:bg-red-900/50 p-4">
|
|
<p class="text-sm font-medium text-red-800 dark:text-red-200">{{ session('error') }}</p>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Statistics -->
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-5">
|
|
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
|
|
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">總計</div>
|
|
<div class="mt-2 text-3xl font-bold text-gray-900 dark:text-gray-100">{{ $stats['total'] }}</div>
|
|
</div>
|
|
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
|
|
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">草稿</div>
|
|
<div class="mt-2 text-3xl font-bold text-gray-600 dark:text-gray-400">{{ $stats['draft'] }}</div>
|
|
</div>
|
|
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
|
|
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">已發布</div>
|
|
<div class="mt-2 text-3xl font-bold text-green-600 dark:text-green-400">{{ $stats['published'] }}</div>
|
|
</div>
|
|
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
|
|
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">已歸檔</div>
|
|
<div class="mt-2 text-3xl font-bold text-yellow-600 dark:text-yellow-400">{{ $stats['archived'] }}</div>
|
|
</div>
|
|
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
|
|
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">置頂中</div>
|
|
<div class="mt-2 text-3xl font-bold text-blue-600 dark:text-blue-400">{{ $stats['pinned'] }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search and Filter -->
|
|
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
|
|
<form method="GET" action="{{ route('admin.articles.index') }}" class="space-y-4">
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
|
|
<div>
|
|
<label for="search" class="block text-sm font-medium text-gray-700 dark:text-gray-300">搜尋</label>
|
|
<input type="text" name="search" id="search" value="{{ request('search') }}" placeholder="標題、內容..."
|
|
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
|
|
</div>
|
|
<div>
|
|
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-300">狀態</label>
|
|
<select name="status" id="status" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
|
|
<option value="">全部</option>
|
|
<option value="draft" {{ request('status') === 'draft' ? 'selected' : '' }}>草稿</option>
|
|
<option value="published" {{ request('status') === 'published' ? 'selected' : '' }}>已發布</option>
|
|
<option value="archived" {{ request('status') === 'archived' ? 'selected' : '' }}>已歸檔</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label for="content_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300">類型</label>
|
|
<select name="content_type" id="content_type" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
|
|
<option value="">全部</option>
|
|
<option value="blog" {{ request('content_type') === 'blog' ? 'selected' : '' }}>部落格</option>
|
|
<option value="notice" {{ request('content_type') === 'notice' ? 'selected' : '' }}>公告</option>
|
|
<option value="document" {{ request('content_type') === 'document' ? 'selected' : '' }}>文件</option>
|
|
<option value="related_news" {{ request('content_type') === 'related_news' ? 'selected' : '' }}>相關新聞</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label for="category" class="block text-sm font-medium text-gray-700 dark:text-gray-300">分類</label>
|
|
<select name="category" id="category" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
|
|
<option value="">全部</option>
|
|
@foreach($categories as $category)
|
|
<option value="{{ $category->id }}" {{ request('category') == $category->id ? 'selected' : '' }}>{{ $category->name }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-end space-x-2">
|
|
<a href="{{ route('admin.articles.index') }}" class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
|
|
清除
|
|
</a>
|
|
<button type="submit" class="rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600">
|
|
搜尋
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="flex justify-between items-center">
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">共 {{ $articles->total() }} 篇文章</p>
|
|
</div>
|
|
|
|
<!-- Articles Table -->
|
|
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg">
|
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
<thead class="bg-gray-50 dark:bg-gray-700">
|
|
<tr>
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">文章</th>
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">類型</th>
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">狀態</th>
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">建立者</th>
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">瀏覽</th>
|
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">建立時間</th>
|
|
<th scope="col" class="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
@forelse($articles as $article)
|
|
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
|
|
<td class="px-6 py-4">
|
|
<div class="flex items-center">
|
|
@if($article->is_pinned)
|
|
<span class="mr-2 text-blue-500" title="置頂文章">📌</span>
|
|
@endif
|
|
<div>
|
|
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
<a href="{{ route('admin.articles.show', $article) }}" class="hover:text-indigo-600 dark:hover:text-indigo-400">
|
|
{{ $article->title }}
|
|
</a>
|
|
</div>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">
|
|
{{ Str::limit(strip_tags($article->content), 60) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
|
{{ $article->getContentTypeLabel() }}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span class="inline-flex rounded-full px-2 text-xs font-semibold leading-5
|
|
@if($article->status === 'draft') bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300
|
|
@elseif($article->status === 'published') bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300
|
|
@else bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-300
|
|
@endif">
|
|
{{ $article->getStatusLabel() }}
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
|
{{ $article->creator->name ?? 'N/A' }}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
|
{{ $article->view_count }}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
|
{{ $article->created_at->format('Y-m-d H:i') }}
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
|
|
<a href="{{ route('admin.articles.show', $article) }}" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300">查看</a>
|
|
@if($article->canBeEditedBy(auth()->user()))
|
|
<a href="{{ route('admin.articles.edit', $article) }}" class="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-300">編輯</a>
|
|
@endif
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="7" class="px-6 py-12 text-center text-sm text-gray-500 dark:text-gray-400">
|
|
沒有找到文章。<a href="{{ route('admin.articles.create') }}" class="text-indigo-600 dark:text-indigo-400 hover:underline">建立第一篇文章</a>
|
|
</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
@if($articles->hasPages())
|
|
<div class="bg-white dark:bg-gray-800 px-4 py-3 border-t border-gray-200 dark:border-gray-700 sm:px-6">
|
|
{{ $articles->links() }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</x-app-layout>
|