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)]
92 pub fn destroy(this: &EditorInstance);
93 }
94
95 #[wasm_bindgen]
97 extern "C" {
98 pub type EditorOptions;
101
102 #[wasm_bindgen(constructor)]
104 pub fn new() -> EditorOptions;
105
106 #[wasm_bindgen(method, setter, js_name = placeholder)]
108 pub fn set_placeholder(this: &EditorOptions, v: &str);
109
110 #[wasm_bindgen(method, setter, js_name = onUpdate)]
112 pub fn set_on_update(this: &EditorOptions, cb: &Closure<dyn FnMut(String)>);
113
114 #[wasm_bindgen(method, setter, js_name = onImageUpload)]
117 pub fn set_on_image_upload(
118 this: &EditorOptions,
119 cb: &Closure<dyn Fn(web_sys::File) -> js_sys::Promise>,
120 );
121
122 #[wasm_bindgen(method, setter, js_name = onReady)]
124 pub fn set_on_ready(this: &EditorOptions, cb: &Closure<dyn FnMut()>);
125
126 #[wasm_bindgen(method, setter, js_name = onUploadEvent)]
128 pub fn set_on_upload_event(this: &EditorOptions, cb: &Closure<dyn FnMut(UploadEventJs)>);
129
130 #[wasm_bindgen(method, setter, js_name = onRunCode)]
133 pub fn set_on_run_code(
134 this: &EditorOptions,
135 cb: &Closure<dyn Fn(RunCodeOptsJs) -> js_sys::Promise>,
136 );
137 }
138
139 #[wasm_bindgen]
141 extern "C" {
142 #[derive(Clone)]
145 pub type UploadEventJs;
146
147 #[wasm_bindgen(method, getter)]
149 pub fn kind(this: &UploadEventJs) -> String;
150
151 #[wasm_bindgen(method, getter, js_name = uploadId)]
153 pub fn upload_id(this: &UploadEventJs) -> String;
154
155 #[wasm_bindgen(method, getter, js_name = fileName)]
157 pub fn file_name(this: &UploadEventJs) -> String;
158
159 #[wasm_bindgen(method, getter, js_name = errorMsg)]
161 pub fn error_msg(this: &UploadEventJs) -> Option<String>;
162
163 #[wasm_bindgen(method, getter)]
165 pub fn counts(this: &UploadEventJs) -> UploadCountsJs;
166 }
167
168 #[wasm_bindgen]
169 extern "C" {
170 #[derive(Clone)]
172 pub type UploadCountsJs;
173
174 #[wasm_bindgen(method, getter)]
176 pub fn uploading(this: &UploadCountsJs) -> u32;
177
178 #[wasm_bindgen(method, getter)]
180 pub fn error(this: &UploadCountsJs) -> u32;
181 }
182
183 #[wasm_bindgen]
187 extern "C" {
188 pub type RunCodeOptsJs;
189
190 #[wasm_bindgen(method, getter)]
192 pub fn language(this: &RunCodeOptsJs) -> String;
193
194 #[wasm_bindgen(method, getter)]
196 pub fn source(this: &RunCodeOptsJs) -> String;
197
198 #[wasm_bindgen(method, getter, js_name = overridesJson)]
201 pub fn overrides_json(this: &RunCodeOptsJs) -> String;
202 }
203
204 pub struct EditorHandle {
212 instance: EditorInstance,
213 _on_update: Closure<dyn FnMut(String)>,
214 _on_image_upload: Closure<dyn Fn(web_sys::File) -> js_sys::Promise>,
215 _on_ready: Closure<dyn FnMut()>,
216 _on_upload_event: Closure<dyn FnMut(UploadEventJs)>,
217 _on_run_code: Closure<dyn Fn(RunCodeOptsJs) -> js_sys::Promise>,
218 }
219
220 impl EditorHandle {
221 pub fn new(
227 instance: EditorInstance,
228 on_update: Closure<dyn FnMut(String)>,
229 on_image_upload: Closure<dyn Fn(web_sys::File) -> js_sys::Promise>,
230 on_ready: Closure<dyn FnMut()>,
231 on_upload_event: Closure<dyn FnMut(UploadEventJs)>,
232 on_run_code: Closure<dyn Fn(RunCodeOptsJs) -> js_sys::Promise>,
233 ) -> Self {
234 Self {
235 instance,
236 _on_update: on_update,
237 _on_image_upload: on_image_upload,
238 _on_ready: on_ready,
239 _on_upload_event: on_upload_event,
240 _on_run_code: on_run_code,
241 }
242 }
243
244 pub fn instance(&self) -> &EditorInstance {
246 &self.instance
247 }
248 }
249
250 impl Drop for EditorHandle {
251 fn drop(&mut self) {
252 self.instance.destroy();
253 }
254 }
255
256 pub fn consume_upload_event(
268 ev: &UploadEventJs,
269 mut uploads_in_flight: dioxus::prelude::Signal<UploadsInFlight>,
270 mut upload_errors: dioxus::prelude::Signal<Vec<UploadErrorEntry>>,
271 ) {
272 let id = ev.upload_id();
273 match ev.kind().as_str() {
274 "error" => {
275 let msg = ev.error_msg().unwrap_or_else(|| "上传失败".to_string());
276 let mut errors = upload_errors.write();
278 if let Some(entry) = errors.iter_mut().find(|e| e.id == id) {
279 entry.message = msg;
280 } else {
281 errors.push(UploadErrorEntry {
282 id: id.clone(),
283 file_name: ev.file_name(),
284 message: msg,
285 });
286 }
287 }
288 "success" | "removed" => {
289 upload_errors.write().retain(|e| e.id != id);
290 }
291 _ => {}
292 }
293 let c = ev.counts();
295 uploads_in_flight.set(UploadsInFlight {
296 uploading: c.uploading(),
297 error: c.error(),
298 });
299 }
300
301 pub async fn upload_image_file(file: web_sys::File) -> Result<String, String> {
316 let form = web_sys::FormData::new().map_err(|_| "无法构造上传表单".to_string())?;
318 form.append_with_blob("image", &file)
319 .map_err(|_| "无法附加文件".to_string())?;
320
321 let init = web_sys::RequestInit::new();
323 init.set_method("POST");
324 init.set_body(form.as_ref());
326 init.set_credentials(web_sys::RequestCredentials::SameOrigin);
327
328 let request = web_sys::Request::new_with_str_and_init("/api/upload", &init)
329 .map_err(|_| "无法构造上传请求".to_string())?;
330
331 let window = web_sys::window().expect("no window");
332 let promise = window.fetch_with_request(&request);
333
334 let resp_val = wasm_bindgen_futures::JsFuture::from(promise)
336 .await
337 .map_err(|e| format!("上传请求失败: {:?}", e))?;
338 let resp: web_sys::Response = resp_val
339 .dyn_into()
340 .map_err(|_| "上传响应类型异常".to_string())?;
341
342 let text_promise = resp.text().map_err(|e| format!("读取响应失败: {:?}", e))?;
344 let text_val = wasm_bindgen_futures::JsFuture::from(text_promise)
345 .await
346 .map_err(|e| format!("读取响应失败: {:?}", e))?;
347 let text = text_val.as_string().unwrap_or_default();
348
349 let data: serde_json::Value =
351 serde_json::from_str(&text).unwrap_or(serde_json::Value::Null);
352
353 if data["success"].as_bool() == Some(true) {
354 let url = data["url"].as_str().unwrap_or("");
355 if !url.is_empty() {
356 Ok(url.to_string())
357 } else {
358 Err("上传成功但未返回图片地址".to_string())
360 }
361 } else {
362 Err(data["error"]
364 .as_str()
365 .map(|s| s.to_string())
366 .unwrap_or_else(|| format!("上传失败: {}", resp.status())))
367 }
368 }
369
370 pub fn make_upload_closure() -> Closure<dyn Fn(web_sys::File) -> js_sys::Promise> {
374 Closure::new(move |file: web_sys::File| -> js_sys::Promise {
375 wasm_bindgen_futures::future_to_promise(async move {
376 upload_image_file(file)
377 .await
378 .map(|url| js_sys::JsString::from(url).into())
379 .map_err(|msg| js_sys::Error::new(&msg).into())
380 })
381 })
382 }
383
384 fn format_run_result(task: &crate::api::code_runner::ExecTask) -> String {
388 use crate::api::code_runner::ExecStatus;
389 let status_label = match task.status {
390 ExecStatus::Success => "Success",
391 ExecStatus::Error => "Error",
392 ExecStatus::Timeout => "Timeout",
393 ExecStatus::OomKilled => "OOM",
394 ExecStatus::Failed => "Failed",
395 _ => "Unknown",
396 };
397 match &task.result {
398 Some(res) => {
399 let mut out = format!("状态: {} · 耗时: {}ms", status_label, res.duration_ms);
400 if !res.stdout.is_empty() {
401 out.push_str("\nStdout:\n");
402 out.push_str(&res.stdout);
403 }
404 if !res.stderr.is_empty() {
405 out.push_str("\nStderr:\n");
406 out.push_str(&res.stderr);
407 }
408 out
409 }
410 None => format!("状态: {} · {}", status_label, task.stage),
411 }
412 }
413
414 pub fn make_run_code_closure() -> Closure<dyn Fn(RunCodeOptsJs) -> js_sys::Promise> {
423 Closure::new(move |opts: RunCodeOptsJs| -> js_sys::Promise {
424 wasm_bindgen_futures::future_to_promise(async move {
425 use crate::api::code_runner::{execute, ExecRequest, ExecStatus};
426 use crate::infra::runner_config::ResourceLimits;
427
428 let language = opts.language();
429 let source = opts.source();
430 let overrides_json = opts.overrides_json();
431
432 let overrides = if overrides_json.trim().is_empty() {
434 None
435 } else {
436 match serde_json::from_str::<ResourceLimits>(&overrides_json) {
437 Ok(o) => Some(o),
438 Err(_) => None, }
440 };
441
442 let req = ExecRequest {
443 language,
444 source,
445 overrides,
446 };
447
448 match execute::start_exec(req).await {
449 Ok(task_id) => {
450 let poll_interval = 500;
451 for _ in 0..60 {
453 crate::utils::time::sleep_ms(poll_interval).await;
454 match execute::get_exec_result(task_id.clone()).await {
455 Ok(task) => {
456 let terminal = task.status != ExecStatus::Queued
457 && task.status != ExecStatus::Running;
458 if terminal {
459 let s = format_run_result(&task);
460 return Ok(js_sys::JsString::from(s).into());
461 }
462 }
463 Err(_) => {
464 return Err(js_sys::Error::new("结果获取异常").into());
465 }
466 }
467 }
468 Err(js_sys::Error::new("轮询超时,请重试").into())
469 }
470 Err(e) => Err(js_sys::Error::new(&e.to_string()).into()),
471 }
472 })
473 })
474 }
475}
476
477#[cfg(target_arch = "wasm32")]
480pub use wasm::{
481 consume_upload_event, get_module, make_run_code_closure, make_upload_closure,
482 upload_image_file, EditorHandle, EditorOptions, UploadEventJs,
483};