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