feat(cache): add PostListItem DTO and use it in list/tag/search caches
This commit is contained in:
parent
31d5a99d2a
commit
170c021b37
@ -6,7 +6,7 @@
|
||||
#[cfg(feature = "server")]
|
||||
use crate::api::error::AppError;
|
||||
#[cfg(feature = "server")]
|
||||
use crate::models::post::{Post, PostStatus};
|
||||
use crate::models::post::{Post, PostListItem, PostStatus};
|
||||
#[cfg(feature = "server")]
|
||||
use crate::utils::text::count_words;
|
||||
|
||||
@ -61,6 +61,46 @@ pub(super) async fn row_to_post_list(
|
||||
}
|
||||
}
|
||||
|
||||
/// 将数据库行转换为轻量列表项 DTO。
|
||||
///
|
||||
/// 不包含 `content_md`/`content_html`,但会临时读取 `content_md` 以计算字数与阅读时长。
|
||||
/// 同步函数,不依赖数据库连接。
|
||||
#[cfg(feature = "server")]
|
||||
pub(super) fn row_to_post_list_item(row: &tokio_postgres::Row) -> PostListItem {
|
||||
let id: i32 = row.get("id");
|
||||
let status_str: String = row.get("status");
|
||||
let status = PostStatus::from_str(&status_str).unwrap_or(PostStatus::Draft);
|
||||
|
||||
// 聚合标签并过滤空字符串。
|
||||
let tags: Vec<String> = row
|
||||
.try_get::<_, Vec<String>>("tags")
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|t| !t.is_empty())
|
||||
.collect();
|
||||
|
||||
// 临时读取 content_md 计算字数与阅读时长,不存入 DTO。
|
||||
let content_md: String = row.get("content_md");
|
||||
let word_count = count_words(&content_md);
|
||||
|
||||
PostListItem {
|
||||
id,
|
||||
author_id: row.get("author_id"),
|
||||
title: row.get("title"),
|
||||
slug: row.get("slug"),
|
||||
summary: row.get("summary"),
|
||||
status,
|
||||
published_at: row.get("published_at"),
|
||||
created_at: row.get("created_at"),
|
||||
updated_at: row.get("updated_at"),
|
||||
deleted_at: row.try_get("deleted_at").ok(),
|
||||
tags,
|
||||
cover_image: row.get("cover_image"),
|
||||
reading_time: (word_count / 200).max(1),
|
||||
word_count,
|
||||
}
|
||||
}
|
||||
|
||||
/// 将数据库行转换为完整文章详情。
|
||||
///
|
||||
/// 相比列表项额外包含上一篇/下一篇导航,
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
use super::helpers::{get_current_admin_user, row_to_post_list};
|
||||
use super::helpers::{get_current_admin_user, row_to_post_list_item};
|
||||
use super::types::PostListResponse;
|
||||
#[cfg(feature = "server")]
|
||||
use crate::api::error::AppError;
|
||||
@ -81,8 +81,8 @@ pub async fn list_published_posts(
|
||||
let limit = per_page as i64;
|
||||
let rows = client
|
||||
.query(
|
||||
"SELECT
|
||||
p.id, p.author_id, p.title, p.slug, p.summary, p.content_md, p.content_html,
|
||||
"SELECT
|
||||
p.id, p.author_id, p.title, p.slug, p.summary, p.content_md,
|
||||
p.status, p.published_at, p.created_at, p.updated_at, p.cover_image,
|
||||
COALESCE(array_agg(t.name) FILTER (WHERE t.name IS NOT NULL), '{}') as tags
|
||||
FROM posts p
|
||||
@ -99,7 +99,7 @@ pub async fn list_published_posts(
|
||||
|
||||
let mut posts = Vec::new();
|
||||
for row in &rows {
|
||||
posts.push(row_to_post_list(&client, row).await);
|
||||
posts.push(row_to_post_list_item(row));
|
||||
}
|
||||
|
||||
crate::cache::set_post_list(&cache_key, posts.clone(), total).await;
|
||||
@ -138,8 +138,8 @@ pub async fn list_posts(page: i32, per_page: i32) -> Result<PostListResponse, Se
|
||||
let limit = per_page as i64;
|
||||
let rows = client
|
||||
.query(
|
||||
"SELECT
|
||||
p.id, p.author_id, p.title, p.slug, p.summary, p.content_md, p.content_html,
|
||||
"SELECT
|
||||
p.id, p.author_id, p.title, p.slug, p.summary, p.content_md,
|
||||
p.status, p.published_at, p.created_at, p.updated_at, p.cover_image,
|
||||
COALESCE(array_agg(t.name) FILTER (WHERE t.name IS NOT NULL), '{}') as tags
|
||||
FROM posts p
|
||||
@ -156,7 +156,7 @@ pub async fn list_posts(page: i32, per_page: i32) -> Result<PostListResponse, Se
|
||||
|
||||
let mut posts = Vec::new();
|
||||
for row in &rows {
|
||||
posts.push(row_to_post_list(&client, row).await);
|
||||
posts.push(row_to_post_list_item(row));
|
||||
}
|
||||
|
||||
Ok(PostListResponse { posts, total })
|
||||
@ -197,8 +197,8 @@ pub async fn list_deleted_posts(
|
||||
let limit = per_page as i64;
|
||||
let rows = client
|
||||
.query(
|
||||
"SELECT
|
||||
p.id, p.author_id, p.title, p.slug, p.summary, p.content_md, p.content_html,
|
||||
"SELECT
|
||||
p.id, p.author_id, p.title, p.slug, p.summary, p.content_md,
|
||||
p.status, p.published_at, p.created_at, p.updated_at, p.cover_image, p.deleted_at,
|
||||
COALESCE(array_agg(t.name) FILTER (WHERE t.name IS NOT NULL), '{}') as tags
|
||||
FROM posts p
|
||||
@ -215,7 +215,7 @@ pub async fn list_deleted_posts(
|
||||
|
||||
let mut posts = Vec::new();
|
||||
for row in &rows {
|
||||
posts.push(row_to_post_list(&client, row).await);
|
||||
posts.push(row_to_post_list_item(row));
|
||||
}
|
||||
|
||||
Ok(PostListResponse { posts, total })
|
||||
@ -250,8 +250,8 @@ pub async fn get_posts_by_tag(tag_name: String) -> Result<PostListResponse, Serv
|
||||
// 通过 JOIN 筛选含目标标签的已发布文章,并聚合该文章的所有标签。
|
||||
let rows = client
|
||||
.query(
|
||||
"SELECT
|
||||
p.id, p.author_id, p.title, p.slug, p.summary, p.content_md, p.content_html,
|
||||
"SELECT
|
||||
p.id, p.author_id, p.title, p.slug, p.summary, p.content_md,
|
||||
p.status, p.published_at, p.created_at, p.updated_at, p.cover_image,
|
||||
COALESCE(array_agg(t2.name) FILTER (WHERE t2.name IS NOT NULL), '{}') as tags
|
||||
FROM posts p
|
||||
@ -270,7 +270,7 @@ pub async fn get_posts_by_tag(tag_name: String) -> Result<PostListResponse, Serv
|
||||
|
||||
let mut posts = Vec::new();
|
||||
for row in &rows {
|
||||
posts.push(row_to_post_list(&client, row).await);
|
||||
posts.push(row_to_post_list_item(row));
|
||||
}
|
||||
|
||||
// 当前查询未分页,返回全部匹配文章,因此 total 等于结果长度。
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
use super::helpers::row_to_post_list;
|
||||
use super::helpers::row_to_post_list_item;
|
||||
use super::types::PostListResponse;
|
||||
#[cfg(feature = "server")]
|
||||
use crate::api::error::AppError;
|
||||
@ -56,8 +56,8 @@ pub async fn search_posts(query: String) -> Result<PostListResponse, ServerFnErr
|
||||
// 使用 ILIKE 做前缀模糊匹配,并按 word_similarity 降序、发布时间降序排序。
|
||||
let rows = client
|
||||
.query(
|
||||
"SELECT
|
||||
p.id, p.author_id, p.title, p.slug, p.summary, p.content_md, p.content_html,
|
||||
"SELECT
|
||||
p.id, p.author_id, p.title, p.slug, p.summary, p.content_md,
|
||||
p.status, p.published_at, p.created_at, p.updated_at, p.cover_image,
|
||||
COALESCE(array_agg(t.name) FILTER (WHERE t.name IS NOT NULL), '{}') as tags,
|
||||
word_similarity(p.search_text, $2) AS sml
|
||||
@ -76,7 +76,7 @@ pub async fn search_posts(query: String) -> Result<PostListResponse, ServerFnErr
|
||||
|
||||
let mut posts = Vec::new();
|
||||
for row in &rows {
|
||||
posts.push(row_to_post_list(&client, row).await);
|
||||
posts.push(row_to_post_list_item(row));
|
||||
}
|
||||
|
||||
let total = posts.len() as i64;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
//! 文章 API 的请求与响应数据结构。
|
||||
|
||||
use crate::models::post::{Post, PostStats, Tag};
|
||||
use crate::models::post::{Post, PostListItem, PostStats, Tag};
|
||||
|
||||
/// 创建/更新/删除文章的统一响应结构。
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
@ -18,8 +18,8 @@ pub struct CreatePostResponse {
|
||||
/// 文章列表响应。
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PostListResponse {
|
||||
/// 文章列表。
|
||||
pub posts: Vec<Post>,
|
||||
/// 文章列表(轻量 DTO,不含正文)。
|
||||
pub posts: Vec<PostListItem>,
|
||||
/// 符合查询条件的总数。
|
||||
pub total: i64,
|
||||
}
|
||||
|
||||
36
src/cache.rs
36
src/cache.rs
@ -14,7 +14,7 @@ use std::time::Duration;
|
||||
#[cfg(feature = "server")]
|
||||
use crate::models::comment::PublicComment;
|
||||
#[cfg(feature = "server")]
|
||||
use crate::models::post::{Post, PostStats, Tag};
|
||||
use crate::models::post::{Post, PostListItem, PostStats, Tag};
|
||||
|
||||
// ============================================================================
|
||||
// 缓存 TTL 配置
|
||||
@ -80,7 +80,7 @@ pub enum CacheKey {
|
||||
|
||||
/// 文章列表缓存类型,值为(文章列表,总数)。
|
||||
#[cfg(feature = "server")]
|
||||
pub type PostListCache = Cache<CacheKey, (Vec<Post>, i64)>;
|
||||
pub type PostListCache = Cache<CacheKey, (Vec<PostListItem>, i64)>;
|
||||
|
||||
/// 标签列表缓存类型。
|
||||
#[cfg(feature = "server")]
|
||||
@ -167,13 +167,13 @@ static PENDING_COUNT_CACHE: LazyLock<Cache<CacheKey, i64>> = LazyLock::new(|| {
|
||||
|
||||
/// 读取文章分页列表缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn get_post_list(key: &CacheKey) -> Option<(Vec<Post>, i64)> {
|
||||
pub async fn get_post_list(key: &CacheKey) -> Option<(Vec<PostListItem>, i64)> {
|
||||
POST_LIST_CACHE.get(key).await
|
||||
}
|
||||
|
||||
/// 写入文章分页列表缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn set_post_list(key: &CacheKey, posts: Vec<Post>, total: i64) {
|
||||
pub async fn set_post_list(key: &CacheKey, posts: Vec<PostListItem>, total: i64) {
|
||||
let _ = POST_LIST_CACHE.insert(key.clone(), (posts, total)).await;
|
||||
}
|
||||
|
||||
@ -224,7 +224,7 @@ pub async fn set_post_by_slug(slug: &str, post: Option<Post>) {
|
||||
|
||||
/// 按标签读取文章列表缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn get_posts_by_tag(tag: &str) -> Option<(Vec<Post>, i64)> {
|
||||
pub async fn get_posts_by_tag(tag: &str) -> Option<(Vec<PostListItem>, i64)> {
|
||||
TAG_POSTS_CACHE
|
||||
.get(&CacheKey::PostsByTag(tag.to_string()))
|
||||
.await
|
||||
@ -232,7 +232,7 @@ pub async fn get_posts_by_tag(tag: &str) -> Option<(Vec<Post>, i64)> {
|
||||
|
||||
/// 按标签写入文章列表缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn set_posts_by_tag(tag: &str, posts: Vec<Post>, total: i64) {
|
||||
pub async fn set_posts_by_tag(tag: &str, posts: Vec<PostListItem>, total: i64) {
|
||||
let _ = TAG_POSTS_CACHE
|
||||
.insert(CacheKey::PostsByTag(tag.to_string()), (posts, total))
|
||||
.await;
|
||||
@ -379,15 +379,31 @@ mod tests {
|
||||
page: 999,
|
||||
per_page: 99,
|
||||
};
|
||||
let posts = vec![];
|
||||
let posts = vec![PostListItem {
|
||||
id: 1,
|
||||
author_id: 1,
|
||||
title: "List Item".to_string(),
|
||||
slug: "list-item".to_string(),
|
||||
summary: None,
|
||||
status: PostStatus::Published,
|
||||
published_at: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
deleted_at: None,
|
||||
tags: vec!["rust".to_string()],
|
||||
cover_image: None,
|
||||
reading_time: 1,
|
||||
word_count: 10,
|
||||
}];
|
||||
|
||||
set_post_list(&key, posts.clone(), 0).await;
|
||||
set_post_list(&key, posts.clone(), 1).await;
|
||||
let cached = get_post_list(&key).await;
|
||||
|
||||
assert!(cached.is_some());
|
||||
let (cached_posts, cached_total) = cached.unwrap();
|
||||
assert_eq!(cached_posts.len(), 0);
|
||||
assert_eq!(cached_total, 0);
|
||||
assert_eq!(cached_posts.len(), 1);
|
||||
assert_eq!(cached_posts[0].title, "List Item");
|
||||
assert_eq!(cached_total, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@ -6,7 +6,7 @@ use dioxus::prelude::*;
|
||||
use dioxus::router::components::Link;
|
||||
|
||||
use crate::components::image_viewer::ImageViewer;
|
||||
use crate::models::post::Post;
|
||||
use crate::models::post::PostListItem;
|
||||
use crate::router::Route;
|
||||
|
||||
/// 文章卡片组件。
|
||||
@ -23,7 +23,7 @@ use crate::router::Route;
|
||||
/// 关键事件:
|
||||
/// - 点击标签时阻止事件冒泡,避免触发整卡跳转
|
||||
#[component]
|
||||
pub fn PostCard(post: Post) -> Element {
|
||||
pub fn PostCard(post: PostListItem) -> Element {
|
||||
let post_slug = post.slug.clone();
|
||||
let date_str = post.formatted_date();
|
||||
let has_cover = post.cover_image.is_some();
|
||||
|
||||
@ -113,6 +113,77 @@ impl Post {
|
||||
}
|
||||
}
|
||||
|
||||
/// 文章列表项 DTO。
|
||||
///
|
||||
/// 仅包含列表/标签/搜索/归档等场景需要的字段,不含 `content_md` 与 `content_html`,
|
||||
/// 以降低缓存内存占用与序列化体积。`deleted_at` 保留,供回收站列表使用。
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PostListItem {
|
||||
/// 文章主键。
|
||||
pub id: i32,
|
||||
/// 作者用户主键。
|
||||
pub author_id: i32,
|
||||
/// 文章标题。
|
||||
pub title: String,
|
||||
/// URL slug,用于生成文章链接。
|
||||
pub slug: String,
|
||||
/// 摘要,可选。
|
||||
pub summary: Option<String>,
|
||||
/// 文章发布状态。
|
||||
pub status: PostStatus,
|
||||
/// 正式发布时间,None 表示尚未发布。
|
||||
pub published_at: Option<DateTime<Utc>>,
|
||||
/// 创建时间。
|
||||
pub created_at: DateTime<Utc>,
|
||||
/// 最后更新时间。
|
||||
pub updated_at: DateTime<Utc>,
|
||||
/// 软删除时间,None 表示未删除。仅回收站查询填充。
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
/// 关联标签列表。
|
||||
pub tags: Vec<String>,
|
||||
/// 封面图片 URL。
|
||||
pub cover_image: Option<String>,
|
||||
/// 预计阅读时间(分钟)。
|
||||
pub reading_time: u32,
|
||||
/// 字数统计。
|
||||
pub word_count: u32,
|
||||
}
|
||||
|
||||
impl PostListItem {
|
||||
/// 返回用于展示的文章日期:优先使用发布时间,否则回退到创建时间。
|
||||
pub fn formatted_date(&self) -> String {
|
||||
self.published_at
|
||||
.map(|d| d.format("%Y-%m-%d").to_string())
|
||||
.unwrap_or_else(|| self.created_at.format("%Y-%m-%d").to_string())
|
||||
}
|
||||
|
||||
/// 返回中文状态标签。
|
||||
pub fn status_label(&self) -> &'static str {
|
||||
match self.status {
|
||||
PostStatus::Published => "已发布",
|
||||
PostStatus::Draft => "草稿",
|
||||
}
|
||||
}
|
||||
|
||||
/// 返回状态文本在 light/dark 模式下的 Tailwind 颜色类。
|
||||
pub fn status_class(&self) -> &'static str {
|
||||
match self.status {
|
||||
PostStatus::Published => "text-green-600 dark:text-green-400",
|
||||
PostStatus::Draft => "text-gray-400 dark:text-[#9b9c9d]",
|
||||
}
|
||||
}
|
||||
|
||||
/// 返回状态徽章在 light/dark 模式下的 Tailwind 背景与颜色类。
|
||||
pub fn status_badge_class(&self) -> &'static str {
|
||||
match self.status {
|
||||
PostStatus::Published => {
|
||||
"bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300"
|
||||
}
|
||||
PostStatus::Draft => "bg-gray-100 dark:bg-[#333] text-gray-600 dark:text-[#9b9c9d]",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 前后文章导航结构体。
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PostNav {
|
||||
|
||||
@ -13,7 +13,7 @@ use crate::api::posts::{get_post_stats, list_posts};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use crate::api::posts::{PostListResponse, PostStatsResponse};
|
||||
use crate::components::ui::ADMIN_CARD_CLASS;
|
||||
use crate::models::post::{Post, PostStats};
|
||||
use crate::models::post::{PostListItem, PostStats};
|
||||
use crate::router::Route;
|
||||
|
||||
/// 后台仪表盘页面组件。
|
||||
@ -25,7 +25,7 @@ use crate::router::Route;
|
||||
pub fn Admin() -> Element {
|
||||
// 仪表盘状态:统计数据、最近文章、待审核评论数与首次加载标志。
|
||||
let mut stats = use_signal(|| None::<PostStats>);
|
||||
let mut recent_posts = use_signal(|| None::<Vec<Post>>);
|
||||
let mut recent_posts = use_signal(|| None::<Vec<PostListItem>>);
|
||||
let mut pending_count = use_signal(|| None::<i64>);
|
||||
let mut loaded = use_signal(|| false);
|
||||
|
||||
@ -165,7 +165,7 @@ fn StatCard(value: String, label: String) -> Element {
|
||||
|
||||
/// 最近文章列表项,显示标题、状态标签与发布日期。
|
||||
#[component]
|
||||
fn RecentPostItem(post: Post) -> Element {
|
||||
fn RecentPostItem(post: PostListItem) -> Element {
|
||||
let date_str = post.formatted_date();
|
||||
let status_label = post.status_label();
|
||||
let status_class = post.status_class();
|
||||
|
||||
@ -15,7 +15,7 @@ use crate::api::posts::{delete_post, rebuild_content_html, CreatePostResponse, R
|
||||
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
||||
use crate::components::skeletons::posts_skeleton::PostsSkeleton;
|
||||
use crate::components::ui::{EmptyState, Pagination, StatusBadge, ADMIN_ROW_HOVER, ADMIN_TABLE_CLASS, BTN_TEXT_RED};
|
||||
use crate::models::post::Post;
|
||||
use crate::models::post::PostListItem;
|
||||
use crate::router::Route;
|
||||
|
||||
/// 每页展示的文章数量。
|
||||
@ -34,7 +34,7 @@ pub fn Posts() -> Element {
|
||||
pub fn PostsPage(page: i32) -> Element {
|
||||
let current_page = page.max(1);
|
||||
// 文章列表、总数、加载状态、删除中 ID、重建缓存状态与结果。
|
||||
let mut posts = use_signal(Vec::new);
|
||||
let mut posts = use_signal(Vec::<PostListItem>::new);
|
||||
let mut total = use_signal(|| 0_i64);
|
||||
let mut loading = use_signal(|| true);
|
||||
let mut deleting = use_signal(|| None::<i32>);
|
||||
@ -96,7 +96,7 @@ pub fn PostsPage(page: i32) -> Element {
|
||||
}
|
||||
});
|
||||
|
||||
let get_posts = move || -> Vec<Post> { posts() };
|
||||
let get_posts = move || -> Vec<PostListItem> { posts() };
|
||||
|
||||
rsx! {
|
||||
div { class: "space-y-6",
|
||||
@ -215,7 +215,7 @@ pub fn PostsPage(page: i32) -> Element {
|
||||
|
||||
/// 文章表格行组件,展示单篇文章的标题、状态、日期与操作按钮。
|
||||
#[component]
|
||||
fn PostRow(post: Post, deleting: bool, on_delete: EventHandler<i32>) -> Element {
|
||||
fn PostRow(post: PostListItem, deleting: bool, on_delete: EventHandler<i32>) -> Element {
|
||||
let date_str = post.formatted_date();
|
||||
|
||||
rsx! {
|
||||
|
||||
@ -23,7 +23,7 @@ use crate::components::ui::{
|
||||
EmptyState, Pagination, StatusBadge, ADMIN_ROW_HOVER, ADMIN_TABLE_CLASS, BTN_SOLID_GREEN,
|
||||
BTN_SOLID_RED, BTN_TEXT_ACCENT, BTN_TEXT_RED, CHECKBOX_CLASS,
|
||||
};
|
||||
use crate::models::post::Post;
|
||||
use crate::models::post::PostListItem;
|
||||
use crate::models::settings::TrashSettings;
|
||||
use crate::router::Route;
|
||||
|
||||
@ -44,7 +44,7 @@ pub fn Trash() -> Element {
|
||||
pub fn TrashPage(page: i32) -> Element {
|
||||
let current_page = page.max(1);
|
||||
let mut selected_ids: Signal<HashSet<i32>> = use_signal(HashSet::new);
|
||||
let mut posts: Signal<Vec<Post>> = use_signal(Vec::new);
|
||||
let mut posts: Signal<Vec<PostListItem>> = use_signal(Vec::new);
|
||||
let mut total: Signal<i64> = use_signal(|| 0);
|
||||
#[allow(unused_mut)]
|
||||
let mut loading: Signal<bool> = use_signal(|| false);
|
||||
@ -500,7 +500,7 @@ pub fn TrashPage(page: i32) -> Element {
|
||||
/// 计算剩余天数(保留期 - 已删除天数)。
|
||||
///
|
||||
/// 返回 (剩余天数, 是否已过期)。基于客户端时钟计算,轻微漂移可接受。
|
||||
fn remaining_days(post: &Post, retention_days: i32) -> (i64, bool) {
|
||||
fn remaining_days(post: &PostListItem, retention_days: i32) -> (i64, bool) {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
if let Some(deleted_at) = post.deleted_at {
|
||||
@ -523,7 +523,7 @@ fn remaining_days(post: &Post, retention_days: i32) -> (i64, bool) {
|
||||
/// 回收站表格行组件。
|
||||
#[component]
|
||||
fn TrashRow(
|
||||
post: Post,
|
||||
post: PostListItem,
|
||||
retention_days: i32,
|
||||
selected: bool,
|
||||
on_select: EventHandler<bool>,
|
||||
|
||||
@ -13,7 +13,7 @@ use dioxus::router::components::Link;
|
||||
use crate::api::posts::{list_published_posts, PostListResponse};
|
||||
use crate::components::skeletons::archive_skeleton::ArchiveSkeleton;
|
||||
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
||||
use crate::models::post::Post;
|
||||
use crate::models::post::PostListItem;
|
||||
use crate::router::Route;
|
||||
|
||||
/// 按年份分组的文章归档结构。
|
||||
@ -28,13 +28,13 @@ struct YearGroup {
|
||||
struct MonthGroup {
|
||||
month: String,
|
||||
month_en: String,
|
||||
posts: Vec<Post>,
|
||||
posts: Vec<PostListItem>,
|
||||
}
|
||||
|
||||
/// 将文章列表按 `formatted_date()` 返回的 `YYYY-MM-DD` 格式进行年、月分组。
|
||||
///
|
||||
/// 返回的结果按原始文章顺序组织,调用前已按发布时间降序排列。
|
||||
fn group_posts(posts: &[Post]) -> Vec<YearGroup> {
|
||||
fn group_posts(posts: &[PostListItem]) -> Vec<YearGroup> {
|
||||
let mut years: Vec<YearGroup> = vec![];
|
||||
|
||||
for post in posts {
|
||||
@ -204,7 +204,7 @@ fn MonthSection(month_group: MonthGroup, year: String) -> Element {
|
||||
|
||||
/// 单条归档文章组件,展示标题与发布日期,并通过覆盖层链接到文章详情。
|
||||
#[component]
|
||||
fn ArchiveEntry(post: Post) -> Element {
|
||||
fn ArchiveEntry(post: PostListItem) -> Element {
|
||||
let date_str = post.formatted_date();
|
||||
|
||||
rsx! {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user