将 7 个 admin 页面中复制粘贴/各自为政的按钮 class 收敛到 ui.rs 令牌: 主操作色系统一为主题绿(bg-paper-accent): - dashboard/posts「发布文章」、write「发布/更新」+「确认」、runner 语言切换选中态 改用 BTN_PRIMARY / BTN_PRIMARY_SM(原为深色 bg-paper-primary,现统一为绿) - system 4 处刷新/执行/创建备份、trash 保存设置、write 发布按钮改用 LoadingButton 统一 loading 态为 spinner 绝对居中叠加(按钮宽度不变,原 system 用文字切换、 write 用整体变灰,现全部一致) 描边/图标/行内按钮统一: - posts 重建、system 刷新列表 → BTN_OUTLINE - trash 清空回收站 → BTN_DANGER_OUTLINE - trash 步进 ± 按钮(2 处重复字符串)→ BTN_ICON - write 关闭 × 按钮(2 处重复)→ BTN_CLOSE_ICON - system BackupRow 恢复/删除绕过令牌 → BTN_TEXT_AMBER / BTN_TEXT_RED (删除色从 red-600 校正为令牌的 red-500,与其他行内删除一致) 消除约 15 处内联 class 字符串重复,system.rs 4 种主操作变体收敛为 1 种。
125 lines
6.0 KiB
Rust
125 lines
6.0 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, BTN_PRIMARY_SM};
|
||
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: "w-full max-w-5xl mx-auto space-y-8",
|
||
// 页头:与 dashboard / posts / system 对齐(h1 text-4xl + 底部分割线)
|
||
div { class: "flex flex-col md:flex-row md:items-end justify-between gap-6 pb-8 border-b border-[var(--color-paper-border)]/50",
|
||
div {
|
||
h1 { class: "text-4xl font-extrabold tracking-tight text-[var(--color-paper-primary)]", "代码试运行沙箱" }
|
||
p { class: "text-base text-[var(--color-paper-secondary)] mt-2",
|
||
"在此快速试运行代码,验证文章中可运行代码块的预期输出。资源钳制与读者侧一致,速率限制对 admin 放行。"
|
||
}
|
||
}
|
||
}
|
||
|
||
// 配置卡片:语言切换 + 资源覆盖
|
||
div { class: "{ADMIN_CARD_CLASS} p-8 flex flex-col gap-6",
|
||
// 语言切换
|
||
div { class: "flex flex-col gap-2",
|
||
label { class: "text-sm font-medium text-[var(--color-paper-secondary)]", "语言" }
|
||
div { class: "flex gap-2",
|
||
for l in SUPPORTED_LANGS {
|
||
button {
|
||
key: "{l}",
|
||
class: (if lang() == *l {
|
||
BTN_PRIMARY_SM
|
||
} else {
|
||
"px-4 py-1.5 text-sm font-medium rounded-full text-[var(--color-paper-secondary)] bg-[var(--color-paper-theme)] hover:bg-[var(--color-paper-border)] hover:text-[var(--color-paper-primary)] transition cursor-pointer"
|
||
}).to_string(),
|
||
onclick: {
|
||
let ll = (*l).to_string();
|
||
move |_| {
|
||
if ll != lang() {
|
||
lang.set(ll.clone());
|
||
source.set(default_source(&ll));
|
||
}
|
||
}
|
||
},
|
||
"{l}"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 资源覆盖(JSON)
|
||
div { class: "flex flex-col gap-2",
|
||
label { class: "text-sm font-medium text-[var(--color-paper-secondary)]",
|
||
"资源覆盖 (JSON, 可选)"
|
||
}
|
||
input {
|
||
class: "w-full px-3 py-2 text-sm border border-paper-border rounded-lg bg-[var(--color-paper-theme)] text-[var(--color-paper-primary)] font-mono focus:outline-none focus:border-[var(--color-paper-accent)] transition-colors",
|
||
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() {
|
||
p { class: "text-xs text-red-500 dark:text-red-400", "{override_error}" }
|
||
} else {
|
||
p { class: "text-xs text-[var(--color-paper-tertiary)]",
|
||
"覆盖 cpu_cores / memory_mb / timeout_secs / output_bytes / allow_network;最终仍受 CODE_RUNNER_MAX_* 钳制"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 运行器
|
||
CodeRunner {
|
||
source: source(),
|
||
language: lang(),
|
||
overrides: overrides,
|
||
}
|
||
}
|
||
}
|
||
}
|