1use chrono::DateTime;
12
13#[cfg(target_arch = "wasm32")]
21pub async fn sleep_ms(ms: u32) {
22 use wasm_bindgen::JsCast;
23 use wasm_bindgen_futures::JsFuture;
24 let promise = js_sys::Promise::new(&mut |resolve, _| {
25 let delay = ms.min(i32::MAX as u32) as i32;
27 let window = web_sys::window().expect("sleep_ms 必须在浏览器上下文中调用:无 window");
28 window
29 .set_timeout_with_callback_and_timeout_and_arguments_0(&resolve.unchecked_into(), delay)
30 .expect("setTimeout with a number delay cannot fail per WebIDL");
31 });
32 let _ = JsFuture::from(promise).await;
33}
34
35#[cfg(all(feature = "server", not(target_arch = "wasm32")))]
42pub async fn sleep_ms(ms: u32) {
43 tokio::time::sleep(std::time::Duration::from_millis(ms as u64)).await;
44}
45
46#[cfg(all(not(feature = "server"), not(target_arch = "wasm32")))]
52pub async fn sleep_ms(_ms: u32) {
53 panic!("sleep_ms 在非 wasm32 且非 server 的无效构建组合下被调用:请检查 feature 配置");
54}
55
56pub fn now_millis() -> i64 {
60 #[cfg(target_arch = "wasm32")]
61 {
62 js_sys::Date::now() as i64
63 }
64 #[cfg(not(target_arch = "wasm32"))]
65 {
66 chrono::Utc::now().timestamp_millis()
67 }
68}
69
70pub fn utc_hhmm_to_local(t: &str) -> String {
75 #[cfg(target_arch = "wasm32")]
76 {
77 let mut parts = t.split(':');
78 let h = parts
79 .next()
80 .and_then(|s| s.parse::<u32>().ok())
81 .unwrap_or(0);
82 let m = parts
83 .next()
84 .and_then(|s| s.parse::<u32>().ok())
85 .unwrap_or(0);
86 let d = js_sys::Date::new_0();
87 d.set_utc_hours(h);
88 d.set_utc_minutes(m);
89 d.set_utc_seconds(0);
90 d.set_utc_milliseconds(0);
91 format!("{:02}:{:02}", d.get_hours(), d.get_minutes())
92 }
93 #[cfg(not(target_arch = "wasm32"))]
94 {
95 t.to_string()
96 }
97}
98
99#[cfg(target_arch = "wasm32")]
102pub fn local_hhmm_to_utc(t: &str) -> String {
103 let mut parts = t.split(':');
104 let Some(h) = parts.next().and_then(|s| s.parse::<u32>().ok()) else {
105 return "04:00".to_string();
106 };
107 let Some(m) = parts.next().and_then(|s| s.parse::<u32>().ok()) else {
108 return "04:00".to_string();
109 };
110 let d = js_sys::Date::new_0();
111 d.set_hours(h);
112 d.set_minutes(m);
113 d.set_seconds(0);
114 d.set_milliseconds(0);
115 format!("{:02}:{:02}", d.get_utc_hours(), d.get_utc_minutes())
116}
117
118#[cfg(any(feature = "server", test))]
119pub fn relative_label_from_millis(delta_millis: i64, created_iso: &str) -> (String, String) {
127 let dt = DateTime::parse_from_rfc3339(created_iso).ok();
128 relative_label_inner(delta_millis, dt.as_ref())
129}
130
131fn relative_label_inner(
133 delta_millis: i64,
134 dt: Option<&chrono::DateTime<chrono::FixedOffset>>,
135) -> (String, String) {
136 let seconds = delta_millis / 1000;
137 let label = if seconds < 60 {
138 "刚刚".to_string()
139 } else {
140 let minutes = seconds / 60;
141 if minutes < 60 {
142 format!("{minutes} 分钟前")
143 } else {
144 let hours = minutes / 60;
145 if hours < 24 {
146 format!("{hours} 小时前")
147 } else {
148 let days = hours / 24;
149 if days < 30 {
150 format!("{days} 天前")
151 } else {
152 String::new()
154 }
155 }
156 }
157 };
158
159 let absolute = dt
161 .map(|d| d.format("%Y-%m-%d").to_string())
162 .unwrap_or_default();
163
164 let label = if label.is_empty() {
165 absolute.clone()
166 } else {
167 label
168 };
169 (label, absolute)
170}
171
172pub fn format_relative_time_iso(created_iso: &str) -> String {
176 let dt = DateTime::parse_from_rfc3339(created_iso).ok();
177 let delta_millis = match &dt {
178 Some(d) => now_millis() - d.timestamp_millis(),
179 None => return "刚刚".to_string(),
180 };
181 relative_label_inner(delta_millis, dt.as_ref()).0
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 const ISO: &str = "2026-06-22T05:43:57.565+00:00";
189
190 #[test]
191 fn relative_label_just_now_under_60s() {
192 let (label, _) = relative_label_from_millis(0, ISO);
193 assert_eq!(label, "刚刚");
194 let (label, _) = relative_label_from_millis(59_999, ISO);
195 assert_eq!(label, "刚刚");
196 }
197
198 #[test]
199 fn relative_label_minutes() {
200 let (label, _) = relative_label_from_millis(60_000, ISO);
201 assert_eq!(label, "1 分钟前");
202 let (label, _) = relative_label_from_millis(5 * 60_000, ISO);
203 assert_eq!(label, "5 分钟前");
204 let (label, _) = relative_label_from_millis(59 * 60_000, ISO);
205 assert_eq!(label, "59 分钟前");
206 }
207
208 #[test]
209 fn relative_label_hours() {
210 let (label, _) = relative_label_from_millis(60 * 60_000, ISO);
211 assert_eq!(label, "1 小时前");
212 let (label, _) = relative_label_from_millis(3 * 3_600_000, ISO);
213 assert_eq!(label, "3 小时前");
214 let (label, _) = relative_label_from_millis(23 * 3_600_000, ISO);
215 assert_eq!(label, "23 小时前");
216 }
217
218 #[test]
219 fn relative_label_days() {
220 let (label, _) = relative_label_from_millis(24 * 3_600_000, ISO);
221 assert_eq!(label, "1 天前");
222 let (label, _) = relative_label_from_millis(7 * 24 * 3_600_000, ISO);
223 assert_eq!(label, "7 天前");
224 let (label, _) = relative_label_from_millis(29 * 24 * 3_600_000, ISO);
225 assert_eq!(label, "29 天前");
226 }
227
228 #[test]
229 fn relative_label_falls_back_to_date_over_30_days() {
230 let (label, absolute) = relative_label_from_millis(60 * 24 * 3_600_000, ISO);
231 assert_eq!(label, "2026-06-22");
232 assert_eq!(absolute, "2026-06-22");
233 }
234
235 #[test]
236 fn relative_label_future_falls_back_to_just_now() {
237 let (label, _) = relative_label_from_millis(-5_000, ISO);
239 assert_eq!(label, "刚刚");
240 }
241
242 #[test]
243 fn relative_label_invalid_iso_still_returns_absolute_empty() {
244 let (label, absolute) = relative_label_from_millis(0, "not-a-date");
246 assert_eq!(label, "刚刚");
247 assert_eq!(absolute, "");
248 }
249
250 #[test]
251 fn format_relative_time_iso_invalid_iso_falls_back() {
252 assert_eq!(format_relative_time_iso("not-a-date"), "刚刚");
254 }
255}