1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub enum PostStatus {
12 Draft,
14 Published,
16}
17
18impl PostStatus {
19 pub fn as_str(&self) -> &'static str {
21 match self {
22 PostStatus::Draft => "draft",
23 PostStatus::Published => "published",
24 }
25 }
26
27 pub fn label(&self) -> &'static str {
29 match self {
30 PostStatus::Draft => "草稿",
31 PostStatus::Published => "已发布",
32 }
33 }
34
35 pub fn tone(&self) -> &'static str {
37 match self {
38 PostStatus::Draft => "muted",
39 PostStatus::Published => "clay",
40 }
41 }
42
43 #[cfg(feature = "server")]
45 #[allow(clippy::should_implement_trait)]
47 pub fn from_str(s: &str) -> Option<Self> {
48 match s {
49 "draft" => Some(PostStatus::Draft),
50 "published" => Some(PostStatus::Published),
51 _ => None,
52 }
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
58pub struct Post {
59 pub id: i32,
61 pub author_id: i32,
63 pub title: String,
65 pub slug: String,
67 pub summary: Option<String>,
69 pub content_md: String,
71 pub content_html: Option<String>,
73 pub status: PostStatus,
75 #[cfg_attr(
77 target_arch = "wasm32",
78 serde(
79 default,
80 deserialize_with = "crate::models::rfc3339::deserialize_option"
81 )
82 )]
83 pub published_at: Option<DateTime<Utc>>,
84 #[cfg_attr(
86 target_arch = "wasm32",
87 serde(deserialize_with = "crate::models::rfc3339::deserialize")
88 )]
89 pub created_at: DateTime<Utc>,
90 #[cfg_attr(
92 target_arch = "wasm32",
93 serde(deserialize_with = "crate::models::rfc3339::deserialize")
94 )]
95 pub updated_at: DateTime<Utc>,
96 #[cfg_attr(
98 target_arch = "wasm32",
99 serde(
100 default,
101 deserialize_with = "crate::models::rfc3339::deserialize_option"
102 )
103 )]
104 pub deleted_at: Option<DateTime<Utc>>,
105 pub tags: Vec<String>,
107 pub cover_image: Option<String>,
109 pub reading_time: u32,
111 pub word_count: u32,
113 pub toc_html: Option<String>,
115 pub prev_post: Option<PostNav>,
117 pub next_post: Option<PostNav>,
119}
120
121#[derive(Debug, Clone, PartialEq)]
126pub struct PostHeaderData {
127 pub id: i32,
128 pub title: String,
129 pub status: PostStatus,
130 pub summary: Option<String>,
131 pub tags: Vec<String>,
132 pub published_at: Option<DateTime<Utc>>,
133 pub created_at: DateTime<Utc>,
134 pub reading_time: u32,
135 pub word_count: u32,
136}
137
138impl From<&Post> for PostHeaderData {
139 fn from(post: &Post) -> Self {
140 Self {
141 id: post.id,
142 title: post.title.clone(),
143 status: post.status.clone(),
144 summary: post.summary.clone(),
145 tags: post.tags.clone(),
146 published_at: post.published_at,
147 created_at: post.created_at,
148 reading_time: post.reading_time,
149 word_count: post.word_count,
150 }
151 }
152}
153
154#[derive(Debug, Clone, PartialEq)]
156pub struct PostFooterData {
157 pub prev_post: Option<PostNav>,
158 pub next_post: Option<PostNav>,
159}
160
161impl From<&Post> for PostFooterData {
162 fn from(post: &Post) -> Self {
163 Self {
164 prev_post: post.prev_post.clone(),
165 next_post: post.next_post.clone(),
166 }
167 }
168}
169
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
175pub struct PostListItem {
176 pub id: i32,
178 pub author_id: i32,
180 pub title: String,
182 pub slug: String,
184 pub summary: Option<String>,
186 pub status: PostStatus,
188 #[cfg_attr(
190 target_arch = "wasm32",
191 serde(
192 default,
193 deserialize_with = "crate::models::rfc3339::deserialize_option"
194 )
195 )]
196 pub published_at: Option<DateTime<Utc>>,
197 #[cfg_attr(
199 target_arch = "wasm32",
200 serde(deserialize_with = "crate::models::rfc3339::deserialize")
201 )]
202 pub created_at: DateTime<Utc>,
203 #[cfg_attr(
205 target_arch = "wasm32",
206 serde(deserialize_with = "crate::models::rfc3339::deserialize")
207 )]
208 pub updated_at: DateTime<Utc>,
209 #[cfg_attr(
211 target_arch = "wasm32",
212 serde(
213 default,
214 deserialize_with = "crate::models::rfc3339::deserialize_option"
215 )
216 )]
217 pub deleted_at: Option<DateTime<Utc>>,
218 pub tags: Vec<String>,
220 pub cover_image: Option<String>,
222 pub reading_time: u32,
224 pub word_count: u32,
226}
227
228#[cfg(any(feature = "server", test))]
229#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
234pub struct FeedItem {
235 pub title: String,
237 pub slug: String,
239 pub summary: Option<String>,
241 pub content_html: Option<String>,
243 #[cfg_attr(
245 target_arch = "wasm32",
246 serde(deserialize_with = "crate::models::rfc3339::deserialize")
247 )]
248 pub published_at: DateTime<Utc>,
249 #[cfg_attr(
251 target_arch = "wasm32",
252 serde(deserialize_with = "crate::models::rfc3339::deserialize")
253 )]
254 pub updated_at: DateTime<Utc>,
255 pub tags: Vec<String>,
257}
258
259#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
261pub struct PostNav {
262 pub title: String,
264 pub slug: String,
266}
267
268#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
270pub struct Tag {
271 pub id: i32,
273 pub name: String,
275 pub post_count: i64,
277}
278
279#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
281pub struct PostStats {
282 pub total: i64,
284 pub drafts: i64,
286 pub published: i64,
288 pub trash: i64,
290 pub recent_30d: i64,
292 pub monthly_published: Vec<MonthCount>,
295}
296
297#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
299pub struct MonthCount {
300 pub year: i32,
301 pub month: u32,
303 pub count: i64,
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
311 #[cfg(feature = "server")]
312 fn post_status_from_str() {
313 assert_eq!(PostStatus::from_str("draft"), Some(PostStatus::Draft));
314 assert_eq!(
315 PostStatus::from_str("published"),
316 Some(PostStatus::Published)
317 );
318 assert_eq!(PostStatus::from_str("unknown"), None);
319 assert_eq!(PostStatus::from_str(""), None);
320 }
321
322 #[test]
323 fn post_status_as_str() {
324 assert_eq!(PostStatus::Draft.as_str(), "draft");
325 assert_eq!(PostStatus::Published.as_str(), "published");
326 }
327
328 #[test]
329 #[cfg(feature = "server")]
330 fn post_status_roundtrip() {
331 for status in [PostStatus::Draft, PostStatus::Published] {
332 assert_eq!(PostStatus::from_str(status.as_str()), Some(status.clone()));
333 }
334 }
335
336 #[test]
337 fn post_status_serde_roundtrip() {
338 let json = serde_json::to_string(&PostStatus::Draft).unwrap();
339 assert_eq!(
340 serde_json::from_str::<PostStatus>(&json).unwrap(),
341 PostStatus::Draft
342 );
343 }
344}