feat: add article management (list, edit, delete)

- Add admin article list page at /admin/articles with status badges,
  pin marker, publish time, and edit/delete actions
- Share the create/edit form: dynamic action URL, prefilled fields,
  is_top checkbox state, and create-vs-edit heading
- Add ArticleUpdate/ArticleDelete handlers (soft-delete via DeletedAt);
  stamp PublishedAt on first publish
- Add "Manage Articles" entry on the dashboard
- Hide home page sign-in/dashboard buttons
- Add EN/ZH i18n keys for the management UI

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 20:13:39 +08:00
co-authored by Claude Fable 5
parent 42516257e0
commit b474ee009a
7 changed files with 329 additions and 89 deletions
+3 -3
View File
@@ -2,7 +2,7 @@
{{template "header" .}}
<section class="max-w-3xl mx-auto px-4 py-10">
<h2 class="text-2xl font-bold text-gray-900 mb-8">{{index .Tr "article_create_title"}}</h2>
<h2 class="text-2xl font-bold text-gray-900 mb-8">{{.FormTitleText}}</h2>
{{if .Error}}
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6 text-sm">
@@ -10,7 +10,7 @@
</div>
{{end}}
<form action="/admin/articles/new" method="post" class="space-y-6">
<form action="{{.FormAction}}" method="post" class="space-y-6">
<!-- Title -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{index .Tr "article_title"}}</label>
@@ -52,7 +52,7 @@
<!-- IsTop -->
<div class="flex items-center gap-2">
<input type="checkbox" name="is_top" value="1" id="isTopCheckbox"
<input type="checkbox" name="is_top" value="1" id="isTopCheckbox" {{if .FormIsTop}}checked{{end}}
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
<label for="isTopCheckbox" class="text-sm font-medium text-gray-700">{{index .Tr "article_is_top"}}</label>
</div>
+60
View File
@@ -0,0 +1,60 @@
{{define "article_list"}}
{{template "header" .}}
<section class="max-w-5xl mx-auto px-4 py-12">
<div class="flex items-center justify-between mb-8">
<h2 class="text-3xl font-bold text-gray-900">{{index .Tr "article_list_title"}}</h2>
<a href="/admin/articles/new"
class="inline-flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg font-medium hover:bg-blue-700 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
{{index .Tr "dash_new_article"}}
</a>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<table class="w-full text-left">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th class="px-4 py-3 text-sm font-semibold text-gray-600">{{index .Tr "article_col_title"}}</th>
<th class="px-4 py-3 text-sm font-semibold text-gray-600">{{index .Tr "article_col_status"}}</th>
<th class="px-4 py-3 text-sm font-semibold text-gray-600">{{index .Tr "article_col_published"}}</th>
<th class="px-4 py-3 text-sm font-semibold text-gray-600 text-right">{{index .Tr "article_col_actions"}}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{{range .Articles}}
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 text-sm text-gray-800">
{{.Title}}
{{if .IsTop}}<span class="ml-1 text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded">{{index $.Tr "article_is_top"}}</span>{{end}}
</td>
<td class="px-4 py-3 text-sm">
{{if eq .Status 1}}<span class="text-xs bg-green-100 text-green-700 px-2 py-1 rounded">{{index $.Tr "article_published"}}</span>
{{else if eq .Status 2}}<span class="text-xs bg-gray-200 text-gray-600 px-2 py-1 rounded">{{index $.Tr "article_archived"}}</span>
{{else}}<span class="text-xs bg-yellow-100 text-yellow-700 px-2 py-1 rounded">{{index $.Tr "article_draft"}}</span>{{end}}
</td>
<td class="px-4 py-3 text-sm text-gray-500">
{{if .PublishedAt}}{{.PublishedAt.Format "2006-01-02 15:04"}}{{else}}—{{end}}
</td>
<td class="px-4 py-3 text-sm text-right whitespace-nowrap">
<a href="/admin/articles/{{.ID}}/edit"
class="text-blue-600 hover:text-blue-800 font-medium mr-3">{{index $.Tr "article_edit"}}</a>
<form action="/admin/articles/{{.ID}}/delete" method="post" class="inline"
onsubmit="return confirm('{{index $.Tr "article_delete_confirm"}}');">
<button type="submit"
class="text-red-600 hover:text-red-800 font-medium cursor-pointer bg-transparent border-none">{{index $.Tr "article_delete"}}</button>
</form>
</td>
</tr>
{{else}}
<tr>
<td colspan="4" class="px-4 py-12 text-center text-gray-500">{{index .Tr "home_no_posts"}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</section>
{{template "footer" .}}
{{end}}
+13 -7
View File
@@ -38,13 +38,19 @@
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-8 text-center">
<h3 class="text-xl font-semibold text-gray-800 mb-2">{{index .Tr "dash_mgmt_title"}}</h3>
<p class="text-gray-500 mb-6">{{index .Tr "dash_mgmt_desc"}}</p>
<a href="/admin/articles/new"
class="inline-flex items-center gap-2 bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
{{index .Tr "dash_new_article"}}
</a>
<div class="flex justify-center gap-3 flex-wrap">
<a href="/admin/articles/new"
class="inline-flex items-center gap-2 bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
{{index .Tr "dash_new_article"}}
</a>
<a href="/admin/articles"
class="inline-flex items-center gap-2 bg-gray-200 text-gray-700 px-6 py-3 rounded-lg font-medium hover:bg-gray-300 transition-colors">
{{index .Tr "article_manage"}}
</a>
</div>
</div>
</section>
{{template "footer" .}}
+2 -2
View File
@@ -7,14 +7,14 @@
<p class="text-xl text-gray-600 mb-8 max-w-2xl mx-auto">
{{index .Tr "home_subtitle"}}
</p>
<div class="flex justify-center gap-4">
<!-- <div class="flex justify-center gap-4">
<a href="/login" class="inline-block bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors">
{{index .Tr "home_sign_in"}}
</a>
<a href="/admin" class="inline-block bg-gray-200 text-gray-700 px-6 py-3 rounded-lg font-medium hover:bg-gray-300 transition-colors">
{{index .Tr "home_to_dash"}}
</a>
</div>
</div> -->
</section>
<section class="max-w-5xl mx-auto px-4 py-16">