feat: ship public ops features and cache docker builds
Some checks failed
docker-images / build-and-push (admin, admin, termi-astro-admin, admin/Dockerfile) (push) Failing after 13s
docker-images / build-and-push (frontend, frontend, termi-astro-frontend, frontend/Dockerfile) (push) Has been cancelled
docker-images / build-and-push (backend, backend, termi-astro-backend, backend/Dockerfile) (push) Has been cancelled

This commit is contained in:
2026-04-01 13:22:19 +08:00
parent 669b79cc95
commit 497a9d713d
75 changed files with 6985 additions and 668 deletions

View File

@@ -820,6 +820,26 @@ export function PostsPage() {
const [pinnedFilter, setPinnedFilter] = useState('all')
const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState<number>(POSTS_PAGE_SIZE_OPTIONS[0])
const [sortKey, setSortKey] = useState('updated_at_desc')
const [totalPosts, setTotalPosts] = useState(0)
const [totalPages, setTotalPages] = useState(1)
const { sortBy, sortOrder } = useMemo(() => {
switch (sortKey) {
case 'created_at_asc':
return { sortBy: 'created_at', sortOrder: 'asc' }
case 'created_at_desc':
return { sortBy: 'created_at', sortOrder: 'desc' }
case 'title_asc':
return { sortBy: 'title', sortOrder: 'asc' }
case 'title_desc':
return { sortBy: 'title', sortOrder: 'desc' }
case 'updated_at_asc':
return { sortBy: 'updated_at', sortOrder: 'asc' }
default:
return { sortBy: 'updated_at', sortOrder: 'desc' }
}
}, [sortKey])
const loadPosts = useCallback(async (showToast = false) => {
try {
@@ -827,9 +847,28 @@ export function PostsPage() {
setRefreshing(true)
}
const next = await adminApi.listPosts()
const next = await adminApi.listPostsPage({
search: searchTerm.trim() || undefined,
postType: typeFilter === 'all' ? undefined : typeFilter,
pinned:
pinnedFilter === 'all'
? undefined
: pinnedFilter === 'pinned',
includePrivate: true,
includeRedirects: true,
preview: true,
page: currentPage,
pageSize,
sortBy,
sortOrder,
})
startTransition(() => {
setPosts(next)
setPosts(next.items)
setTotalPosts(next.total)
setTotalPages(next.total_pages)
if (next.page !== currentPage) {
setCurrentPage(next.page)
}
})
if (showToast) {
@@ -844,7 +883,7 @@ export function PostsPage() {
setLoading(false)
setRefreshing(false)
}
}, [])
}, [currentPage, pageSize, pinnedFilter, searchTerm, sortBy, sortOrder, typeFilter])
const loadEditor = useCallback(
async (nextSlug: string) => {
@@ -931,49 +970,17 @@ export function PostsPage() {
}
}, [createDialogOpen, metadataDialog, navigate, slug])
const normalizedSearchTerm = searchTerm.trim().toLowerCase()
const filteredPosts = useMemo(() => {
return posts.filter((post) => {
const matchesSearch =
!normalizedSearchTerm ||
[
post.title ?? '',
post.slug,
post.category ?? '',
post.description ?? '',
post.post_type ?? '',
postTagsToList(post.tags).join(' '),
]
.join('\n')
.toLowerCase()
.includes(normalizedSearchTerm)
const matchesType = typeFilter === 'all' || (post.post_type ?? 'article') === typeFilter
const pinnedValue = Boolean(post.pinned)
const matchesPinned =
pinnedFilter === 'all' ||
(pinnedFilter === 'pinned' && pinnedValue) ||
(pinnedFilter === 'regular' && !pinnedValue)
return matchesSearch && matchesType && matchesPinned
})
}, [normalizedSearchTerm, pinnedFilter, posts, typeFilter])
useEffect(() => {
setCurrentPage(1)
}, [pageSize, pinnedFilter, searchTerm, typeFilter])
}, [pageSize, pinnedFilter, searchTerm, sortKey, typeFilter])
const totalPages = Math.max(1, Math.ceil(filteredPosts.length / pageSize))
const safeCurrentPage = Math.min(currentPage, totalPages)
useEffect(() => {
setCurrentPage((current) => Math.min(current, totalPages))
}, [totalPages])
const paginatedPosts = useMemo(() => {
const startIndex = (safeCurrentPage - 1) * pageSize
return filteredPosts.slice(startIndex, startIndex + pageSize)
}, [filteredPosts, pageSize, safeCurrentPage])
const paginatedPosts = posts
const paginationItems = useMemo(() => {
const maxVisiblePages = 5
@@ -988,8 +995,8 @@ export function PostsPage() {
return Array.from({ length: endPage - startPage + 1 }, (_, index) => startPage + index)
}, [safeCurrentPage, totalPages])
const pageStart = filteredPosts.length ? (safeCurrentPage - 1) * pageSize + 1 : 0
const pageEnd = filteredPosts.length ? Math.min(safeCurrentPage * pageSize, filteredPosts.length) : 0
const pageStart = totalPosts ? (safeCurrentPage - 1) * pageSize + 1 : 0
const pageEnd = totalPosts ? Math.min(safeCurrentPage * pageSize, totalPosts) : 0
const pinnedPostCount = useMemo(
() => posts.filter((post) => Boolean(post.pinned)).length,
[posts],
@@ -1904,7 +1911,7 @@ export function PostsPage() {
</CardDescription>
</div>
<Badge variant="outline">{filteredPosts.length} / {posts.length}</Badge>
<Badge variant="outline">{paginatedPosts.length} / {totalPosts}</Badge>
</div>
<div className="grid gap-3">
<div className="flex flex-col gap-3 lg:flex-row">
@@ -1921,7 +1928,7 @@ export function PostsPage() {
</Button>
) : null}
</div>
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
<Select value={typeFilter} onChange={(event) => setTypeFilter(event.target.value)}>
<option value="all"></option>
<option value="article"></option>
@@ -1947,11 +1954,18 @@ export function PostsPage() {
</option>
))}
</Select>
<Select value={sortKey} onChange={(event) => setSortKey(event.target.value)}>
<option value="updated_at_desc"></option>
<option value="created_at_desc"></option>
<option value="created_at_asc"></option>
<option value="title_asc"> A Z</option>
<option value="title_desc"> Z A</option>
</Select>
</div>
</div>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary"> {filteredPosts.length}</Badge>
<Badge variant="outline"> {pinnedPostCount}</Badge>
<Badge variant="secondary"> {totalPosts}</Badge>
<Badge variant="outline"> {pinnedPostCount}</Badge>
<Badge variant="outline">
{safeCurrentPage} / {totalPages}
</Badge>
@@ -2008,18 +2022,18 @@ export function PostsPage() {
)
})}
{!filteredPosts.length ? (
{!totalPosts ? (
<div className="rounded-[1.8rem] border border-dashed border-border/80 px-5 py-12 text-center text-sm text-muted-foreground">
</div>
) : null}
</div>
{filteredPosts.length ? (
{totalPosts ? (
<div className="rounded-[1.5rem] border border-border/70 bg-background/65 px-4 py-3">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<p className="text-sm text-muted-foreground">
{pageStart} - {pageEnd} {filteredPosts.length}
{pageStart} - {pageEnd} {totalPosts}
</p>
<div className="flex flex-wrap items-center gap-2">
<Button