use crate::db; use rusqlite::params; use serde::{Deserialize, Serialize}; use std::io::{BufRead, BufReader}; use std::path::Path; use std::process::Stdio; use uuid::Uuid; // ── 数据结构 ────────────────────────────────────────────────────────────────── #[derive(Serialize, Deserialize, Clone)] pub struct TemplateFile { pub id: String, pub template_id: String, pub path: String, pub content: String, } #[derive(Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct ProjectTemplate { pub id: String, pub name: String, pub description: Option, pub platform: String, // "win" | "wsl" | "both" pub tech_stack: Option, pub init_commands: Option, pub is_builtin: bool, pub created_at: Option, #[serde(default)] pub files: Vec, /// JSON 数组,格式:[{label, description, initCommands}] /// 存在时前端显示下拉让用户选择初始化方式 pub variants: Option, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct TemplateFileInput { pub path: String, pub content: String, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct SaveTemplateInput { pub id: Option, pub name: String, pub description: Option, pub platform: String, pub tech_stack: Option, pub init_commands: Option, pub files: Vec, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct CreateProjectResult { pub project_id: String, pub logs: Vec, } // ── Tauri 命令 ──────────────────────────────────────────────────────────────── #[tauri::command] pub fn list_templates() -> Result, String> { let conn = db::pool().get().map_err(|e| e.to_string())?; let mut stmt = conn .prepare( "SELECT id, name, description, platform, tech_stack, init_commands, is_builtin, created_at, variants FROM project_templates ORDER BY is_builtin DESC, name", ) .map_err(|e| e.to_string())?; let templates = stmt .query_map([], |row| { Ok(ProjectTemplate { id: row.get(0)?, name: row.get(1)?, description: row.get(2)?, platform: row.get(3)?, tech_stack: row.get(4)?, init_commands: row.get(5)?, is_builtin: row.get::<_, i64>(6)? == 1, created_at: row.get(7)?, files: vec![], variants: row.get(8)?, }) }) .map_err(|e| e.to_string())? .collect::, _>>() .map_err(|e| e.to_string())?; Ok(templates) } #[tauri::command] pub fn get_template(id: String) -> Result { let conn = db::pool().get().map_err(|e| e.to_string())?; let mut tmpl = conn .query_row( "SELECT id, name, description, platform, tech_stack, init_commands, is_builtin, created_at, variants FROM project_templates WHERE id = ?1", params![id], |row| { Ok(ProjectTemplate { id: row.get(0)?, name: row.get(1)?, description: row.get(2)?, platform: row.get(3)?, tech_stack: row.get(4)?, init_commands: row.get(5)?, is_builtin: row.get::<_, i64>(6)? == 1, created_at: row.get(7)?, files: vec![], variants: row.get(8)?, }) }, ) .map_err(|e| format!("模板不存在: {e}"))?; let mut file_stmt = conn .prepare( "SELECT id, template_id, path, content FROM project_template_files WHERE template_id = ?1 ORDER BY path", ) .map_err(|e| e.to_string())?; tmpl.files = file_stmt .query_map(params![id], |row| { Ok(TemplateFile { id: row.get(0)?, template_id: row.get(1)?, path: row.get(2)?, content: row.get(3)?, }) }) .map_err(|e| e.to_string())? .collect::, _>>() .map_err(|e| e.to_string())?; Ok(tmpl) } #[tauri::command] pub fn save_template(input: SaveTemplateInput) -> Result { let conn = db::pool().get().map_err(|e| e.to_string())?; let id = input.id.unwrap_or_else(|| Uuid::new_v4().to_string()); conn.execute( "INSERT INTO project_templates (id, name, description, platform, tech_stack, init_commands, is_builtin) VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0) ON CONFLICT(id) DO UPDATE SET name = excluded.name, description = excluded.description, platform = excluded.platform, tech_stack = excluded.tech_stack, init_commands = excluded.init_commands", params![id, input.name, input.description, input.platform, input.tech_stack, input.init_commands], ) .map_err(|e| e.to_string())?; // 全量替换文件列表 conn.execute( "DELETE FROM project_template_files WHERE template_id = ?1", params![id], ) .map_err(|e| e.to_string())?; for file in &input.files { let file_id = Uuid::new_v4().to_string(); conn.execute( "INSERT INTO project_template_files (id, template_id, path, content) VALUES (?1, ?2, ?3, ?4)", params![file_id, id, file.path, file.content], ) .map_err(|e| e.to_string())?; } Ok(id) } #[tauri::command] pub fn delete_template(id: String) -> Result<(), String> { let conn = db::pool().get().map_err(|e| e.to_string())?; conn.execute( "DELETE FROM project_templates WHERE id = ?1 AND is_builtin = 0", params![id], ) .map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] pub fn create_project_from_template( app: tauri::AppHandle, template_id: String, parent_path: String, project_name: String, platform: String, space_id: Option, variant_init_commands: Option, ) -> Result { use tauri::Emitter; let mut tmpl = get_template(template_id)?; // 如果用户选择了 variant,用其 initCommands 覆盖模板默认值 if let Some(v) = variant_init_commands { tmpl.init_commands = Some(v); } let mut logs: Vec = Vec::new(); // 辅助:同时 emit 事件 + 记录到 logs macro_rules! log_push { ($msg:expr) => {{ let m: String = $msg; let _ = app.emit("template:progress", &m); logs.push(m); }}; } // ── 1. 计算项目路径 ─────────────────────────────────────────────────────── let win_parent = crate::commands::publish::wsl_to_win(&parent_path); let project_win_path = format!( "{}\\{}", win_parent.trim_end_matches('\\'), project_name ); let project_root = Path::new(&project_win_path); if project_root.exists() { return Err(format!("目录已存在: {}", project_win_path)); } // ── 2. 创建根目录 ───────────────────────────────────────────────────────── let _ = app.emit("template:progress", "__step:1"); std::fs::create_dir_all(project_root) .map_err(|e| format!("创建目录失败: {e}"))?; log_push!(format!("📁 已创建目录: {}", project_win_path)); // ── 3. 写入模板文件 ─────────────────────────────────────────────────────── for file in &tmpl.files { if file.path.trim().is_empty() { continue; } let content = apply_vars(&file.content, &project_name); let file_path = project_root.join(&file.path); if let Some(parent) = file_path.parent() { std::fs::create_dir_all(parent) .map_err(|e| format!("创建子目录失败 {}: {e}", file.path))?; } std::fs::write(&file_path, content) .map_err(|e| format!("写入 {} 失败: {e}", file.path))?; log_push!(format!("📄 {}", file.path)); } // ── 4. 执行初始化命令 ───────────────────────────────────────────────────── let _ = app.emit("template:progress", "__step:2"); if let Some(cmds) = &tmpl.init_commands { let linux_project_path = if platform == "wsl" { crate::commands::publish::extract_linux_path(&parent_path, &win_parent) .map(|p| format!("{}/{}", p.trim_end_matches('/'), project_name)) } else { None }; for line in cmds.lines() { let cmd = line.trim(); if cmd.is_empty() || cmd.starts_with('#') { continue; } let _ = app.emit("template:progress", &format!("__cmd:{}", cmd)); log_push!(format!("⚙ 执行: {}", cmd)); // 用 spawn 流式读 stdout,实时推送每行输出 let spawn_result = if platform == "wsl" { let lp = linux_project_path.as_deref().unwrap_or(&project_win_path); crate::silent_cmd("wsl") .args(["bash", "-c", &format!("cd '{}' && {}", lp, cmd)]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() } else { crate::silent_cmd("cmd") .args(["/c", &format!("chcp 65001 > nul && {}", cmd)]) .current_dir(&project_win_path) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() }; match spawn_result { Ok(mut child) => { // 流式读 stdout,每行立即 emit if let Some(stdout) = child.stdout.take() { for line in BufReader::new(stdout).lines() { if let Ok(line) = line { let line = line.trim().to_string(); if !line.is_empty() { log_push!(line); } } } } // 等进程结束,收集 stderr let output = child.wait_with_output().map_err(|e| e.to_string())?; let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); if !stderr.is_empty() { log_push!(format!("⚠ {}", stderr)); } if output.status.success() { log_push!(" ✓".to_string()); } else { log_push!(format!(" ✗ 退出码: {}", output.status)); } } Err(e) => { log_push!(format!(" ✗ 启动失败: {e}")); } } } } // ── 5. 注册到数据库 ─────────────────────────────────────────────────────── let _ = app.emit("template:progress", "__step:3"); let conn = db::pool().get().map_err(|e| e.to_string())?; let profile_id = Uuid::new_v4().to_string(); let workspace_id = Uuid::new_v4().to_string(); conn.execute( "INSERT INTO project_profiles (id, name, description, type, status, priority, tech_stack) VALUES (?1, ?2, ?3, 'app', 'active', 'mid', ?4)", params![profile_id, project_name, tmpl.description, tmpl.tech_stack], ) .map_err(|e| e.to_string())?; let is_wsl = platform == "wsl"; let (wsl_path, win_path_col): (Option<&str>, Option<&str>) = if is_wsl { (Some(&project_win_path), None) } else { (None, Some(&project_win_path)) }; let stored_platform = if is_wsl { "wsl" } else { "windows" }; conn.execute( "INSERT INTO project_workspaces (id, profile_id, space_id, wsl_path, win_path, platform) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", params![workspace_id, profile_id, space_id, wsl_path, win_path_col, stored_platform], ) .map_err(|e| e.to_string())?; log_push!(format!("✅ 项目已注册,ID: {}", workspace_id)); Ok(CreateProjectResult { project_id: workspace_id, logs, }) } // ── 变量替换 ────────────────────────────────────────────────────────────────── fn apply_vars(content: &str, project_name: &str) -> String { let kebab = project_name .to_lowercase() .replace(' ', "-") .replace('_', "-"); let snake = project_name .to_lowercase() .replace(' ', "_") .replace('-', "_"); content .replace("{{project_name}}", project_name) .replace("{{project_name_kebab}}", &kebab) .replace("{{project_name_snake}}", &snake) } // ── 内置模板种子数据 ────────────────────────────────────────────────────────── pub fn seed_builtin_templates() { let conn = match db::pool().get() { Ok(c) => c, Err(_) => return, }; // 不做全局跳过,直接用 INSERT OR IGNORE 保证幂等 // 这样新增内置模板时,已有用户也能自动获得 struct Tpl { id: &'static str, name: &'static str, description: &'static str, platform: &'static str, tech_stack: &'static str, init_commands: &'static str, files: Vec<(&'static str, &'static str)>, /// JSON 字符串,存在时前端显示下拉选择初始化方式 variants: Option<&'static str>, } let templates = vec![ Tpl { id: "builtin-pnpm-monorepo", name: "pnpm Monorepo", description: "多包 monorepo,含 apps/ 与 packages/ 目录", platform: "both", tech_stack: r#"{"客户端":["Web"],"框架/工具":["Node","pnpm"]}"#, init_commands: "pnpm install", variants: None, files: vec![ ("package.json", r#"{ "name": "{{project_name_kebab}}", "private": true, "scripts": { "dev": "pnpm -r --filter './apps/**' dev", "build": "pnpm -r --filter './apps/**' build", "lint": "pnpm -r lint" }, "engines": { "node": ">=18", "pnpm": ">=8" } } "#), ("pnpm-workspace.yaml", r#"packages: - 'apps/*' - 'packages/*' "#), (".npmrc", "strict-peer-dependencies=false\nauto-install-peers=true\n"), ("apps/.gitkeep", ""), ("packages/.gitkeep", ""), (".gitignore", "node_modules/\ndist/\n.next/\n*.log\n.env\n.env.*\n!.env.example\n"), ], }, Tpl { id: "builtin-pnpm-app", name: "pnpm App", description: "单包 Node.js 应用", platform: "both", tech_stack: r#"{"客户端":["Web"],"框架/工具":["Node","pnpm"]}"#, init_commands: "pnpm install", variants: None, files: vec![ ("package.json", r#"{ "name": "{{project_name_kebab}}", "version": "0.1.0", "private": true, "type": "module", "scripts": { "start": "node src/index.js", "dev": "node --watch src/index.js" } } "#), ("src/index.js", "console.log('Hello from {{project_name}}!')\n"), (".gitignore", "node_modules/\ndist/\n*.log\n.env\n"), ], }, Tpl { id: "builtin-rust", name: "Rust", description: "Rust 二进制项目", platform: "both", tech_stack: r#"{"客户端":["CLI","Desktop"],"语言":["Rust"]}"#, init_commands: "cargo build", variants: None, files: vec![ ("Cargo.toml", r#"[package] name = "{{project_name_kebab}}" version = "0.1.0" edition = "2021" [dependencies] "#), ("src/main.rs", "fn main() {\n println!(\"Hello from {{project_name}}!\");\n}\n"), (".gitignore", "target/\n"), ], }, Tpl { id: "builtin-empty", name: "空文件夹", description: "只创建目录,不添加任何文件", platform: "both", tech_stack: "", init_commands: "", variants: None, files: vec![], }, Tpl { id: "builtin-tauri-react", name: "Tauri + React", description: "桌面应用,Tauri v2 + React 19 + TypeScript + Vite", platform: "both", tech_stack: r#"{"客户端":["Desktop"],"语言":["TypeScript","Rust"],"框架/工具":["React","Tauri","Node","pnpm"]}"#, init_commands: "pnpm install", variants: None, files: vec![ ("package.json", r#"{ "name": "{{project_name_kebab}}", "private": true, "version": "0.1.0", "type": "module", "scripts": { "dev": "tauri dev", "build": "tauri build", "preview": "vite preview" }, "dependencies": { "@tauri-apps/api": "^2", "react": "^19", "react-dom": "^19" }, "devDependencies": { "@tauri-apps/cli": "^2", "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "^4", "typescript": "^5", "vite": "^6" } } "#), ("vite.config.ts", r#"import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; export default defineConfig(async () => ({ plugins: [react()], clearScreen: false, server: { port: 1420, strictPort: true, watch: { ignored: ["**/src-tauri/**"], }, }, })); "#), ("tsconfig.json", r#"{ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "strict": true }, "include": ["src"] } "#), ("index.html", r#" {{project_name}}
"#), ("src/main.tsx", r#"import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( ); "#), ("src/App.tsx", r#"import { useState } from "react"; function App() { const [count, setCount] = useState(0); return (

{{project_name}}

Tauri v2 + React + TypeScript

); } export default App; "#), ("src-tauri/tauri.conf.json", r#"{ "$schema": "https://schema.tauri.app/config/2", "productName": "{{project_name_kebab}}", "version": "0.1.0", "identifier": "com.example.{{project_name_snake}}", "build": { "beforeDevCommand": "pnpm vite", "devUrl": "http://localhost:1420", "beforeBuildCommand": "pnpm vite build", "frontendDist": "../dist" }, "app": { "windows": [ { "title": "{{project_name}}", "width": 1200, "height": 800 } ], "security": { "csp": null } }, "bundle": { "active": true, "targets": "all", "icon": [] } } "#), ("src-tauri/Cargo.toml", r#"[package] name = "{{project_name_snake}}" version = "0.1.0" description = "A Tauri App" authors = [] edition = "2021" [lib] name = "{{project_name_snake}}_lib" crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] tauri-build = { version = "2", features = [] } [dependencies] tauri = { version = "2", features = [] } tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" "#), ("src-tauri/build.rs", "fn main() {\n tauri_build::build()\n}\n"), ("src-tauri/src/main.rs", r#"// Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { {{project_name_snake}}_lib::run() } "#), ("src-tauri/src/lib.rs", r#"#[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } "#), ("src-tauri/capabilities/default.json", r#"{ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Capability for the main window", "windows": ["main"], "permissions": [ "core:default", "opener:default" ] } "#), (".gitignore", "node_modules/\ndist/\n.env\n.env.*\n!.env.example\nsrc-tauri/target/\nsrc-tauri/gen/\n"), ], }, Tpl { id: "builtin-expo", name: "Expo", description: "React Native 移动端 App(Android / iOS)。TypeScript + Expo SDK,适合游戏、工具、内容类手机应用。选择下方模板变体来确定初始化方式。", platform: "both", tech_stack: r#"{"客户端":["Android","iOS"],"语言":["TypeScript"],"框架/工具":["React Native","Expo"]}"#, init_commands: "pnpm create expo-app@latest . --template blank-typescript", variants: Some(r#"[ {"label":"blank-typescript(推荐)","description":"最小化 TypeScript 起步,适合游戏类、工具类、独立 App","initCommands":"pnpm create expo-app@latest . --template blank-typescript"}, {"label":"tabs(底部标签导航)","description":"内置底部 Tab 栏 + Expo Router,适合多页面内容型 App","initCommands":"pnpm create expo-app@latest . --template tabs"}, {"label":"bare(裸原生配置)","description":"完整原生 Expo 配置,可深度定制原生模块,适合进阶项目","initCommands":"pnpm create expo-app@latest . --template bare-minimum"}, {"label":"默认(向导模式)","description":"运行官方交互式向导,在终端中选择模板类型","initCommands":"pnpm create expo-app@latest ."} ]"#), files: vec![], }, Tpl { id: "builtin-pnpm-expo-monorepo", name: "pnpm Monorepo + Expo", description: "pnpm 多包 Monorepo:apps/ 内含 Expo Router (React Native) 应用,packages/ 内含共享 @repo/types 包。Metro 已配置 monorepo 路径解析(含 Windows 兼容修复),适合多 App 扩展场景。", platform: "both", tech_stack: r#"{"客户端":["Android","iOS","Web"],"语言":["TypeScript"],"框架/工具":["React Native","Expo","pnpm"]}"#, init_commands: "pnpm install", variants: None, files: vec![ // ── monorepo 根 ────────────────────────────────────────────── ("package.json", r#"{ "name": "{{project_name_kebab}}", "private": true, "scripts": { "start": "pnpm --filter app start", "android": "pnpm --filter app android", "ios": "pnpm --filter app ios", "lint": "pnpm -r lint" }, "engines": { "node": ">=18", "pnpm": ">=8" } } "#), ("pnpm-workspace.yaml", "packages:\n - 'apps/*'\n - 'packages/*'\n"), ("tsconfig.base.json", r#"{ "compilerOptions": { "target": "ES2020", "lib": ["ES2020"], "module": "ESNext", "moduleResolution": "bundler", "resolveJsonModule": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, "strict": true, "skipLibCheck": true, "declaration": true, "declarationMap": true, "sourceMap": true }, "exclude": ["node_modules", "dist", "build"] } "#), (".gitignore", "node_modules/\ndist/\n.expo/\nandroid/\nios/\n*.log\n.env\n.env.*\n!.env.example\n"), (".npmrc", "strict-peer-dependencies=false\nauto-install-peers=true\n"), // ── packages/types ─────────────────────────────────────────── ("packages/types/package.json", r#"{ "name": "@repo/types", "version": "0.0.1", "private": true, "type": "module", "exports": { ".": { "types": "./src/index.ts", "import": "./src/index.ts" } }, "files": ["src"], "scripts": { "typecheck": "tsc --noEmit" }, "devDependencies": { "typescript": "^5.4.5" } } "#), ("packages/types/src/index.ts", "// 在此导出跨 App 共享的类型\nexport type {}\n"), // ── apps/app ───────────────────────────────────────────────── ("apps/app/package.json", r#"{ "name": "app", "version": "1.0.0", "private": true, "main": "expo-router/entry", "scripts": { "start": "expo start", "android": "expo run:android", "ios": "expo run:ios", "lint": "eslint . --ext .ts,.tsx" }, "dependencies": { "@repo/types": "workspace:*", "expo": "~52.0.0", "expo-router": "~4.0.0", "expo-status-bar": "~2.0.0", "react": "18.3.1", "react-native": "0.76.3", "react-native-gesture-handler": "~2.20.0", "react-native-reanimated": "~3.16.1", "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.1.0" }, "devDependencies": { "@babel/core": "^7.25.2", "@types/react": "~18.3.12", "typescript": "^5.4.5" } } "#), ("apps/app/app.json", r##"{ "expo": { "name": "{{project_name}}", "slug": "{{project_name_kebab}}", "version": "1.0.0", "orientation": "default", "userInterfaceStyle": "light", "scheme": "{{project_name_kebab}}", "ios": { "supportsTablet": true, "bundleIdentifier": "com.example.{{project_name_snake}}" }, "android": { "adaptiveIcon": { "backgroundColor": "#ffffff" }, "package": "com.example.{{project_name_snake}}" }, "plugins": ["expo-router"], "experiments": { "typedRoutes": true } } } "##), ("apps/app/tsconfig.json", r#"{ "extends": "expo/tsconfig.base", "compilerOptions": { "strict": true, "paths": { "@repo/types": ["../../packages/types/src/index.ts"] } }, "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"] } "#), ("apps/app/babel.config.js", r#"module.exports = function (api) { api.cache(true) return { presets: ['babel-preset-expo'], } } "#), // Metro 配置:monorepo 路径解析 + Windows release 构建兼容修复 ("apps/app/metro.config.js", r#"const { getDefaultConfig } = require('expo/metro-config') const path = require('path') const projectRoot = __dirname const monorepoRoot = path.resolve(projectRoot, '../..') const config = getDefaultConfig(projectRoot) // Windows release 构建兼容:强制 unstable_serverRoot 为 projectRoot, // 避免 Metro 与 RN Gradle 插件的路径基准不一致。 config.server = { ...config.server, unstable_serverRoot: projectRoot, } // 让 Metro 监听 monorepo 根目录,确保根 node_modules 可被解析 config.watchFolders = [monorepoRoot] // 模块解析:优先查 app 自己的 node_modules,再查 monorepo 根 config.resolver.nodeModulesPaths = [ path.resolve(projectRoot, 'node_modules'), path.resolve(monorepoRoot, 'node_modules'), ] module.exports = config "#), ("apps/app/expo-env.d.ts", "/// \n\n// NOTE: This file should not be edited and should be in your git ignore\n"), ("apps/app/app/_layout.tsx", r#"import { Stack } from 'expo-router' import { StatusBar } from 'expo-status-bar' export default function RootLayout() { return ( <> ) } "#), ("apps/app/app/index.tsx", r#"import { View, Text, StyleSheet } from 'react-native' import { SafeAreaView } from 'react-native-safe-area-context' export default function HomeScreen() { return ( {{project_name}} Expo Router + pnpm Monorepo ) } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff' }, content: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 8 }, title: { fontSize: 24, fontWeight: 'bold' }, subtitle: { fontSize: 14, color: '#666' }, }) "#), ], }, // ── P1: React + Vite SPA ───────────────────────────────────────────── Tpl { id: "builtin-vite-react", name: "React + Vite SPA", description: "纯前端 Web 应用,React 19 + TypeScript + TailwindCSS v4 + React Router v7", platform: "both", tech_stack: r#"{"客户端":["Web"],"语言":["TypeScript"],"框架/工具":["React","Vite","TailwindCSS"]}"#, init_commands: "pnpm install", variants: None, files: vec![ ("package.json", r#"{ "name": "{{project_name_kebab}}", "private": true, "version": "0.1.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc -b && vite build", "preview": "vite preview" }, "dependencies": { "react": "^19", "react-dom": "^19", "react-router": "^7" }, "devDependencies": { "@tailwindcss/vite": "^4", "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "^4", "tailwindcss": "^4", "typescript": "^5", "vite": "^6" } } "#), ("vite.config.ts", r#"import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ plugins: [react(), tailwindcss()], }); "#), ("tsconfig.json", r#"{ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "strict": true }, "include": ["src"] } "#), ("index.html", r#" {{project_name}}
"#), ("src/main.tsx", r#"import React from "react"; import ReactDOM from "react-dom/client"; import { BrowserRouter } from "react-router"; import App from "./App"; import "./index.css"; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( ); "#), ("src/App.tsx", r#"import { Routes, Route } from "react-router"; import Home from "./pages/Home"; export default function App() { return ( } /> ); } "#), ("src/index.css", "@import \"tailwindcss\";\n"), ("src/pages/Home.tsx", r#"export default function Home() { return (

{{project_name}}

React + Vite + TailwindCSS

); } "#), (".gitignore", "node_modules/\ndist/\n.env\n.env.*\n!.env.example\n"), ], }, // ── P1: Tauri + React + TailwindCSS ────────────────────────────────── Tpl { id: "builtin-tauri-react-tailwind", name: "Tauri + React + Tailwind", description: "桌面应用,Tauri v2 + React 19 + TypeScript + TailwindCSS v4", platform: "both", tech_stack: r#"{"客户端":["Desktop"],"语言":["TypeScript","Rust"],"框架/工具":["React","Tauri","TailwindCSS","Node","pnpm"]}"#, init_commands: "pnpm install", variants: None, files: vec![ ("package.json", r#"{ "name": "{{project_name_kebab}}", "private": true, "version": "0.1.0", "type": "module", "scripts": { "dev": "tauri dev", "build": "tauri build", "preview": "vite preview" }, "dependencies": { "@tauri-apps/api": "^2", "react": "^19", "react-dom": "^19" }, "devDependencies": { "@tailwindcss/vite": "^4", "@tauri-apps/cli": "^2", "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "^4", "tailwindcss": "^4", "typescript": "^5", "vite": "^6" } } "#), ("vite.config.ts", r#"import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; export default defineConfig(async () => ({ plugins: [react(), tailwindcss()], clearScreen: false, server: { port: 1420, strictPort: true, watch: { ignored: ["**/src-tauri/**"], }, }, })); "#), ("tsconfig.json", r#"{ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "strict": true }, "include": ["src"] } "#), ("index.html", r#" {{project_name}}
"#), ("src/main.tsx", r#"import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; import "./index.css"; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( ); "#), ("src/index.css", "@import \"tailwindcss\";\n"), ("src/App.tsx", r#"import { useState } from "react"; function App() { const [count, setCount] = useState(0); return (

{{project_name}}

Tauri v2 + React + TailwindCSS

); } export default App; "#), ("src-tauri/tauri.conf.json", r#"{ "$schema": "https://schema.tauri.app/config/2", "productName": "{{project_name_kebab}}", "version": "0.1.0", "identifier": "com.example.{{project_name_snake}}", "build": { "beforeDevCommand": "pnpm vite", "devUrl": "http://localhost:1420", "beforeBuildCommand": "pnpm vite build", "frontendDist": "../dist" }, "app": { "windows": [ { "title": "{{project_name}}", "width": 1200, "height": 800 } ], "security": { "csp": null } }, "bundle": { "active": true, "targets": "all", "icon": [] } } "#), ("src-tauri/Cargo.toml", r#"[package] name = "{{project_name_snake}}" version = "0.1.0" description = "A Tauri App" authors = [] edition = "2021" [lib] name = "{{project_name_snake}}_lib" crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] tauri-build = { version = "2", features = [] } [dependencies] tauri = { version = "2", features = [] } tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" "#), ("src-tauri/build.rs", "fn main() {\n tauri_build::build()\n}\n"), ("src-tauri/src/main.rs", r#"// Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { {{project_name_snake}}_lib::run() } "#), ("src-tauri/src/lib.rs", r#"#[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } "#), ("src-tauri/capabilities/default.json", r#"{ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Capability for the main window", "windows": ["main"], "permissions": [ "core:default", "opener:default" ] } "#), (".gitignore", "node_modules/\ndist/\n.env\n.env.*\n!.env.example\nsrc-tauri/target/\nsrc-tauri/gen/\n"), ], }, // ── P3: Web + Mobile 共享 Monorepo ──────────────────────────────────── Tpl { id: "builtin-web-mobile-monorepo", name: "Web + Mobile Monorepo", description: "跨平台代码复用:packages/core 共享业务逻辑,apps/web(Vite + Tailwind)+ apps/mobile(Expo + NativeWind)", platform: "both", tech_stack: r#"{"客户端":["Android","iOS","Web"],"语言":["TypeScript"],"框架/工具":["React","React Native","Expo","Vite","TailwindCSS","NativeWind","pnpm"]}"#, init_commands: "pnpm install", variants: None, files: vec![ // ── 根目录 ────────────────────────────────────────────────────── ("package.json", r#"{ "name": "{{project_name_kebab}}", "private": true, "scripts": { "dev": "pnpm --filter web dev", "build:web": "pnpm --filter web build", "start:mobile": "pnpm --filter mobile start", "android": "pnpm --filter mobile android", "ios": "pnpm --filter mobile ios", "typecheck": "pnpm -r typecheck" }, "engines": { "node": ">=18", "pnpm": ">=8" } } "#), ("pnpm-workspace.yaml", "packages:\n - 'apps/*'\n - 'packages/*'\n"), ("tsconfig.base.json", r#"{ "compilerOptions": { "target": "ES2020", "lib": ["ES2020"], "module": "ESNext", "moduleResolution": "bundler", "resolveJsonModule": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, "strict": true, "skipLibCheck": true, "declaration": true, "declarationMap": true, "sourceMap": true }, "exclude": ["node_modules", "dist", "build"] } "#), (".gitignore", "node_modules/\ndist/\nbuild/\n.expo/\nandroid/\nios/\n*.log\n.env\n.env.*\n!.env.example\n"), (".npmrc", "strict-peer-dependencies=false\nauto-install-peers=true\n"), // ── packages/types ────────────────────────────────────────────── ("packages/types/package.json", r#"{ "name": "@repo/types", "version": "0.0.1", "private": true, "type": "module", "exports": { ".": { "types": "./src/index.ts", "import": "./src/index.ts" } }, "scripts": { "typecheck": "tsc --noEmit" }, "devDependencies": { "typescript": "^5" } } "#), ("packages/types/tsconfig.json", r#"{ "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "dist", "rootDir": "src" }, "include": ["src"] } "#), ("packages/types/src/index.ts", r#"// 在此定义跨平台共享的类型 export interface User { id: string name: string email: string } export interface ApiResponse { data: T error?: string } "#), // ── packages/core ─────────────────────────────────────────────── ("packages/core/package.json", r#"{ "name": "@repo/core", "version": "0.0.1", "private": true, "type": "module", "exports": { ".": { "types": "./src/index.ts", "import": "./src/index.ts" } }, "scripts": { "typecheck": "tsc --noEmit" }, "dependencies": { "@repo/types": "workspace:*" }, "devDependencies": { "typescript": "^5" } } "#), ("packages/core/tsconfig.json", r#"{ "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "dist", "rootDir": "src" }, "include": ["src"] } "#), ("packages/core/src/index.ts", r#"// 在此导出跨平台共享的业务逻辑(纯 TS,不依赖任何平台 API) export * from "./api" "#), ("packages/core/src/api.ts", r#"import type { ApiResponse, User } from "@repo/types" const BASE_URL = "" export async function getUser(id: string): Promise> { const res = await fetch(`${BASE_URL}/api/users/${id}`) if (!res.ok) return { data: null as unknown as User, error: res.statusText } const data = await res.json() return { data } } "#), // ── apps/web ──────────────────────────────────────────────────── ("apps/web/package.json", r#"{ "name": "web", "version": "0.0.1", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "tsc -b && vite build", "preview": "vite preview", "typecheck": "tsc --noEmit" }, "dependencies": { "@repo/core": "workspace:*", "@repo/types": "workspace:*", "react": "^19", "react-dom": "^19", "react-router": "^7" }, "devDependencies": { "@tailwindcss/vite": "^4", "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "^4", "tailwindcss": "^4", "typescript": "^5", "vite": "^6" } } "#), ("apps/web/tsconfig.json", r#"{ "extends": "../../tsconfig.base.json", "compilerOptions": { "lib": ["ES2020", "DOM", "DOM.Iterable"], "noEmit": true, "jsx": "react-jsx", "paths": { "@repo/core": ["../../packages/core/src/index.ts"], "@repo/types": ["../../packages/types/src/index.ts"] } }, "include": ["src"] } "#), ("apps/web/vite.config.ts", r#"import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ plugins: [react(), tailwindcss()], }); "#), ("apps/web/index.html", r#" {{project_name}}
"#), ("apps/web/src/main.tsx", r#"import React from "react"; import ReactDOM from "react-dom/client"; import { BrowserRouter } from "react-router"; import App from "./App"; import "./index.css"; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( ); "#), ("apps/web/src/index.css", "@import \"tailwindcss\";\n"), ("apps/web/src/App.tsx", r#"import { Routes, Route } from "react-router"; import Home from "./pages/Home"; export default function App() { return ( } /> ); } "#), ("apps/web/src/pages/Home.tsx", r#"export default function Home() { return (

{{project_name}}

Web + Mobile Monorepo

); } "#), // ── apps/mobile ───────────────────────────────────────────────── ("apps/mobile/package.json", r#"{ "name": "mobile", "version": "1.0.0", "private": true, "main": "expo-router/entry", "scripts": { "start": "expo start", "android": "expo run:android", "ios": "expo run:ios", "typecheck": "tsc --noEmit" }, "dependencies": { "@repo/core": "workspace:*", "@repo/types": "workspace:*", "expo": "~52.0.0", "expo-router": "~4.0.0", "expo-status-bar": "~2.0.0", "nativewind": "^4.0.1", "react": "18.3.1", "react-native": "0.76.3", "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.1.0", "tailwindcss": "^3.4.0" }, "devDependencies": { "@babel/core": "^7.25.2", "@types/react": "~18.3.12", "typescript": "^5.4.5" } } "#), ("apps/mobile/app.json", r##"{ "expo": { "name": "{{project_name}}", "slug": "{{project_name_kebab}}", "version": "1.0.0", "scheme": "{{project_name_kebab}}", "userInterfaceStyle": "automatic", "ios": { "supportsTablet": true, "bundleIdentifier": "com.example.{{project_name_snake}}" }, "android": { "adaptiveIcon": { "backgroundColor": "#ffffff" }, "package": "com.example.{{project_name_snake}}" }, "plugins": ["expo-router"], "experiments": { "typedRoutes": true } } } "##), ("apps/mobile/tsconfig.json", r#"{ "extends": "expo/tsconfig.base", "compilerOptions": { "strict": true, "paths": { "@repo/core": ["../../packages/core/src/index.ts"], "@repo/types": ["../../packages/types/src/index.ts"] } }, "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"] } "#), ("apps/mobile/tailwind.config.js", r#"/** @type {import('tailwindcss').Config} */ module.exports = { content: ["./app/**/*.{ts,tsx}"], presets: [require("nativewind/preset")], theme: { extend: {}, }, plugins: [], } "#), ("apps/mobile/global.css", "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"), ("apps/mobile/babel.config.js", r#"module.exports = function (api) { api.cache(true) return { presets: ['babel-preset-expo'], plugins: ['nativewind/babel'], } } "#), ("apps/mobile/metro.config.js", r#"const { getDefaultConfig } = require('expo/metro-config') const { withNativeWind } = require('nativewind/metro') const path = require('path') const projectRoot = __dirname const monorepoRoot = path.resolve(projectRoot, '../..') const config = getDefaultConfig(projectRoot) config.watchFolders = [monorepoRoot] config.resolver.nodeModulesPaths = [ path.resolve(projectRoot, 'node_modules'), path.resolve(monorepoRoot, 'node_modules'), ] module.exports = withNativeWind(config, { input: './global.css' }) "#), ("apps/mobile/expo-env.d.ts", "/// \n"), ("apps/mobile/app/_layout.tsx", r#"import '../global.css' import { Stack } from 'expo-router' import { StatusBar } from 'expo-status-bar' export default function RootLayout() { return ( <> ) } "#), ("apps/mobile/app/index.tsx", r#"import { View, Text } from 'react-native' export default function HomeScreen() { return ( {{project_name}} Web + Mobile Monorepo ) } "#), ], }, // ── 三端体系模板(同一架构,按需起步)──────────────────────────────────── Tpl { id: "builtin-react-web", name: "React Web", description: "三端体系 Web 基础。React 18 + TypeScript + Ant Design 5 + Zustand,可随时扩展为 Capacitor Android 或 Tauri Desktop。", platform: "both", tech_stack: r#"{"体系":["三端"],"客户端":["Web"],"语言":["TypeScript"],"框架/工具":["React","Ant Design","Zustand","Vite","pnpm"]}"#, init_commands: "pnpm install", variants: None, files: vec![ ("package.json", r#"{ "name": "{{project_name_kebab}}", "version": "0.1.0", "private": true, "type": "module", "scripts": { "web": "vite", "build": "vite build", "preview": "vite preview", "typecheck": "tsc --noEmit" }, "dependencies": { "antd": "^5.21.0", "react": "^18.3.0", "react-dom": "^18.3.0", "zustand": "^5.0.0" }, "devDependencies": { "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.0", "typescript": "^5.5.0", "vite": "^5.4.0" } } "#), ("vite.config.ts", r#"import { defineConfig, loadEnv } from "vite"; import react from "@vitejs/plugin-react"; export default defineConfig(({ mode }) => { loadEnv(mode, process.cwd(), ""); return { plugins: [react()], base: mode === "capacitor" ? "./" : "/", }; }); "#), ("tsconfig.json", r#"{ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "strict": true }, "include": ["src"] } "#), ("index.html", r#" {{project_name}}
"#), (".env", "VITE_PLATFORM=web\n"), ("src/main.tsx", r#"import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( ); "#), ("src/App.tsx", r#"import { Button } from "antd"; import { useAppStore } from "./store"; export default function App() { const { count, increment } = useAppStore(); return (

{{project_name}}

React 18 + Ant Design 5 + Zustand

); } "#), ("src/store/index.ts", r#"import { create } from "zustand"; interface AppStore { count: number; increment: () => void; } export const useAppStore = create((set) => ({ count: 0, increment: () => set((s) => ({ count: s.count + 1 })), })); "#), (".gitignore", "node_modules/\ndist/\n*.log\n.env\n.env.*\n!.env.example\n"), ], }, Tpl { id: "builtin-react-capacitor", name: "React + Capacitor", description: "Web + Android。React 18 + Ant Design 5 + Zustand + Capacitor 8,pnpm build:cap 打包为原生 APK。", platform: "both", tech_stack: r#"{"体系":["三端"],"客户端":["Web","Android"],"语言":["TypeScript"],"框架/工具":["React","Ant Design","Zustand","Capacitor","Vite","pnpm"]}"#, init_commands: "pnpm install", variants: None, files: vec![ ("package.json", r#"{ "name": "{{project_name_kebab}}", "version": "0.1.0", "private": true, "type": "module", "scripts": { "web": "vite", "build": "vite build", "build:cap": "vite build --mode capacitor && npx cap sync", "preview": "vite preview", "typecheck": "tsc --noEmit" }, "dependencies": { "@capacitor/core": "^8.0.0", "antd": "^5.21.0", "react": "^18.3.0", "react-dom": "^18.3.0", "zustand": "^5.0.0" }, "devDependencies": { "@capacitor/android": "^8.0.0", "@capacitor/cli": "^8.0.0", "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.0", "typescript": "^5.5.0", "vite": "^5.4.0" } } "#), ("vite.config.ts", r#"import { defineConfig, loadEnv } from "vite"; import react from "@vitejs/plugin-react"; export default defineConfig(({ mode }) => { loadEnv(mode, process.cwd(), ""); return { plugins: [react()], base: mode === "capacitor" ? "./" : "/", }; }); "#), ("tsconfig.json", r#"{ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "strict": true }, "include": ["src", "capacitor.config.ts"] } "#), ("index.html", r#" {{project_name}}
"#), (".env", "VITE_PLATFORM=web\n"), (".env.capacitor", "VITE_PLATFORM=android\n"), ("capacitor.config.ts", r#"import type { CapacitorConfig } from "@capacitor/cli"; const config: CapacitorConfig = { appId: "com.example.{{project_name_snake}}", appName: "{{project_name}}", webDir: "dist", }; export default config; "#), ("src/main.tsx", r#"import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( ); "#), ("src/App.tsx", r#"import { Button } from "antd"; import { useAppStore } from "./store"; export default function App() { const { count, increment } = useAppStore(); return (

{{project_name}}

React 18 + Ant Design 5 + Zustand + Capacitor

); } "#), ("src/store/index.ts", r#"import { create } from "zustand"; interface AppStore { count: number; increment: () => void; } export const useAppStore = create((set) => ({ count: 0, increment: () => set((s) => ({ count: s.count + 1 })), })); "#), (".gitignore", "node_modules/\ndist/\n*.log\n.env\n.env.*\n!.env.example\nandroid/\n"), ], }, Tpl { id: "builtin-react-tauri-antd", name: "React + Tauri", description: "Web + Desktop。React 18 + Ant Design 5 + Zustand + Tauri 2,pnpm build:desktop 打包为原生桌面应用。", platform: "both", tech_stack: r#"{"体系":["三端"],"客户端":["Web","Desktop"],"语言":["TypeScript","Rust"],"框架/工具":["React","Ant Design","Zustand","Tauri","Vite","pnpm"]}"#, init_commands: "pnpm install", variants: None, files: vec![ ("package.json", r#"{ "name": "{{project_name_kebab}}", "version": "0.1.0", "private": true, "type": "module", "scripts": { "web": "vite", "build": "vite build", "build:desktop": "tauri build", "dev:desktop": "tauri dev", "preview": "vite preview", "typecheck": "tsc --noEmit" }, "dependencies": { "@tauri-apps/api": "^2", "antd": "^5.21.0", "react": "^18.3.0", "react-dom": "^18.3.0", "zustand": "^5.0.0" }, "devDependencies": { "@tauri-apps/cli": "^2", "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.0", "typescript": "^5.5.0", "vite": "^5.4.0" } } "#), ("vite.config.ts", r#"import { defineConfig, loadEnv } from "vite"; import react from "@vitejs/plugin-react"; export default defineConfig(({ mode }) => { loadEnv(mode, process.cwd(), ""); return { plugins: [react()], base: mode === "capacitor" ? "./" : "/", clearScreen: false, server: { port: 1420, strictPort: true, watch: { ignored: ["**/src-tauri/**"], }, }, }; }); "#), ("tsconfig.json", r#"{ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "strict": true }, "include": ["src"] } "#), ("index.html", r#" {{project_name}}
"#), (".env", "VITE_PLATFORM=web\n"), ("src/main.tsx", r#"import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( ); "#), ("src/App.tsx", r#"import { Button } from "antd"; import { useAppStore } from "./store"; export default function App() { const { count, increment } = useAppStore(); return (

{{project_name}}

React 18 + Ant Design 5 + Zustand + Tauri 2

); } "#), ("src/store/index.ts", r#"import { create } from "zustand"; interface AppStore { count: number; increment: () => void; } export const useAppStore = create((set) => ({ count: 0, increment: () => set((s) => ({ count: s.count + 1 })), })); "#), ("src-tauri/tauri.conf.json", r#"{ "$schema": "https://schema.tauri.app/config/2", "productName": "{{project_name_kebab}}", "version": "0.1.0", "identifier": "com.example.{{project_name_snake}}", "build": { "beforeDevCommand": "pnpm web", "devUrl": "http://localhost:1420", "beforeBuildCommand": "pnpm build", "frontendDist": "../dist" }, "app": { "windows": [ { "title": "{{project_name}}", "width": 1200, "height": 800 } ], "security": { "csp": null } }, "bundle": { "active": true, "targets": "all", "icon": [] } } "#), ("src-tauri/Cargo.toml", r#"[package] name = "{{project_name_snake}}" version = "0.1.0" description = "A Tauri App" authors = [] edition = "2021" [lib] name = "{{project_name_snake}}_lib" crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] tauri-build = { version = "2", features = [] } [dependencies] tauri = { version = "2", features = [] } tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" "#), ("src-tauri/build.rs", "fn main() {\n tauri_build::build()\n}\n"), ("src-tauri/src/main.rs", r#"// Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { {{project_name_snake}}_lib::run() } "#), ("src-tauri/src/lib.rs", r#"#[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } "#), ("src-tauri/capabilities/default.json", r#"{ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Capability for the main window", "windows": ["main"], "permissions": [ "core:default", "opener:default" ] } "#), (".gitignore", "node_modules/\ndist/\n*.log\n.env\n.env.*\n!.env.example\nsrc-tauri/target/\nsrc-tauri/gen/\n"), ], }, Tpl { id: "builtin-react-triplatform", name: "React Tri-Platform", description: "三端合一。同一套 React 18 + Ant Design 5 + Zustand 代码,pnpm web / build:cap / build:desktop 分别构建 Web / Android / Desktop。", platform: "both", tech_stack: r#"{"体系":["三端"],"客户端":["Web","Android","Desktop"],"语言":["TypeScript","Rust"],"框架/工具":["React","Ant Design","Zustand","Capacitor","Tauri","Vite","pnpm"]}"#, init_commands: "pnpm install", variants: None, files: vec![ ("package.json", r#"{ "name": "{{project_name_kebab}}", "version": "0.1.0", "private": true, "type": "module", "scripts": { "web": "vite", "build": "vite build", "build:cap": "vite build --mode capacitor && npx cap sync", "build:desktop": "tauri build", "dev:desktop": "tauri dev", "preview": "vite preview", "typecheck": "tsc --noEmit" }, "dependencies": { "@capacitor/core": "^8.0.0", "@tauri-apps/api": "^2", "antd": "^5.21.0", "react": "^18.3.0", "react-dom": "^18.3.0", "zustand": "^5.0.0" }, "devDependencies": { "@capacitor/android": "^8.0.0", "@capacitor/cli": "^8.0.0", "@tauri-apps/cli": "^2", "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.0", "typescript": "^5.5.0", "vite": "^5.4.0" } } "#), ("vite.config.ts", r#"import { defineConfig, loadEnv } from "vite"; import react from "@vitejs/plugin-react"; export default defineConfig(({ mode }) => { loadEnv(mode, process.cwd(), ""); return { plugins: [react()], base: mode === "capacitor" ? "./" : "/", clearScreen: false, server: { port: 1420, strictPort: true, watch: { ignored: ["**/src-tauri/**"], }, }, }; }); "#), ("tsconfig.json", r#"{ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "strict": true }, "include": ["src", "capacitor.config.ts"] } "#), ("index.html", r#" {{project_name}}
"#), (".env", "VITE_PLATFORM=web\n"), (".env.capacitor", "VITE_PLATFORM=android\n"), ("capacitor.config.ts", r#"import type { CapacitorConfig } from "@capacitor/cli"; const config: CapacitorConfig = { appId: "com.example.{{project_name_snake}}", appName: "{{project_name}}", webDir: "dist", }; export default config; "#), ("src/main.tsx", r#"import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( ); "#), ("src/App.tsx", r#"import { Button } from "antd"; import { useAppStore } from "./store"; export default function App() { const { count, increment } = useAppStore(); return (

{{project_name}}

React 18 + Ant Design 5 + Zustand · Web / Android / Desktop

); } "#), ("src/store/index.ts", r#"import { create } from "zustand"; interface AppStore { count: number; increment: () => void; } export const useAppStore = create((set) => ({ count: 0, increment: () => set((s) => ({ count: s.count + 1 })), })); "#), ("src-tauri/tauri.conf.json", r#"{ "$schema": "https://schema.tauri.app/config/2", "productName": "{{project_name_kebab}}", "version": "0.1.0", "identifier": "com.example.{{project_name_snake}}", "build": { "beforeDevCommand": "pnpm web", "devUrl": "http://localhost:1420", "beforeBuildCommand": "pnpm build", "frontendDist": "../dist" }, "app": { "windows": [ { "title": "{{project_name}}", "width": 1200, "height": 800 } ], "security": { "csp": null } }, "bundle": { "active": true, "targets": "all", "icon": [] } } "#), ("src-tauri/Cargo.toml", r#"[package] name = "{{project_name_snake}}" version = "0.1.0" description = "A Tauri App" authors = [] edition = "2021" [lib] name = "{{project_name_snake}}_lib" crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] tauri-build = { version = "2", features = [] } [dependencies] tauri = { version = "2", features = [] } tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" "#), ("src-tauri/build.rs", "fn main() {\n tauri_build::build()\n}\n"), ("src-tauri/src/main.rs", r#"// Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { {{project_name_snake}}_lib::run() } "#), ("src-tauri/src/lib.rs", r#"#[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } "#), ("src-tauri/capabilities/default.json", r#"{ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Capability for the main window", "windows": ["main"], "permissions": [ "core:default", "opener:default" ] } "#), (".gitignore", "node_modules/\ndist/\n*.log\n.env\n.env.*\n!.env.example\nandroid/\nsrc-tauri/target/\nsrc-tauri/gen/\n"), ], }, // ── 知识库(择升环)──────────────────────────────────────────────────── Tpl { id: "builtin-knowledge-loop", name: "知识库(择升环)", description: "方法论 / 知识沉淀文档库。三层晋升结构:dialogues 对话原料 → concepts 概念卡 → CORE 稳定核心,晋升规则内置于 AGENTS.md,agent 进库即按择升环工作流运转。", platform: "both", tech_stack: r#"{"体系":["知识库"],"框架/工具":["Markdown","Git"]}"#, init_commands: "git init", variants: None, files: vec![ ("CORE.md", r#"# {{project_name}} · 核心层 > 本文件是知识库的稳定核心——只收录经过实践检验、表述已收敛的内容。 > ⚠️ 过滤铁律:全文上限 300 行。收录新内容超限时,必须合并或挤掉旧内容—— > 只升不择的核心层会失去判断力。 ## 定义 ## 已验证原理 ## 修订记录 "#), ("AGENTS.md", r#"# {{project_name}} — AI Agent 工作规程 本仓库是知识库,不是代码库。目录即晋升流水线: ``` dialogues/(对话原料,允许粗糙) → concepts/(概念卡,一事一文件) → CORE.md(稳定核心,上限 300 行) ``` cases/ 是检验层:概念卡晋升 CORE 的依据来自真实案例。 ## 每次会话必须执行 1. 先读 `CORE.md` 与本次话题相关的 `concepts/` 卡——上一轮的结论是这一轮的输入 2. 探讨结束后,把整理稿写入 `dialogues/YYYY-MM-DD-主题.md` 3. 对照晋升规则检查是否触发晋升 / 降级,触发则执行 4. git commit 收尾——本库以 git 历史作为演化台账 ## 晋升规则(择) | 从 | 到 | 条件 | |----|----|------| | dialogues/ | concepts/ | 同一想法在 ≥2 次对话中出现,或 ≥1 个真实案例站住 | | concepts/ | CORE.md | 状态「稳定」+ 经 cases/ 检验 + 表述收敛为一段话 | 降级同样重要:被案例证伪的概念卡标记废弃;CORE.md 超 300 行必须挤旧。 只升不择,核心层会膨胀成噪音。 ## 写作纪律 - 单一事实来源:同一结论只在一层的一个文件里详述,其他位置引用 - 概念卡必须写「边界」(什么条件下不成立)——没有边界的结论不可信 - 修改 CORE.md 必须在其修订记录写明:进了什么、挤了什么、为什么 - 案例复盘必做归因二分:执行缺口(概念没用好)修行动; 规格缺口(概念本身有漏洞)回改概念卡——后者是全库最值钱的信号 "#), ("concepts/README.md", r#"# concepts/ — 概念卡层 一个概念一个文件,文件名 kebab-case(如 `pinggu-wai-huan.md`)。 ## 概念卡格式 ```markdown # 概念名 - 状态:孵化 | 稳定 | 已入核 | 已废弃 - 出处:dialogues/YYYY-MM-DD-xxx.md - 最近验证:YYYY-MM-DD(cases/xxx.md) ## 定义 一段话说清它是什么。 ## 为什么成立 支撑它的推理或证据。 ## 边界 它在什么条件下不成立。 ``` ## 晋升与降级 - 晋升进 CORE.md 后,状态改「已入核」,详述保留在卡内,CORE 只收一段话结论 - 被案例证伪 → 状态改「已废弃」并写明证伪案例,不直接删(失败也是判断力) "#), ("dialogues/README.md", r#"# dialogues/ — 对话沉淀层(原料) 每次探讨结束后,把整理稿存入本目录。这一层的目标是「不丢」,不是「精炼」。 - 命名:`YYYY-MM-DD-主题.md` - 开头写三行摘要:讨论了什么 / 新想法 / 待验证 - 同一想法第二次出现时,检查是否触发晋升为概念卡 "#), ("cases/README.md", r#"# cases/ — 实例复盘层(检验) 记录概念在真实事件中的应用与翻车。案例是概念卡晋升 CORE 的检验依据。 - 命名:`YYYY-MM-DD-事件.md` - 必写字段:用了哪个概念 / 预期 / 实际结果 / 归因(执行缺口 or 规格缺口) - 规格缺口 → 回改概念卡;执行缺口 → 概念不动,记录教训 "#), (".gitignore", ".DS_Store\nThumbs.db\n*.tmp\n"), ], }, ]; for t in &templates { let _ = conn.execute( "INSERT INTO project_templates (id, name, description, platform, tech_stack, init_commands, is_builtin, variants) VALUES (?1, ?2, ?3, ?4, ?5, ?6, 1, ?7) ON CONFLICT(id) DO UPDATE SET name = excluded.name, description = excluded.description, tech_stack = excluded.tech_stack, init_commands = excluded.init_commands, variants = excluded.variants", params![ t.id, t.name, if t.description.is_empty() { None } else { Some(t.description) }, t.platform, if t.tech_stack.is_empty() { None } else { Some(t.tech_stack) }, if t.init_commands.is_empty() { None } else { Some(t.init_commands) }, t.variants, ], ); for (path, content) in &t.files { let file_id = format!("{}-{}", t.id, path.replace('/', "-")); let _ = conn.execute( "INSERT OR REPLACE INTO project_template_files (id, template_id, path, content) VALUES (?1, ?2, ?3, ?4)", params![file_id, t.id, path, content], ); } } }