1#[derive(Clone, Copy, Default)]
12pub struct UploadsInFlight {
13 pub uploading: u32,
14 pub error: u32,
15}
16
17#[derive(Clone, PartialEq)]
19pub struct UploadErrorEntry {
20 pub id: String,
21 pub file_name: String,
22 pub message: String,
23}
24
25#[cfg(target_arch = "wasm32")]
30pub mod wasm {
31 use super::{UploadErrorEntry, UploadsInFlight};
32 use dioxus::prelude::WritableExt;
34 use wasm_bindgen::prelude::*;
35 use wasm_bindgen::JsCast;
36
37 #[wasm_bindgen]
44 extern "C" {
45 pub type TiptapEditorModule;
48
49 #[wasm_bindgen(method, catch)]
52 pub fn create(
53 this: &TiptapEditorModule,
54 container_id: &str,
55 opts: &EditorOptions,
56 ) -> Result<Option<EditorInstance>, JsValue>;
57 }
58
59 pub fn get_module() -> TiptapEditorModule {
66 let window = web_sys::window().expect("no window");
67 let val = js_sys::Reflect::get(&window, &"TiptapEditor".into())
68 .expect("window.TiptapEditor missing");
69 val.unchecked_into::<TiptapEditorModule>()
70 }
71
72 #[wasm_bindgen]
74 extern "C" {
75 pub type EditorInstance;
77
78 #[wasm_bindgen(method, js_name = getMarkdown)]
80 pub fn get_markdown(this: &EditorInstance) -> String;
81
82 #[wasm_bindgen(method, js_name = setMarkdown)]
84 pub fn set_markdown(this: &EditorInstance, content: &str);
85
86 #[wasm_bindgen(method, js_name = removeUploadByUploadId)]
88 pub fn remove_upload_by_upload_id(this: &EditorInstance, upload_id: &str) -> bool;
89
90 #[wasm_bindgen(method, js_name = insertUploading)]
93 pub fn insert_uploading(this: &EditorInstance, file: web_sys::File);
94
95 #[wasm_bindgen(method, js_name = insertImagesFromLibrary)]
99 pub fn insert_images_from_library(this: &EditorInstance, json: &str);
100
101 #[wasm_bindgen(method)]
103 pub fn destroy(this: &EditorInstance);
104 }
105
106 #[wasm_bindgen]
108 extern "C" {
109 pub type EditorOptions;
112
113 #[wasm_bindgen(constructor)]
115 pub fn new() -> EditorOptions;
116
117 #[wasm_bindgen(method, setter, js_name = placeholder)]
119 pub fn set_placeholder(this: &EditorOptions, v: &str);
120
121 #[wasm_bindgen(method, setter, js_name = variant)]
124 pub fn set_variant(this: &EditorOptions, v: &str);
125
126 #[wasm_bindgen(method, setter, js_name = onUpdate)]
128 pub fn set_on_update(this: &EditorOptions, cb: &Closure<dyn FnMut(String)>);
129
130 #[wasm_bindgen(method, setter, js_name = onImageUpload)]
133 pub fn set_on_image_upload(
134 this: &EditorOptions,
135 cb: &Closure<dyn Fn(web_sys::File) -> js_sys::Promise>,
136 );
137
138 #[wasm_bindgen(method, setter, js_name = onReady)]
140 pub fn set_on_ready(this: &EditorOptions, cb: &Closure<dyn FnMut()>);
141
142 #[wasm_bindgen(method, setter, js_name = onUploadEvent)]
144 pub fn set_on_upload_event(this: &EditorOptions, cb: &Closure<dyn FnMut(UploadEventJs)>);
145
146 #[wasm_bindgen(method, setter, js_name = onRunCode)]
149 pub fn set_on_run_code(
150 this: &EditorOptions,
151 cb: &Closure<dyn Fn(RunCodeOptsJs) -> js_sys::Promise>,
152 );
153
154 #[wasm_bindgen(method, setter, js_name = onPickFromLibrary)]
158 pub fn set_on_pick_from_library(this: &EditorOptions, cb: &Closure<dyn FnMut()>);
159 }
160
161 #[wasm_bindgen]
163 extern "C" {
164 #[derive(Clone)]
167 pub type UploadEventJs;
168
169 #[wasm_bindgen(method, getter)]
171 pub fn kind(this: &UploadEventJs) -> String;
172
173 #[wasm_bindgen(method, getter, js_name = uploadId)]
175 pub fn upload_id(this: &UploadEventJs) -> String;
176
177 #[wasm_bindgen(method, getter, js_name = fileName)]
179 pub fn file_name(this: &UploadEventJs) -> String;
180
181 #[wasm_bindgen(method, getter, js_name = errorMsg)]
183 pub fn error_msg(this: &UploadEventJs) -> Option<String>;
184
185 #[wasm_bindgen(method, getter)]
187 pub fn counts(this: &UploadEventJs) -> UploadCountsJs;
188 }
189
190 #[wasm_bindgen]
191 extern "C" {
192 #[derive(Clone)]
194 pub type UploadCountsJs;
195
196 #[wasm_bindgen(method, getter)]
198 pub fn uploading(this: &UploadCountsJs) -> u32;
199
200 #[wasm_bindgen(method, getter)]
202 pub fn error(this: &UploadCountsJs) -> u32;
203 }
204
205 #[wasm_bindgen]
209 extern "C" {
210 pub type RunCodeOptsJs;
211
212 #[wasm_bindgen(method, getter)]
214 pub fn language(this: &RunCodeOptsJs) -> String;
215
216 #[wasm_bindgen(method, getter)]
218 pub fn source(this: &RunCodeOptsJs) -> String;
219
220 #[wasm_bindgen(method, getter, js_name = overridesJson)]
223 pub fn overrides_json(this: &RunCodeOptsJs) -> String;
224 }
225
226 pub struct EditorHandle {
234 instance: EditorInstance,
235 _on_update: Closure<dyn FnMut(String)>,
236 _on_image_upload: Closure<dyn Fn(web_sys::File) -> js_sys::Promise>,
237 _on_ready: Closure<dyn FnMut()>,
238 _on_upload_event: Closure<dyn FnMut(UploadEventJs)>,
239 _on_run_code: Closure<dyn Fn(RunCodeOptsJs) -> js_sys::Promise>,
240 _on_pick_from_library: Closure<dyn FnMut()>,
241 }
242
243 impl EditorHandle {
244 pub fn new(
250 instance: EditorInstance,
251 on_update: Closure<dyn FnMut(String)>,
252 on_image_upload: Closure<dyn Fn(web_sys::File) -> js_sys::Promise>,
253 on_ready: Closure<dyn FnMut()>,
254 on_upload_event: Closure<dyn FnMut(UploadEventJs)>,
255 on_run_code: Closure<dyn Fn(RunCodeOptsJs) -> js_sys::Promise>,
256 on_pick_from_library: Closure<dyn FnMut()>,
257 ) -> Self {
258 Self {
259 instance,
260 _on_update: on_update,
261 _on_image_upload: on_image_upload,
262 _on_ready: on_ready,
263 _on_upload_event: on_upload_event,
264 _on_run_code: on_run_code,
265 _on_pick_from_library: on_pick_from_library,
266 }
267 }
268
269 pub fn instance(&self) -> &EditorInstance {
271 &self.instance
272 }
273
274 pub fn new_comment(
279 instance: EditorInstance,
280 on_update: Closure<dyn FnMut(String)>,
281 on_image_upload: Closure<dyn Fn(web_sys::File) -> js_sys::Promise>,
282 on_ready: Closure<dyn FnMut()>,
283 on_upload_event: Closure<dyn FnMut(UploadEventJs)>,
284 ) -> Self {
285 Self::new(
286 instance,
287 on_update,
288 on_image_upload,
289 on_ready,
290 on_upload_event,
291 Closure::new(|_opts: RunCodeOptsJs| js_sys::Promise::resolve(&JsValue::null())),
292 Closure::new(|| {}),
293 )
294 }
295 }
296
297 impl Drop for EditorHandle {
298 fn drop(&mut self) {
299 self.instance.destroy();
300 }
301 }
302
303 pub fn consume_upload_event(
315 ev: &UploadEventJs,
316 mut uploads_in_flight: dioxus::prelude::Signal<UploadsInFlight>,
317 mut upload_errors: dioxus::prelude::Signal<Vec<UploadErrorEntry>>,
318 ) {
319 let id = ev.upload_id();
320 match ev.kind().as_str() {
321 "error" => {
322 let msg = ev.error_msg().unwrap_or_else(|| "上传失败".to_string());
323 let mut errors = upload_errors.write();
325 if let Some(entry) = errors.iter_mut().find(|e| e.id == id) {
326 entry.message = msg;
327 } else {
328 errors.push(UploadErrorEntry {
329 id: id.clone(),
330 file_name: ev.file_name(),
331 message: msg,
332 });
333 }
334 }
335 "success" | "removed" => {
336 upload_errors.write().retain(|e| e.id != id);
337 }
338 _ => {}
339 }
340 let c = ev.counts();
342 uploads_in_flight.set(UploadsInFlight {
343 uploading: c.uploading(),
344 error: c.error(),
345 });
346 }
347
348 async fn upload_file_to(url: &str, file: web_sys::File) -> Result<String, String> {
364 let data = crate::utils::web_upload::post_multipart_file(url, "image", &file).await?;
365 let url = data["url"].as_str().unwrap_or("");
366 if url.is_empty() {
367 return Err("上传成功但未返回图片地址".to_string());
369 }
370 Ok(url.to_string())
371 }
372
373 pub async fn upload_image_file(file: web_sys::File) -> Result<String, String> {
375 upload_file_to("/api/upload", file).await
376 }
377
378 pub async fn upload_comment_image_file(file: web_sys::File) -> Result<String, String> {
380 upload_file_to("/api/comments/upload", file).await
381 }
382
383 pub fn make_upload_closure() -> Closure<dyn Fn(web_sys::File) -> js_sys::Promise> {
387 Closure::new(move |file: web_sys::File| -> js_sys::Promise {
388 wasm_bindgen_futures::future_to_promise(async move {
389 upload_image_file(file)
390 .await
391 .map(|url| js_sys::JsString::from(url).into())
392 .map_err(|msg| js_sys::Error::new(&msg).into())
393 })
394 })
395 }
396
397 pub fn make_comment_upload_closure() -> Closure<dyn Fn(web_sys::File) -> js_sys::Promise> {
400 Closure::new(move |file: web_sys::File| -> js_sys::Promise {
401 wasm_bindgen_futures::future_to_promise(async move {
402 upload_comment_image_file(file)
403 .await
404 .map(|url| js_sys::JsString::from(url).into())
405 .map_err(|msg| js_sys::Error::new(&msg).into())
406 })
407 })
408 }
409
410 fn format_run_result(task: &crate::api::code_runner::ExecTask) -> String {
414 use crate::api::code_runner::ExecStatus;
415 let status_label = match task.status {
416 ExecStatus::Success => "Success",
417 ExecStatus::Error => "Error",
418 ExecStatus::Timeout => "Timeout",
419 ExecStatus::OomKilled => "OOM",
420 ExecStatus::Failed => "Failed",
421 _ => "Unknown",
422 };
423 match &task.result {
424 Some(res) => {
425 let mut out = format!("状态: {} · 耗时: {}ms", status_label, res.duration_ms);
426 if !res.stdout.is_empty() {
427 out.push_str("\nStdout:\n");
428 out.push_str(&res.stdout);
429 }
430 if !res.stderr.is_empty() {
431 out.push_str("\nStderr:\n");
432 out.push_str(&res.stderr);
433 }
434 out
435 }
436 None => format!("状态: {} · {}", status_label, task.stage),
437 }
438 }
439
440 pub fn make_run_code_closure() -> Closure<dyn Fn(RunCodeOptsJs) -> js_sys::Promise> {
449 Closure::new(move |opts: RunCodeOptsJs| -> js_sys::Promise {
450 wasm_bindgen_futures::future_to_promise(async move {
451 use crate::api::code_runner::{execute, ExecRequest, ExecStatus};
452 use crate::infra::runner_config::ResourceLimits;
453
454 let language = opts.language();
455 let source = opts.source();
456 let overrides_json = opts.overrides_json();
457
458 let overrides = if overrides_json.trim().is_empty() {
460 None
461 } else {
462 match serde_json::from_str::<ResourceLimits>(&overrides_json) {
463 Ok(o) => Some(o),
464 Err(_) => None, }
466 };
467
468 let req = ExecRequest {
469 language,
470 source,
471 overrides,
472 };
473
474 match execute::start_exec(req).await {
475 Ok(task_id) => {
476 let poll_interval = 500;
477 for _ in 0..60 {
479 crate::utils::time::sleep_ms(poll_interval).await;
480 match execute::get_exec_result(task_id.clone()).await {
481 Ok(task) => {
482 let terminal = task.status != ExecStatus::Queued
483 && task.status != ExecStatus::Running;
484 if terminal {
485 let s = format_run_result(&task);
486 return Ok(js_sys::JsString::from(s).into());
487 }
488 }
489 Err(_) => {
490 return Err(js_sys::Error::new("结果获取异常").into());
491 }
492 }
493 }
494 Err(js_sys::Error::new("轮询超时,请重试").into())
495 }
496 Err(e) => Err(js_sys::Error::new(&e.to_string()).into()),
497 }
498 })
499 })
500 }
501}
502
503#[cfg(target_arch = "wasm32")]
506pub use wasm::{
507 consume_upload_event, get_module, make_comment_upload_closure, make_run_code_closure,
508 make_upload_closure, upload_image_file, EditorHandle, EditorOptions, UploadEventJs,
509};