1#[derive(Clone, Copy, Default, PartialEq)]
19pub struct UploadsInFlight {
20 pub uploading: u32,
21 pub error: u32,
22}
23
24#[derive(Clone, PartialEq)]
26pub struct UploadErrorEntry {
27 pub id: String,
28 pub file_name: String,
29 pub message: String,
30}
31
32#[cfg(target_arch = "wasm32")]
37pub mod wasm {
38 use super::{UploadErrorEntry, UploadsInFlight};
39 use leptos::prelude::{RwSignal, Set, Update};
40 use wasm_bindgen::prelude::*;
41 use wasm_bindgen::JsCast;
42
43 #[wasm_bindgen]
48 extern "C" {
49 #[wasm_bindgen(js_namespace = TiptapEditor, js_name = create, catch)]
52 pub fn create(
53 container: &web_sys::HtmlElement,
54 options: &EditorOptions,
55 ) -> Result<EditorInstance, JsValue>;
56 }
57
58 #[wasm_bindgen]
60 extern "C" {
61 pub type EditorInstance;
63
64 #[wasm_bindgen(method, js_name = getMarkdown)]
66 pub fn get_markdown(this: &EditorInstance) -> String;
67
68 #[wasm_bindgen(method, js_name = setMarkdown)]
73 pub fn set_markdown(this: &EditorInstance, content: &str) -> js_sys::Promise;
74
75 #[wasm_bindgen(method, js_name = removeUploadByUploadId)]
77 pub fn remove_upload_by_upload_id(this: &EditorInstance, upload_id: &str) -> bool;
78
79 #[wasm_bindgen(method, js_name = insertUploading)]
82 pub fn insert_uploading(this: &EditorInstance, file: web_sys::File);
83
84 #[wasm_bindgen(method, js_name = insertImagesFromLibrary)]
88 pub fn insert_images_from_library(this: &EditorInstance, json: &str);
89
90 #[wasm_bindgen(method)]
92 pub fn destroy(this: &EditorInstance);
93 }
94
95 #[wasm_bindgen]
97 extern "C" {
98 pub type EditorOptions;
101
102 #[wasm_bindgen(method, setter, js_name = placeholder)]
104 pub fn set_placeholder(this: &EditorOptions, v: &str);
105
106 #[wasm_bindgen(method, setter, js_name = variant)]
109 pub fn set_variant(this: &EditorOptions, v: &str);
110
111 #[wasm_bindgen(method, setter, js_name = onUpdate)]
113 pub fn set_on_update(this: &EditorOptions, cb: &Closure<dyn FnMut(String)>);
114
115 #[wasm_bindgen(method, setter, js_name = onImageUpload)]
118 pub fn set_on_image_upload(
119 this: &EditorOptions,
120 cb: &Closure<dyn Fn(web_sys::File) -> js_sys::Promise>,
121 );
122
123 #[wasm_bindgen(method, setter, js_name = onReady)]
125 pub fn set_on_ready(this: &EditorOptions, cb: &Closure<dyn FnMut()>);
126
127 #[wasm_bindgen(method, setter, js_name = onUploadEvent)]
129 pub fn set_on_upload_event(this: &EditorOptions, cb: &Closure<dyn FnMut(UploadEventJs)>);
130
131 #[wasm_bindgen(method, setter, js_name = onRunCode)]
134 pub fn set_on_run_code(
135 this: &EditorOptions,
136 cb: &Closure<dyn Fn(RunCodeOptsJs) -> js_sys::Promise>,
137 );
138
139 #[wasm_bindgen(method, setter, js_name = onPickFromLibrary)]
143 pub fn set_on_pick_from_library(this: &EditorOptions, cb: &Closure<dyn FnMut()>);
144
145 #[wasm_bindgen(method, setter, js_name = checkRoundtrip)]
148 pub fn set_check_roundtrip(
149 this: &EditorOptions,
150 cb: &Closure<dyn Fn(String, String) -> js_sys::Promise>,
151 );
152
153 #[wasm_bindgen(method, setter, js_name = onRenderMermaid)]
156 pub fn set_on_render_mermaid(
157 this: &EditorOptions,
158 cb: &Closure<dyn Fn(String) -> js_sys::Promise>,
159 );
160 }
161
162 impl Default for EditorOptions {
163 fn default() -> Self {
165 js_sys::Object::new().unchecked_into()
166 }
167 }
168
169 #[wasm_bindgen]
171 extern "C" {
172 #[derive(Clone)]
175 pub type UploadEventJs;
176
177 #[wasm_bindgen(method, getter)]
179 pub fn kind(this: &UploadEventJs) -> String;
180
181 #[wasm_bindgen(method, getter, js_name = uploadId)]
183 pub fn upload_id(this: &UploadEventJs) -> String;
184
185 #[wasm_bindgen(method, getter, js_name = fileName)]
187 pub fn file_name(this: &UploadEventJs) -> String;
188
189 #[wasm_bindgen(method, getter, js_name = errorMsg)]
191 pub fn error_msg(this: &UploadEventJs) -> Option<String>;
192
193 #[wasm_bindgen(method, getter)]
195 pub fn counts(this: &UploadEventJs) -> UploadCountsJs;
196 }
197
198 #[wasm_bindgen]
199 extern "C" {
200 #[derive(Clone)]
202 pub type UploadCountsJs;
203
204 #[wasm_bindgen(method, getter)]
206 pub fn uploading(this: &UploadCountsJs) -> u32;
207
208 #[wasm_bindgen(method, getter)]
210 pub fn error(this: &UploadCountsJs) -> u32;
211 }
212
213 #[wasm_bindgen]
217 extern "C" {
218 pub type RunCodeOptsJs;
219
220 #[wasm_bindgen(method, getter)]
222 pub fn language(this: &RunCodeOptsJs) -> String;
223
224 #[wasm_bindgen(method, getter)]
226 pub fn source(this: &RunCodeOptsJs) -> String;
227
228 #[wasm_bindgen(method, getter, js_name = overridesJson)]
231 pub fn overrides_json(this: &RunCodeOptsJs) -> String;
232 }
233
234 pub struct EditorHandle {
242 instance: EditorInstance,
243 _on_update: Closure<dyn FnMut(String)>,
244 _on_image_upload: Closure<dyn Fn(web_sys::File) -> js_sys::Promise>,
245 _on_ready: Closure<dyn FnMut()>,
246 _on_upload_event: Closure<dyn FnMut(UploadEventJs)>,
247 _on_run_code: Closure<dyn Fn(RunCodeOptsJs) -> js_sys::Promise>,
248 _on_pick_from_library: Closure<dyn FnMut()>,
249 _on_check_roundtrip: Option<Closure<dyn Fn(String, String) -> js_sys::Promise>>,
250 _on_render_mermaid: Option<Closure<dyn Fn(String) -> js_sys::Promise>>,
251 }
252
253 impl EditorHandle {
254 pub fn new(
260 instance: EditorInstance,
261 on_update: Closure<dyn FnMut(String)>,
262 on_image_upload: Closure<dyn Fn(web_sys::File) -> js_sys::Promise>,
263 on_ready: Closure<dyn FnMut()>,
264 on_upload_event: Closure<dyn FnMut(UploadEventJs)>,
265 on_run_code: Closure<dyn Fn(RunCodeOptsJs) -> js_sys::Promise>,
266 on_pick_from_library: Closure<dyn FnMut()>,
267 ) -> Self {
268 Self {
269 instance,
270 _on_update: on_update,
271 _on_image_upload: on_image_upload,
272 _on_ready: on_ready,
273 _on_upload_event: on_upload_event,
274 _on_run_code: on_run_code,
275 _on_pick_from_library: on_pick_from_library,
276 _on_check_roundtrip: None,
277 _on_render_mermaid: None,
278 }
279 }
280
281 pub fn instance(&self) -> &EditorInstance {
283 &self.instance
284 }
285
286 pub fn with_roundtrip_check(
288 mut self,
289 cb: Closure<dyn Fn(String, String) -> js_sys::Promise>,
290 ) -> Self {
291 self._on_check_roundtrip = Some(cb);
292 self
293 }
294
295 pub fn with_render_mermaid(
297 mut self,
298 cb: Closure<dyn Fn(String) -> js_sys::Promise>,
299 ) -> Self {
300 self._on_render_mermaid = Some(cb);
301 self
302 }
303
304 pub fn new_comment(
309 instance: EditorInstance,
310 on_update: Closure<dyn FnMut(String)>,
311 on_image_upload: Closure<dyn Fn(web_sys::File) -> js_sys::Promise>,
312 on_ready: Closure<dyn FnMut()>,
313 on_upload_event: Closure<dyn FnMut(UploadEventJs)>,
314 ) -> Self {
315 Self::new(
316 instance,
317 on_update,
318 on_image_upload,
319 on_ready,
320 on_upload_event,
321 Closure::new(|_opts: RunCodeOptsJs| js_sys::Promise::resolve(&JsValue::null())),
322 Closure::new(|| {}),
323 )
324 }
325 }
326
327 impl Drop for EditorHandle {
328 fn drop(&mut self) {
329 self.instance.destroy();
330 }
331 }
332
333 pub fn consume_upload_event(
345 ev: &UploadEventJs,
346 uploads_in_flight: RwSignal<UploadsInFlight>,
347 upload_errors: RwSignal<Vec<UploadErrorEntry>>,
348 ) {
349 let id = ev.upload_id();
350 match ev.kind().as_str() {
351 "error" => {
352 let msg = ev.error_msg().unwrap_or_else(|| "上传失败".to_string());
353 let file_name = ev.file_name();
354 upload_errors.update(|errors| {
356 if let Some(entry) = errors.iter_mut().find(|e| e.id == id) {
357 entry.message = msg;
358 } else {
359 errors.push(UploadErrorEntry {
360 id: id.clone(),
361 file_name,
362 message: msg,
363 });
364 }
365 });
366 }
367 "success" | "removed" => {
368 upload_errors.update(|errors| errors.retain(|e| e.id != id));
369 }
370 _ => {}
371 }
372 let c = ev.counts();
374 uploads_in_flight.set(UploadsInFlight {
375 uploading: c.uploading(),
376 error: c.error(),
377 });
378 }
379
380 async fn upload_file_to(url: &str, file: web_sys::File) -> Result<String, String> {
396 let data = crate::utils::web_upload::post_multipart_file(url, "image", &file).await?;
397 let url = data["url"].as_str().unwrap_or("");
398 if url.is_empty() {
399 return Err("上传成功但未返回图片地址".to_string());
401 }
402 Ok(url.to_string())
403 }
404
405 pub async fn upload_image_file(file: web_sys::File) -> Result<String, String> {
407 upload_file_to("/api/upload", file).await
408 }
409
410 pub async fn upload_note_image_file(file: web_sys::File) -> Result<String, String> {
412 upload_file_to("/api/notes/upload", file).await
413 }
414
415 pub async fn upload_comment_image_file(file: web_sys::File) -> Result<String, String> {
417 upload_file_to("/api/comments/upload", file).await
418 }
419
420 pub fn make_upload_closure() -> Closure<dyn Fn(web_sys::File) -> js_sys::Promise> {
424 Closure::new(move |file: web_sys::File| -> js_sys::Promise {
425 wasm_bindgen_futures::future_to_promise(async move {
426 upload_image_file(file)
427 .await
428 .map(|url| js_sys::JsString::from(url).into())
429 .map_err(|msg| js_sys::Error::new(&msg).into())
430 })
431 })
432 }
433
434 pub fn make_comment_upload_closure() -> Closure<dyn Fn(web_sys::File) -> js_sys::Promise> {
437 Closure::new(move |file: web_sys::File| -> js_sys::Promise {
438 wasm_bindgen_futures::future_to_promise(async move {
439 upload_comment_image_file(file)
440 .await
441 .map(|url| js_sys::JsString::from(url).into())
442 .map_err(|msg| js_sys::Error::new(&msg).into())
443 })
444 })
445 }
446
447 fn format_run_result(task: &crate::api::code_runner::ExecTask) -> String {
451 use crate::api::code_runner::ExecStatus;
452 let status_label = match task.status {
453 ExecStatus::Success => "Success",
454 ExecStatus::Error => "Error",
455 ExecStatus::Timeout => "Timeout",
456 ExecStatus::OomKilled => "OOM",
457 ExecStatus::Failed => "Failed",
458 _ => "Unknown",
459 };
460 match &task.result {
461 Some(res) => {
462 let mut out = format!("状态: {} · 耗时: {}ms", status_label, res.duration_ms);
463 if !res.stdout.is_empty() {
464 out.push_str("\nStdout:\n");
465 out.push_str(&res.stdout);
466 }
467 if !res.stderr.is_empty() {
468 out.push_str("\nStderr:\n");
469 out.push_str(&res.stderr);
470 }
471 out
472 }
473 None => format!("状态: {} · {}", status_label, task.stage),
474 }
475 }
476
477 pub fn make_run_code_closure() -> Closure<dyn Fn(RunCodeOptsJs) -> js_sys::Promise> {
486 Closure::new(move |opts: RunCodeOptsJs| -> js_sys::Promise {
487 wasm_bindgen_futures::future_to_promise(async move {
488 use crate::api::code_runner::{execute, ExecRequest, ExecStatus};
489 use crate::infra::runner_config::ResourceLimits;
490
491 let language = opts.language();
492 let source = opts.source();
493 let overrides_json = opts.overrides_json();
494
495 let overrides = if overrides_json.trim().is_empty() {
498 None
499 } else {
500 serde_json::from_slice::<ResourceLimits>(overrides_json.as_bytes()).ok()
501 };
502
503 let req = ExecRequest {
504 language,
505 source,
506 overrides,
507 };
508
509 match execute::start_exec(req).await {
510 Ok(task_id) => {
511 let poll_interval = 500;
512 for _ in 0..60 {
514 crate::utils::time::sleep_ms(poll_interval).await;
515 match execute::get_exec_result(task_id.clone()).await {
516 Ok(task) => {
517 let terminal = task.status != ExecStatus::Queued
518 && task.status != ExecStatus::Running;
519 if terminal {
520 let s = format_run_result(&task);
521 return Ok(js_sys::JsString::from(s).into());
522 }
523 }
524 Err(_) => {
525 return Err(js_sys::Error::new("结果获取异常").into());
526 }
527 }
528 }
529 Err(js_sys::Error::new("轮询超时,请重试").into())
530 }
531 Err(e) => Err(js_sys::Error::new(&crate::utils::error::failure_message(
532 "运行失败",
533 &e,
534 ))
535 .into()),
536 }
537 })
538 })
539 }
540
541 pub fn make_roundtrip_check_closure() -> Closure<dyn Fn(String, String) -> js_sys::Promise> {
546 Closure::new(
547 move |original: String, roundtrip: String| -> js_sys::Promise {
548 wasm_bindgen_futures::future_to_promise(async move {
549 crate::api::posts::check_markdown_roundtrip(original, roundtrip)
550 .await
551 .map(JsValue::from_bool)
552 .map_err(|e| {
553 js_sys::Error::new(&crate::utils::error::failure_message(
554 "往返检查失败",
555 &e,
556 ))
557 .into()
558 })
559 })
560 },
561 )
562 }
563
564 pub fn make_render_mermaid_closure() -> Closure<dyn Fn(String) -> js_sys::Promise> {
568 Closure::new(move |source: String| -> js_sys::Promise {
569 wasm_bindgen_futures::future_to_promise(async move {
570 crate::api::posts::render_mermaid_preview(source)
571 .await
572 .map(|html| js_sys::JsString::from(html).into())
573 .map_err(|e| {
574 js_sys::Error::new(&crate::utils::error::failure_message(
575 "图表渲染失败",
576 &e,
577 ))
578 .into()
579 })
580 })
581 })
582 }
583}
584
585#[cfg(target_arch = "wasm32")]
595pub use wasm::{
596 consume_upload_event, create, make_comment_upload_closure, make_render_mermaid_closure,
597 make_roundtrip_check_closure, make_run_code_closure, make_upload_closure, upload_image_file,
598 EditorHandle, EditorOptions, UploadEventJs,
599};