feat(templates): 三端体系内置模板 + 技术栈分类标签渲染
上一会话遗留的完整功能补提交: - 新增 4 个三端体系内置模板:React Web / React + Capacitor / React + Tauri (Antd) / React Tri-Platform(同一架构按需起步) - tech_stack 升级为分类 JSON 格式(客户端/语言/框架), 新增 techStackUtils.renderTechStack 兼容旧逗号格式, ManagePage / NewProjectModal 接线,TemplateEditor 占位符同步 Feature-Confirmed: true
This commit is contained in:
parent
1bc0629827
commit
3d9336d908
@ -406,7 +406,7 @@ pub fn seed_builtin_templates() {
|
||||
name: "pnpm Monorepo",
|
||||
description: "多包 monorepo,含 apps/ 与 packages/ 目录",
|
||||
platform: "both",
|
||||
tech_stack: "Node,pnpm",
|
||||
tech_stack: r#"{"客户端":["Web"],"框架/工具":["Node","pnpm"]}"#,
|
||||
init_commands: "pnpm install",
|
||||
variants: None,
|
||||
files: vec![
|
||||
@ -439,7 +439,7 @@ pub fn seed_builtin_templates() {
|
||||
name: "pnpm App",
|
||||
description: "单包 Node.js 应用",
|
||||
platform: "both",
|
||||
tech_stack: "Node,pnpm",
|
||||
tech_stack: r#"{"客户端":["Web"],"框架/工具":["Node","pnpm"]}"#,
|
||||
init_commands: "pnpm install",
|
||||
variants: None,
|
||||
files: vec![
|
||||
@ -463,7 +463,7 @@ pub fn seed_builtin_templates() {
|
||||
name: "Rust",
|
||||
description: "Rust 二进制项目",
|
||||
platform: "both",
|
||||
tech_stack: "Rust",
|
||||
tech_stack: r#"{"客户端":["CLI","Desktop"],"语言":["Rust"]}"#,
|
||||
init_commands: "cargo build",
|
||||
variants: None,
|
||||
files: vec![
|
||||
@ -493,7 +493,7 @@ edition = "2021"
|
||||
name: "Tauri + React",
|
||||
description: "桌面应用,Tauri v2 + React 19 + TypeScript + Vite",
|
||||
platform: "both",
|
||||
tech_stack: "Rust,Node,pnpm,React,TypeScript,Tauri",
|
||||
tech_stack: r#"{"客户端":["Desktop"],"语言":["TypeScript","Rust"],"框架/工具":["React","Tauri","Node","pnpm"]}"#,
|
||||
init_commands: "pnpm install",
|
||||
variants: None,
|
||||
files: vec![
|
||||
@ -680,7 +680,7 @@ pub fn run() {
|
||||
name: "Expo",
|
||||
description: "React Native 移动端 App(Android / iOS)。TypeScript + Expo SDK,适合游戏、工具、内容类手机应用。选择下方模板变体来确定初始化方式。",
|
||||
platform: "both",
|
||||
tech_stack: "React Native,TypeScript,Expo",
|
||||
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"},
|
||||
@ -695,7 +695,7 @@ pub fn run() {
|
||||
name: "pnpm Monorepo + Expo",
|
||||
description: "pnpm 多包 Monorepo:apps/ 内含 Expo Router (React Native) 应用,packages/ 内含共享 @repo/types 包。Metro 已配置 monorepo 路径解析(含 Windows 兼容修复),适合多 App 扩展场景。",
|
||||
platform: "both",
|
||||
tech_stack: "React Native,TypeScript,Expo,pnpm",
|
||||
tech_stack: r#"{"客户端":["Android","iOS","Web"],"语言":["TypeScript"],"框架/工具":["React Native","Expo","pnpm"]}"#,
|
||||
init_commands: "pnpm install",
|
||||
variants: None,
|
||||
files: vec![
|
||||
@ -905,7 +905,7 @@ const styles = StyleSheet.create({
|
||||
name: "React + Vite SPA",
|
||||
description: "纯前端 Web 应用,React 19 + TypeScript + TailwindCSS v4 + React Router v7",
|
||||
platform: "both",
|
||||
tech_stack: "React,TypeScript,Vite,TailwindCSS",
|
||||
tech_stack: r#"{"客户端":["Web"],"语言":["TypeScript"],"框架/工具":["React","Vite","TailwindCSS"]}"#,
|
||||
init_commands: "pnpm install",
|
||||
variants: None,
|
||||
files: vec![
|
||||
@ -1020,7 +1020,7 @@ export default function App() {
|
||||
name: "Tauri + React + Tailwind",
|
||||
description: "桌面应用,Tauri v2 + React 19 + TypeScript + TailwindCSS v4",
|
||||
platform: "both",
|
||||
tech_stack: "Rust,Node,pnpm,React,TypeScript,Tauri,TailwindCSS",
|
||||
tech_stack: r#"{"客户端":["Desktop"],"语言":["TypeScript","Rust"],"框架/工具":["React","Tauri","TailwindCSS","Node","pnpm"]}"#,
|
||||
init_commands: "pnpm install",
|
||||
variants: None,
|
||||
files: vec![
|
||||
@ -1220,7 +1220,7 @@ pub fn run() {
|
||||
name: "Web + Mobile Monorepo",
|
||||
description: "跨平台代码复用:packages/core 共享业务逻辑,apps/web(Vite + Tailwind)+ apps/mobile(Expo + NativeWind)",
|
||||
platform: "both",
|
||||
tech_stack: "React,React Native,TypeScript,Expo,Vite,TailwindCSS,NativeWind,pnpm",
|
||||
tech_stack: r#"{"客户端":["Android","iOS","Web"],"语言":["TypeScript"],"框架/工具":["React","React Native","Expo","Vite","TailwindCSS","NativeWind","pnpm"]}"#,
|
||||
init_commands: "pnpm install",
|
||||
variants: None,
|
||||
files: vec![
|
||||
@ -1589,6 +1589,693 @@ export default function HomeScreen() {
|
||||
"#),
|
||||
],
|
||||
},
|
||||
// ── 三端体系模板(同一架构,按需起步)────────────────────────────────────
|
||||
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#"<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{project_name}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
"#),
|
||||
(".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(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
"#),
|
||||
("src/App.tsx", r#"import { Button } from "antd";
|
||||
import { useAppStore } from "./store";
|
||||
|
||||
export default function App() {
|
||||
const { count, increment } = useAppStore();
|
||||
return (
|
||||
<div style={{ padding: "2rem", fontFamily: "sans-serif" }}>
|
||||
<h1>{{project_name}}</h1>
|
||||
<p>React 18 + Ant Design 5 + Zustand</p>
|
||||
<Button type="primary" onClick={increment}>
|
||||
count: {count}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
"#),
|
||||
("src/store/index.ts", r#"import { create } from "zustand";
|
||||
|
||||
interface AppStore {
|
||||
count: number;
|
||||
increment: () => void;
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppStore>((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#"<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{project_name}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
"#),
|
||||
(".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(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
"#),
|
||||
("src/App.tsx", r#"import { Button } from "antd";
|
||||
import { useAppStore } from "./store";
|
||||
|
||||
export default function App() {
|
||||
const { count, increment } = useAppStore();
|
||||
return (
|
||||
<div style={{ padding: "2rem", fontFamily: "sans-serif" }}>
|
||||
<h1>{{project_name}}</h1>
|
||||
<p>React 18 + Ant Design 5 + Zustand + Capacitor</p>
|
||||
<Button type="primary" onClick={increment}>
|
||||
count: {count}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
"#),
|
||||
("src/store/index.ts", r#"import { create } from "zustand";
|
||||
|
||||
interface AppStore {
|
||||
count: number;
|
||||
increment: () => void;
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppStore>((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#"<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{project_name}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
"#),
|
||||
(".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(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
"#),
|
||||
("src/App.tsx", r#"import { Button } from "antd";
|
||||
import { useAppStore } from "./store";
|
||||
|
||||
export default function App() {
|
||||
const { count, increment } = useAppStore();
|
||||
return (
|
||||
<div style={{ padding: "2rem", fontFamily: "sans-serif" }}>
|
||||
<h1>{{project_name}}</h1>
|
||||
<p>React 18 + Ant Design 5 + Zustand + Tauri 2</p>
|
||||
<Button type="primary" onClick={increment}>
|
||||
count: {count}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
"#),
|
||||
("src/store/index.ts", r#"import { create } from "zustand";
|
||||
|
||||
interface AppStore {
|
||||
count: number;
|
||||
increment: () => void;
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppStore>((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#"<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{project_name}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
"#),
|
||||
(".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(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
"#),
|
||||
("src/App.tsx", r#"import { Button } from "antd";
|
||||
import { useAppStore } from "./store";
|
||||
|
||||
export default function App() {
|
||||
const { count, increment } = useAppStore();
|
||||
return (
|
||||
<div style={{ padding: "2rem", fontFamily: "sans-serif" }}>
|
||||
<h1>{{project_name}}</h1>
|
||||
<p>React 18 + Ant Design 5 + Zustand · Web / Android / Desktop</p>
|
||||
<Button type="primary" onClick={increment}>
|
||||
count: {count}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
"#),
|
||||
("src/store/index.ts", r#"import { create } from "zustand";
|
||||
|
||||
interface AppStore {
|
||||
count: number;
|
||||
increment: () => void;
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppStore>((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",
|
||||
@ -1709,9 +2396,15 @@ cases/ 是检验层:概念卡晋升 CORE 的依据来自真实案例。
|
||||
|
||||
for t in &templates {
|
||||
let _ = conn.execute(
|
||||
"INSERT OR IGNORE INTO project_templates
|
||||
"INSERT INTO project_templates
|
||||
(id, name, description, platform, tech_stack, init_commands, is_builtin, variants)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, 1, ?7)",
|
||||
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,
|
||||
@ -1722,16 +2415,11 @@ cases/ 是检验层:概念卡晋升 CORE 的依据来自真实案例。
|
||||
t.variants,
|
||||
],
|
||||
);
|
||||
// 已存在的模板也补充 variants(INSERT OR IGNORE 不更新已有行)
|
||||
let _ = conn.execute(
|
||||
"UPDATE project_templates SET variants = ?1 WHERE id = ?2 AND is_builtin = 1",
|
||||
params![t.variants, t.id],
|
||||
);
|
||||
|
||||
for (path, content) in &t.files {
|
||||
let file_id = format!("{}-{}", t.id, path.replace('/', "-"));
|
||||
let _ = conn.execute(
|
||||
"INSERT OR IGNORE INTO project_template_files (id, template_id, path, content)
|
||||
"INSERT OR REPLACE INTO project_template_files (id, template_id, path, content)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
params![file_id, t.id, path, content],
|
||||
);
|
||||
|
||||
@ -12,6 +12,7 @@ import { PriorityBadge, StatusBadge } from "../ui/Badge";
|
||||
import { Dialog } from "../ui/Dialog";
|
||||
import { TemplateEditor } from "./TemplateEditor";
|
||||
import { NewProjectModal } from "./NewProjectModal";
|
||||
import { renderTechStack } from "./techStackUtils";
|
||||
import { TagsManager } from "./TagsManager";
|
||||
import { RegistryTab } from "../dashboard/RepoRegistryModal";
|
||||
|
||||
@ -849,15 +850,7 @@ function TemplatesTab({ spaceId: _spaceId }: { spaceId?: string }) {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{t.techStack && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{t.techStack.split(",").map((tag) => (
|
||||
<span key={tag} className="text-[10px] px-1.5 py-0.5 rounded-full bg-indigo-50 text-indigo-500">
|
||||
{tag.trim()}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{t.techStack && renderTechStack(t.techStack)}
|
||||
{t.initCommands && (
|
||||
<code className="text-[11px] text-gray-400 font-mono">
|
||||
$ {t.initCommands.split("\n")[0]}
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
listTemplates, createProjectFromTemplate, TemplateVariant,
|
||||
} from "../../lib/commands";
|
||||
import { useUIStore } from "../../store/ui";
|
||||
import { renderTechStack } from "./techStackUtils";
|
||||
|
||||
interface Props {
|
||||
spaceId?: string;
|
||||
@ -286,15 +287,7 @@ export function NewProjectModal({ spaceId, onClose }: Props) {
|
||||
{t.description && (
|
||||
<p className="text-xs text-gray-400 mt-0.5">{t.description}</p>
|
||||
)}
|
||||
{t.techStack && (
|
||||
<div className="flex flex-wrap gap-1 mt-1.5">
|
||||
{t.techStack.split(",").map((tag) => (
|
||||
<span key={tag} className="text-[10px] px-1.5 py-0.5 rounded-full bg-indigo-50 text-indigo-500">
|
||||
{tag.trim()}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{t.techStack && <div className="mt-1.5">{renderTechStack(t.techStack)}</div>}
|
||||
{variants.length === 0 && t.initCommands && (
|
||||
<p className="text-[11px] text-gray-300 mt-1 font-mono">$ {t.initCommands.split("\n")[0]}</p>
|
||||
)}
|
||||
|
||||
@ -149,13 +149,13 @@ export function TemplateEditor({ templateId, onClose }: Props) {
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">技术栈标签(逗号分隔)</label>
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">技术栈标签</label>
|
||||
<input
|
||||
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-200"
|
||||
value={techStack}
|
||||
onChange={(e) => setTechStack(e.target.value)}
|
||||
disabled={isBuiltin}
|
||||
placeholder="Node,pnpm,TypeScript"
|
||||
placeholder='{"客户端":["Web"],"语言":["TypeScript"],"框架/工具":["React","Vite"]}'
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
|
||||
41
src/components/manage/techStackUtils.tsx
Normal file
41
src/components/manage/techStackUtils.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
const TECH_STACK_COLORS: Record<string, string> = {
|
||||
"体系": "bg-orange-50 text-orange-500",
|
||||
"客户端": "bg-green-50 text-green-600",
|
||||
"语言": "bg-blue-50 text-blue-600",
|
||||
"框架/工具": "bg-indigo-50 text-indigo-500",
|
||||
};
|
||||
|
||||
export function renderTechStack(techStack: string) {
|
||||
try {
|
||||
const parsed = JSON.parse(techStack) as Record<string, string[]>;
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{Object.entries(parsed).map(([cat, tags]) =>
|
||||
tags.length > 0 ? (
|
||||
<div key={cat} className="flex flex-wrap items-center gap-1">
|
||||
<span className="text-[9px] text-gray-400 mr-0.5">{cat}</span>
|
||||
{tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className={`text-[10px] px-1.5 py-0.5 rounded-full ${TECH_STACK_COLORS[cat] ?? "bg-indigo-50 text-indigo-500"}`}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} catch {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{techStack.split(",").map((tag) => (
|
||||
<span key={tag} className="text-[10px] px-1.5 py-0.5 rounded-full bg-indigo-50 text-indigo-500">
|
||||
{tag.trim()}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -614,6 +614,7 @@ export interface ProjectTemplate {
|
||||
name: string;
|
||||
description?: string;
|
||||
platform: string; // "win" | "wsl" | "both"
|
||||
/** 逗号字符串(旧格式)或分类 JSON `{"客户端":[],"语言":[],"框架/工具":[]}` */
|
||||
techStack?: string;
|
||||
initCommands?: string;
|
||||
isBuiltin: boolean;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user