前端生成
- 作者仓库星标 70
- 许可证 AGPL-3.0
- 作者更新于 实时读取
- 作者仓库 lightfriend
- 领域
- 通用
- 兼容 Agent
-
- Claude Code
- Cursor
- Cline
- Codex
- Windsurf
- Gemini CLI
- +20
- 信任分
- 94 / 100 · 已通过审计
- 作者 / 版本 / 许可
- @ahtavarasmus · AGPL-3.0
- Token 消耗评级
- 低消耗
- 接入复杂程度
- 需简单配置
- 是否需要外部 API Key
- 不需要
- 兼容的系统
- 未声明(默认跨平台)
- 底层运行要求
- 无特殊要求
- 文件与系统权限
-
- 只读
- 允许写入 / 修改
- Shell 执行
- 网络行为
- 允许外网请求
- 安装命令数
- 26 条
档案由构建时根据 SKILL.md 与安装命令自动衍生,可能与作者实际意图存在差异。
需要注意: 未限定 allowed-tools,默认拥有全部工具权限。
---
name: lightfriend-add-frontend-page
description: Step-by-step guide for adding new pages to the Yew frontend This skill guides you through adding…
category: 通用
runtime: 无特殊运行时
---
# lightfriend-add-frontend-page 输出预览
## PART A: 任务判断
- 适用问题:通用任务拆解、检查和交付。
- 输入要求:目标材料、限制条件、期望输出和验收方式。
- 证据边界:围绕“Overview / Step-by-Step Process / 1. Create Page Component”读取原文规则,不把推断写成作者承诺。
## PART B: 执行结果
- **01** 任务判断:确认你的需求是否属于通用任务拆解、检查和交付,并标出输入、限制和预期结果。
- **02** 执行计划:优先按“Overview / Step-by-Step Process / 1. Create Page Component”拆成步骤,说明每一步会读取什么、修改什么、产出什么。
- **03** 交付结果:给出可复制的命令、文件改动、检查清单或内容草稿,并说明如何继续迭代。
- **04** 风险边界:结合 读取文件、写入/修改文件、执行终端命令、会按任务需要访问外部网络、通常不需要额外 API Key 给出执行前确认项。
## Running Rules
- 读取文件、写入/修改文件、执行终端命令;会按任务需要访问外部网络;通常不需要额外 API Key。
- 先小样例验证,再放大到真实任务。
- 交付时同时给结果、检查口径和下一步迭代建议。 原文没有稳定的斜杠命令要求。安装验证后通常全局生效,直接在对话里点名这个 Skill 并描述任务即可。
告诉 Agent 目标文件或材料、期望结果、不可改范围、是否允许联网或执行命令。本 Skill 的权限画像是:读取文件、写入/修改文件、执行终端命令。
先用一个小任务确认它会围绕“Overview / Step-by-Step Process / 1. Create Page Component”工作;涉及文件或命令时,先看 diff、日志、预览或测试结果。
检查最终产物是否包含明确结果、必要证据和下一步动作;如果输出泛泛而谈,就补充输入、边界和验收标准后重跑。
---
name: lightfriend-add-frontend-page
description: Step-by-step guide for adding new pages to the Yew frontend This skill guides you through adding…
category: 通用
source: ahtavarasmus/lightfriend
---
# lightfriend-add-frontend-page
## 什么时候使用
- 把通用方向的常用动作沉淀成 Agent 可调用的技能 适合处理通用任务拆解、检查、交付和复盘,核心价值是把输入、判断、执行、验证和交付边界固定下来,避免 Agent 泛泛回答。 把任务拆成可执行、可检查、可继续迭代的步骤;通常不需要额外…
- 面向通用任务拆解、检查和交付,优先处理能明确输入、步骤和验收标准的工作。
## 需要提供什么
- 目标材料、目录范围、期望结果和不可改动内容。
- 是否允许联网、执行命令、读写文件或调用外部服务。
## 执行规则
- 围绕「Overview / Step-by-Step Process / 1. Create Page Component」组织步骤,不把推断写成作者事实。
- 读取文件、写入/修改文件、执行终端命令;会按任务需要访问外部网络;通常不需要额外 API Key。
- 先跑小样例,确认结果可检查后再扩大任务范围。
## 输出要求
- 给出最终产物、关键证据、验证方式和下一步动作。
- 信息不足时标记 unknown,不编造命令、平台或依赖。 作者原文负责流程事实;仓库文件负责来源和命令;流狐只补充适用场景、限制和质量判断。
skill "lightfriend-add-frontend-page" {
输入层 -> 用户目标 + 目标文件 + 禁止范围 + 验收标准
上下文层 -> Overview / Step-by-Step Process / 1. Create Page Component
规则层 -> SKILL.md 触发条件 / 执行顺序 / 输出格式
运行层 -> 无特殊运行时 | 读取文件、写入/修改文件、执行终端命令 | 会按任务需要访问外部网络
安全层 -> 通常不需要额外 API Key + 小任务验证 + diff / 日志复核
输出层 -> 可复制结果 + 检查清单 + 下一步迭代
} Adding a New Frontend Page
This skill guides you through adding a new page to the Lightfriend Yew WebAssembly frontend.
Overview
A complete page includes:
- Page component in
frontend/src/pages/ - Route enum variant in
main.rs - Route handler in switch function
- Navigation link (if applicable)
Step-by-Step Process
1. Create Page Component
Create frontend/src/pages/{page_name}.rs:
use yew::prelude::*;
use gloo_net::http::Request;
use crate::config;
#[function_component(PageName)]
pub fn page_name() -> Html {
// State management
let data = use_state(|| None::<SomeData>);
let loading = use_state(|| true);
let error = use_state(|| None::<String>);
// Load data on mount
{
let data = data.clone();
let loading = loading.clone();
let error = error.clone();
use_effect_with((), move |_| {
wasm_bindgen_futures::spawn_local(async move {
match fetch_data().await {
Ok(result) => {
data.set(Some(result));
loading.set(false);
}
Err(e) => {
error.set(Some(e.to_string()));
loading.set(false);
}
}
});
});
}
html! {
<div class="page-container">
<h1>{"Page Title"}</h1>
if *loading {
<p>{"Loading..."}</p>
} else if let Some(err) = (*error).clone() {
<p class="error">{err}</p>
} else if let Some(content) = (*data).clone() {
// Render content
<div>
{format!("Content: {:?}", content)}
</div>
}
</div>
}
}
// Helper functions
async fn fetch_data() -> Result<SomeData, Box<dyn std::error::Error>> {
let token = /* get from context or local storage */;
let backend_url = config::get_backend_url();
let response = Request::get(&format!("{}/api/endpoint", backend_url))
.header("Authorization", &format!("Bearer {}", token))
.send()
.await?
.json::<SomeData>()
.await?;
Ok(response)
}
#[derive(Clone, serde::Deserialize, serde::Serialize)]
struct SomeData {
// Define your data structure
}
2. Add Module Declaration
CRITICAL: This codebase does NOT use mod.rs files!
Instead, add the module declaration to the inline mod pages { } block in frontend/src/main.rs:
mod pages {
pub mod home;
pub mod landing;
pub mod {page_name}; // Add your new page here
// ... other pages
}
NEVER create a mod.rs file - this is a common mistake. Lightfriend uses named module files (e.g., home.rs, landing.rs) and declares them in the inline module block in main.rs.
3. Add Route Variant
In frontend/src/main.rs, add a route variant to the Route enum:
#[derive(Clone, Routable, PartialEq)]
pub enum Route {
#[at("/")]
Home,
#[at("/page-name")]
PageName,
// ... other routes
#[not_found]
#[at("/404")]
NotFound,
}
4. Add Route Handler
In the switch() function in frontend/src/main.rs, add:
fn switch(route: Route) -> Html {
match route {
Route::Home => html! { <Home /> },
Route::PageName => html! { <PageName /> },
// ... other routes
Route::NotFound => html! { <h1>{"404 - Page Not Found"}</h1> },
}
}
5. Add Navigation Link (Optional)
If the page should appear in navigation, add to the Nav component in frontend/src/main.rs:
#[function_component(Nav)]
fn nav() -> Html {
html! {
<nav>
<Link<Route> to={Route::Home}>{"Home"}</Link<Route>>
<Link<Route> to={Route::PageName}>{"Page Name"}</Link<Route>>
// ... other links
</nav>
}
}
6. Test the Page
cd frontend && trunk serve
Navigate to http://localhost:8080/page-name
Common Patterns
Protected Routes (Require Auth)
use yew_hooks::use_local_storage;
#[function_component(ProtectedPage)]
pub fn protected_page() -> Html {
let token = use_local_storage::<String>("token".to_string());
if token.is_none() {
// Redirect to login
let navigator = use_navigator().unwrap();
navigator.push(&Route::Login);
return html! {};
}
html! {
<div>{"Protected content"}</div>
}
}
Page with Form
use web_sys::HtmlInputElement;
#[function_component(FormPage)]
pub fn form_page() -> Html {
let name_ref = use_node_ref();
let email_ref = use_node_ref();
let submitting = use_state(|| false);
let on_submit = {
let name_ref = name_ref.clone();
let email_ref = email_ref.clone();
let submitting = submitting.clone();
Callback::from(move |e: SubmitEvent| {
e.prevent_default();
let submitting = submitting.clone();
let name = name_ref.cast::<HtmlInputElement>()
.unwrap()
.value();
let email = email_ref.cast::<HtmlInputElement>()
.unwrap()
.value();
submitting.set(true);
wasm_bindgen_futures::spawn_local(async move {
match submit_form(name, email).await {
Ok(_) => {
// Handle success
}
Err(e) => {
// Handle error
}
}
submitting.set(false);
});
})
};
html! {
<form onsubmit={on_submit}>
<input
ref={name_ref}
type="text"
placeholder="Name"
required=true
/>
<input
ref={email_ref}
type="email"
placeholder="Email"
required=true
/>
<button type="submit" disabled={*submitting}>
if *submitting {
{"Submitting..."}
} else {
{"Submit"}
}
</button>
</form>
}
}
async fn submit_form(name: String, email: String) -> Result<(), Box<dyn std::error::Error>> {
let token = /* get from context */;
Request::post(&format!("{}/api/submit", config::get_backend_url()))
.header("Authorization", &format!("Bearer {}", token))
.json(&serde_json::json!({
"name": name,
"email": email,
}))?
.send()
.await?;
Ok(())
}
Page with Context
use yew::prelude::*;
#[derive(Clone, PartialEq)]
pub struct UserContext {
pub user_id: i32,
pub email: String,
}
#[function_component(ContextPage)]
pub fn context_page() -> Html {
let user_ctx = use_context::<UserContext>()
.expect("UserContext not found");
html! {
<div>
<p>{format!("User ID: {}", user_ctx.user_id)}</p>
<p>{format!("Email: {}", user_ctx.email)}</p>
</div>
}
}
Page with Route Parameters
#[derive(Clone, Routable, PartialEq)]
pub enum Route {
#[at("/users/:id")]
UserDetail { id: i32 },
}
#[derive(Properties, PartialEq)]
pub struct UserDetailProps {
pub id: i32,
}
#[function_component(UserDetail)]
pub fn user_detail(props: &UserDetailProps) -> Html {
let user_data = use_state(|| None::<User>);
{
let user_data = user_data.clone();
let user_id = props.id;
use_effect_with(user_id, move |_| {
wasm_bindgen_futures::spawn_local(async move {
if let Ok(user) = fetch_user(user_id).await {
user_data.set(Some(user));
}
});
});
}
html! {
<div>
if let Some(user) = (*user_data).clone() {
<h1>{user.name}</h1>
}
</div>
}
}
// In switch function:
fn switch(route: Route) -> Html {
match route {
Route::UserDetail { id } => html! { <UserDetail id={id} /> },
// ...
}
}
Page with Multiple API Calls
#[function_component(DashboardPage)]
pub fn dashboard_page() -> Html {
let stats = use_state(|| None::<Stats>);
let activity = use_state(|| None::<Vec<Activity>>);
let loading = use_state(|| true);
{
let stats = stats.clone();
let activity = activity.clone();
let loading = loading.clone();
use_effect_with((), move |_| {
wasm_bindgen_futures::spawn_local(async move {
// Fetch multiple endpoints in parallel
let (stats_result, activity_result) = tokio::join!(
fetch_stats(),
fetch_activity()
);
if let Ok(s) = stats_result {
stats.set(Some(s));
}
if let Ok(a) = activity_result {
activity.set(Some(a));
}
loading.set(false);
});
});
}
html! {
<div>
if *loading {
<p>{"Loading dashboard..."}</p>
} else {
<div>
{render_stats(&stats)}
{render_activity(&activity)}
</div>
}
</div>
}
}
Styling
Lightfriend uses CSS style blocks within the html! macro, NOT inline Tailwind classes.
Common pattern:
html! {
<div class="page-container">
<h1 class="page-title">{"Title"}</h1>
<div class="content-grid">
<div class="card">
{"Card content"}
</div>
</div>
<style>
{r#"
.page-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.page-title {
font-size: 2rem;
font-weight: bold;
margin-bottom: 1rem;
}
.content-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
}
.card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 1rem;
}
"#}
</style>
</div>
}
Testing Checklist
- Page renders without errors
- Route works in browser
- Navigation link works (if added)
- API calls succeed
- Loading states display correctly
- Error states display correctly
- Authentication checks work (if protected)
- Mobile responsive (if applicable)
File Reference
- Page components:
frontend/src/pages/{page}.rs - Routes:
frontend/src/main.rs(Route enum + switch function) - Navigation:
frontend/src/main.rs(Nav component) - Shared components:
frontend/src/components/ - Backend config:
frontend/src/config.rs
先判断是否适合
作者设计意图
作者的方法与取舍
边界和复核