1use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14pub struct Asset {
15 pub id: String,
16 pub path: String,
18 pub filename: String,
19 pub mime: String,
20 pub size_bytes: i64,
21 pub width: i32,
22 pub height: i32,
23 pub alt: Option<String>,
24 pub created_at: chrono::DateTime<chrono::Utc>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
34#[serde(tag = "kind", rename_all = "snake_case")]
35pub enum AssetRef {
36 Post {
38 post_id: i32,
39 title: String,
40 slug: String,
41 status: AssetRefPostStatus,
42 },
43 Comment {
45 comment_id: i64,
46 author_name: String,
47 post_id: i32,
48 post_title: String,
49 post_slug: String,
50 post_status: AssetRefPostStatus,
51 },
52 UserAvatar { user_id: i32, label: String },
54 FriendAvatar { friend_id: i32, name: String },
56}
57
58impl AssetRef {
59 pub fn describe(&self) -> String {
61 match self {
62 AssetRef::Post { title, .. } => format!("文章《{title}》"),
63 AssetRef::Comment {
64 author_name,
65 post_title,
66 ..
67 } => format!("评论({author_name} 在《{post_title}》)"),
68 AssetRef::UserAvatar { label, .. } => format!("用户头像({label})"),
69 AssetRef::FriendAvatar { name, .. } => format!("友链头像({name})"),
70 }
71 }
72}
73
74#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
79#[serde(rename_all = "snake_case")]
80pub enum AssetRefPostStatus {
81 Published,
82 Draft,
83 Trashed,
85}
86
87impl AssetRefPostStatus {
88 #[cfg(any(feature = "server", test))]
91 pub fn resolve(status: &str, deleted_at: Option<chrono::DateTime<chrono::Utc>>) -> Self {
92 if deleted_at.is_some() {
93 Self::Trashed
94 } else if status == "published" {
95 Self::Published
96 } else {
97 Self::Draft
98 }
99 }
100
101 pub fn badge(&self) -> Option<(&'static str, &'static str)> {
103 match self {
104 Self::Published => None,
105 Self::Draft => Some((
106 "草稿",
107 "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400",
108 )),
109 Self::Trashed => Some((
110 "回收站",
111 "bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400",
112 )),
113 }
114 }
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
119pub struct AssetDto {
120 #[serde(flatten)]
121 pub asset: Asset,
122 pub ref_count: i64,
123 pub refs: Vec<AssetRef>,
124}
125
126#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)]
128pub enum AssetFilter {
129 #[default]
130 All,
131 Used,
132 Orphan,
133}
134
135#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)]
137pub enum AssetSort {
138 #[default]
139 CreatedDesc,
140 SizeDesc,
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn ref_post_status_resolve() {
149 let now = chrono::Utc::now();
150 assert_eq!(
152 AssetRefPostStatus::resolve("published", Some(now)),
153 AssetRefPostStatus::Trashed
154 );
155 assert_eq!(
156 AssetRefPostStatus::resolve("published", None),
157 AssetRefPostStatus::Published
158 );
159 assert_eq!(
160 AssetRefPostStatus::resolve("draft", None),
161 AssetRefPostStatus::Draft
162 );
163 assert_eq!(
165 AssetRefPostStatus::resolve("unknown", None),
166 AssetRefPostStatus::Draft
167 );
168 }
169
170 #[test]
171 fn ref_post_status_badge() {
172 assert_eq!(AssetRefPostStatus::Published.badge(), None);
173 assert_eq!(AssetRefPostStatus::Draft.badge().map(|b| b.0), Some("草稿"));
174 assert_eq!(
175 AssetRefPostStatus::Trashed.badge().map(|b| b.0),
176 Some("回收站")
177 );
178 }
179
180 #[test]
181 fn asset_ref_describe_covers_all_kinds() {
182 let status = AssetRefPostStatus::Published;
183 let cases = [
184 (
185 AssetRef::Post {
186 post_id: 1,
187 title: "标题".into(),
188 slug: "s".into(),
189 status,
190 },
191 "文章《标题》",
192 ),
193 (
194 AssetRef::Comment {
195 comment_id: 1,
196 author_name: "小明".into(),
197 post_id: 1,
198 post_title: "标题".into(),
199 post_slug: "s".into(),
200 post_status: status,
201 },
202 "评论(小明 在《标题》)",
203 ),
204 (
205 AssetRef::UserAvatar {
206 user_id: 1,
207 label: "xfy".into(),
208 },
209 "用户头像(xfy)",
210 ),
211 (
212 AssetRef::FriendAvatar {
213 friend_id: 1,
214 name: "某博客".into(),
215 },
216 "友链头像(某博客)",
217 ),
218 ];
219 for (r, want) in cases {
220 assert_eq!(r.describe(), want);
221 }
222 }
223
224 #[test]
225 fn asset_ref_serde_tagged_shape() {
226 let r = AssetRef::UserAvatar {
228 user_id: 7,
229 label: "xfy".into(),
230 };
231 let json = serde_json::to_value(&r).unwrap();
232 assert_eq!(json["kind"], "user_avatar");
233 assert_eq!(json["user_id"], 7);
234 let back: AssetRef = serde_json::from_value(json).unwrap();
235 assert_eq!(back, r);
236 }
237}