yggdrasil/src/pages/admin/runner.rs
xfy 575b6b533e feat(code-runner): CodeRunner 挂载 CodeMirror 编辑器,支持双向绑定
兑现组件一直承诺但无人实现的「调用方挂载编辑器」契约——此前容器 div 只
输出 id,admin/runner 沙箱与文章正文 runnable 块都只能提交写死的初始源码,
作者/读者无法编辑。

- CodeRunner 在 WASM 端按自身 container_id 调 codemirror_bridge.create
  挂载编辑器,onChange 回写内部 source_signal(编辑器内容 = 唯一真源)
- run_code 改读 source_signal 当前值,提交编辑后内容而非初始快照
- 主题切换经 use_resolved_theme 订阅,热切换编辑器主题
- use_drop 销毁 EditorHandle,释放 CodeMirror 实例与 closure 槽位
- source prop 外部变更(admin 切换语言重置示例)同步进 signal + 编辑器
- admin/runner 页面 overrides 解析挪进 use_memo,避免 render 体写状态

范式镜像 SQL 控制台(system.rs)与 Tiptap 编辑器(write.rs)。
2026-07-05 14:32:39 +08:00

109 lines
4.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 管理后台「代码试运行」页面。
//!
//! 作者在写作时可在此沙箱快速试运行代码(验证围栏 ` ```lang runnable ` 的预期输出),
//! 而无需进入文章渲染后才能运行。沙箱使用与读者相同的 StartExec / GetExecResult
//! 接口受同一套资源钳制约束admin 跳过速率限制,见 `start_exec`)。
//!
//! 仅 WASM 前端交互;语言在受支持集合内切换。
use dioxus::prelude::*;
use crate::components::code_runner::CodeRunner;
use crate::components::ui::ADMIN_CARD_CLASS;
use crate::infra::runner_config::ResourceLimits;
/// 受支持的语言集合(与 LANGUAGES 注册表 / CODE_RUNNER_LANGUAGES 对齐)。
const SUPPORTED_LANGS: &[&str] = &["python", "node"];
/// 默认示例源码(按语言)。
fn default_source(lang: &str) -> String {
match lang {
"python" => "print('Hello from author sandbox')\nfor i in range(3):\n print(f'line {i}')\n".to_string(),
"node" => "console.log('Hello from author sandbox');\n[0,1,2].forEach(i => console.log(`line ${i}`));\n".to_string(),
_ => String::new(),
}
}
/// 管理后台代码试运行页面。
#[component]
#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut))]
pub fn Runner() -> Element {
let mut lang = use_signal(|| "python".to_string());
// 语言切换时刷新示例源码(首次进入也有默认值)。
let mut source = use_signal(|| default_source("python"));
let mut overrides_json = use_signal(String::new);
// overrides 解析用 use_memo 承载render 体只读不写Dioxus render purity
// 避免 render 期间 .set() override_error。畸形 JSON 标记在 memo 返回值里。
let parsed = use_memo(move || {
let raw = overrides_json();
match serde_json::from_str::<ResourceLimits>(raw.trim()) {
Ok(o) => (Some(o), String::new()),
Err(_) => {
if raw.trim().is_empty() {
(None, String::new())
} else {
(None, "overrides JSON 格式错误,已忽略".to_string())
}
}
}
});
let (overrides, override_error) = (parsed.read().0.clone(), parsed.read().1.clone());
rsx! {
div { class: "flex flex-col gap-6 max-w-4xl mx-auto w-full",
div { class: "flex flex-col gap-2",
h1 { class: "text-3xl font-extrabold tracking-tight", "代码试运行沙箱" }
p { class: "text-base text-base-content/60",
"在此快速试运行代码,验证文章中可运行代码块的预期输出。资源钳制与读者侧一致,速率限制对 admin 放行。"
}
}
div { class: "{ADMIN_CARD_CLASS} flex flex-col gap-3",
div { class: "flex flex-wrap items-center gap-3",
label { class: "text-sm font-semibold text-base-content/70", "语言" }
div { class: "join",
for l in SUPPORTED_LANGS {
button {
key: "{l}",
class: (if lang() == *l { "btn btn-sm join-item btn-primary" } else { "btn btn-sm join-item btn-ghost" }).to_string(),
onclick: {
let ll = (*l).to_string();
move |_| {
if ll != lang() {
lang.set(ll.clone());
source.set(default_source(&ll));
}
}
},
"{l}"
}
}
}
}
div { class: "flex flex-col gap-1",
label { class: "text-sm font-semibold text-base-content/70",
"资源覆盖 (JSON, 可选)"
}
input {
class: "input input-bordered input-sm font-mono w-full",
r#type: "text",
placeholder: "如 {{\"timeout_secs\":10,\"memory_mb\":512}}",
value: "{overrides_json()}",
oninput: move |e| overrides_json.set(e.value()),
}
if !override_error.is_empty() {
span { class: "text-xs text-error", "{override_error}" }
}
}
}
CodeRunner {
source: source(),
language: lang(),
overrides: overrides,
}
}
}
}