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
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user