兑现组件一直承诺但无人实现的「调用方挂载编辑器」契约——此前容器 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)。
109 lines
4.7 KiB
Rust
109 lines
4.7 KiB
Rust
//! 管理后台「代码试运行」页面。
|
||
//!
|
||
//! 作者在写作时可在此沙箱快速试运行代码(验证围栏 ` ```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,
|
||
}
|
||
}
|
||
}
|
||
}
|