diff --git a/src/components/admin_layout.rs b/src/components/admin_layout.rs index 1f5635a..02e9d34 100644 --- a/src/components/admin_layout.rs +++ b/src/components/admin_layout.rs @@ -46,6 +46,7 @@ pub fn AdminLayout() -> Element { (Route::Write {}, "写文章"), (Route::Posts {}, "管理文章"), (Route::Trash {}, "回收站"), + (Route::Runner {}, "试运行"), (Route::System {}, "系统"), ]; diff --git a/src/pages/admin/mod.rs b/src/pages/admin/mod.rs index 4437d96..7ba9d34 100644 --- a/src/pages/admin/mod.rs +++ b/src/pages/admin/mod.rs @@ -8,6 +8,8 @@ pub mod comments; pub mod dashboard; /// 文章管理列表页面模块。 pub mod posts; +/// 代码试运行沙箱页面模块。 +pub mod runner; /// 系统管理页面模块(数据库 + 服务器状态 + SQL 控制台 + 导出 + 备份)。 pub mod system; /// 回收站管理页面模块。 @@ -21,6 +23,8 @@ pub use comments::{AdminComments, AdminCommentsPage}; pub use dashboard::Admin; /// 文章管理列表组件(带默认分页)。 pub use posts::{Posts, PostsPage}; +/// 代码试运行沙箱组件。 +pub use runner::Runner; /// 系统管理入口组件。 pub use system::System; /// 回收站管理组件(带默认分页)。 diff --git a/src/pages/admin/runner.rs b/src/pages/admin/runner.rs new file mode 100644 index 0000000..c05b0b7 --- /dev/null +++ b/src/pages/admin/runner.rs @@ -0,0 +1,112 @@ +//! 管理后台「代码试运行」页面。 +//! +//! 作者在写作时可在此沙箱快速试运行代码(验证围栏 ` ```lang runnable ` 的预期输出), +//! 而无需进入文章渲染后才能运行。沙箱使用与读者相同的 StartExec / GetExecResult +//! 接口,受同一套资源钳制与速率限制约束。 +//! +//! 仅 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()); + let mut override_error = use_signal(|| String::new()); + + let current_source = source(); + let current_lang = lang(); + let overrides = match serde_json::from_str::(overrides_json().trim()) { + Ok(o) => { + // 解析成功时清掉错误。 + override_error.set(String::new()); + Some(o) + } + Err(_) => { + // 空字符串视为 None(不报错);非空且畸形才提示。 + if overrides_json().trim().is_empty() { + override_error.set(String::new()); + None + } else { + override_error.set("overrides JSON 格式错误,已忽略".to_string()); + None + } + } + }; + + 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", + "在此快速试运行代码,验证文章中可运行代码块的预期输出。受同一套资源钳制与速率限制约束。" + } + } + + 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: current_source, + language: current_lang, + overrides: overrides, + } + } + } +} diff --git a/src/router.rs b/src/router.rs index e4e90c8..feda103 100644 --- a/src/router.rs +++ b/src/router.rs @@ -12,8 +12,8 @@ use crate::components::frontend_layout::FrontendLayout; use crate::context::UserContext; use crate::pages::about::About; use crate::pages::admin::{ - Admin, AdminComments, AdminCommentsPage, Posts, PostsPage, System, Trash, TrashPage, Write, - WriteEdit, + Admin, AdminComments, AdminCommentsPage, Posts, PostsPage, Runner, System, Trash, TrashPage, + Write, WriteEdit, }; use crate::pages::archives::Archives; use crate::pages::home::{Home, HomePage}; @@ -94,6 +94,9 @@ pub enum Route { /// 系统管理(数据库 + 服务器状态 + SQL 控制台 + 导出 + 备份) #[route("/system")] System {}, + /// 代码试运行沙箱(作者预览可运行代码块输出) + #[route("/runner")] + Runner {}, #[end_layout] #[end_nest]