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:
lanrtop 2026-07-18 10:48:27 +09:00
parent 1bc0629827
commit 3d9336d908
6 changed files with 753 additions and 37 deletions

View File

@ -406,7 +406,7 @@ pub fn seed_builtin_templates() {
name: "pnpm Monorepo", name: "pnpm Monorepo",
description: "多包 monorepo含 apps/ 与 packages/ 目录", description: "多包 monorepo含 apps/ 与 packages/ 目录",
platform: "both", platform: "both",
tech_stack: "Node,pnpm", tech_stack: r#"{"":["Web"],"/":["Node","pnpm"]}"#,
init_commands: "pnpm install", init_commands: "pnpm install",
variants: None, variants: None,
files: vec![ files: vec![
@ -439,7 +439,7 @@ pub fn seed_builtin_templates() {
name: "pnpm App", name: "pnpm App",
description: "单包 Node.js 应用", description: "单包 Node.js 应用",
platform: "both", platform: "both",
tech_stack: "Node,pnpm", tech_stack: r#"{"":["Web"],"/":["Node","pnpm"]}"#,
init_commands: "pnpm install", init_commands: "pnpm install",
variants: None, variants: None,
files: vec![ files: vec![
@ -463,7 +463,7 @@ pub fn seed_builtin_templates() {
name: "Rust", name: "Rust",
description: "Rust 二进制项目", description: "Rust 二进制项目",
platform: "both", platform: "both",
tech_stack: "Rust", tech_stack: r#"{"":["CLI","Desktop"],"":["Rust"]}"#,
init_commands: "cargo build", init_commands: "cargo build",
variants: None, variants: None,
files: vec![ files: vec![
@ -493,7 +493,7 @@ edition = "2021"
name: "Tauri + React", name: "Tauri + React",
description: "桌面应用Tauri v2 + React 19 + TypeScript + Vite", description: "桌面应用Tauri v2 + React 19 + TypeScript + Vite",
platform: "both", platform: "both",
tech_stack: "Rust,Node,pnpm,React,TypeScript,Tauri", tech_stack: r#"{"":["Desktop"],"":["TypeScript","Rust"],"/":["React","Tauri","Node","pnpm"]}"#,
init_commands: "pnpm install", init_commands: "pnpm install",
variants: None, variants: None,
files: vec![ files: vec![
@ -680,7 +680,7 @@ pub fn run() {
name: "Expo", name: "Expo",
description: "React Native 移动端 AppAndroid / iOS。TypeScript + Expo SDK适合游戏、工具、内容类手机应用。选择下方模板变体来确定初始化方式。", description: "React Native 移动端 AppAndroid / iOS。TypeScript + Expo SDK适合游戏、工具、内容类手机应用。选择下方模板变体来确定初始化方式。",
platform: "both", 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", init_commands: "pnpm create expo-app@latest . --template blank-typescript",
variants: Some(r#"[ variants: Some(r#"[
{"label":"blank-typescript推荐","description":"最小化 TypeScript 起步,适合游戏类、工具类、独立 App","initCommands":"pnpm create expo-app@latest . --template blank-typescript"}, {"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", name: "pnpm Monorepo + Expo",
description: "pnpm 多包 Monorepoapps/ 内含 Expo Router (React Native) 应用packages/ 内含共享 @repo/types 包。Metro 已配置 monorepo 路径解析(含 Windows 兼容修复),适合多 App 扩展场景。", description: "pnpm 多包 Monorepoapps/ 内含 Expo Router (React Native) 应用packages/ 内含共享 @repo/types 包。Metro 已配置 monorepo 路径解析(含 Windows 兼容修复),适合多 App 扩展场景。",
platform: "both", platform: "both",
tech_stack: "React Native,TypeScript,Expo,pnpm", tech_stack: r#"{"":["Android","iOS","Web"],"":["TypeScript"],"/":["React Native","Expo","pnpm"]}"#,
init_commands: "pnpm install", init_commands: "pnpm install",
variants: None, variants: None,
files: vec![ files: vec![
@ -905,7 +905,7 @@ const styles = StyleSheet.create({
name: "React + Vite SPA", name: "React + Vite SPA",
description: "纯前端 Web 应用React 19 + TypeScript + TailwindCSS v4 + React Router v7", description: "纯前端 Web 应用React 19 + TypeScript + TailwindCSS v4 + React Router v7",
platform: "both", platform: "both",
tech_stack: "React,TypeScript,Vite,TailwindCSS", tech_stack: r#"{"":["Web"],"":["TypeScript"],"/":["React","Vite","TailwindCSS"]}"#,
init_commands: "pnpm install", init_commands: "pnpm install",
variants: None, variants: None,
files: vec![ files: vec![
@ -1020,7 +1020,7 @@ export default function App() {
name: "Tauri + React + Tailwind", name: "Tauri + React + Tailwind",
description: "桌面应用Tauri v2 + React 19 + TypeScript + TailwindCSS v4", description: "桌面应用Tauri v2 + React 19 + TypeScript + TailwindCSS v4",
platform: "both", 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", init_commands: "pnpm install",
variants: None, variants: None,
files: vec![ files: vec![
@ -1220,7 +1220,7 @@ pub fn run() {
name: "Web + Mobile Monorepo", name: "Web + Mobile Monorepo",
description: "跨平台代码复用packages/core 共享业务逻辑apps/webVite + Tailwind+ apps/mobileExpo + NativeWind", description: "跨平台代码复用packages/core 共享业务逻辑apps/webVite + Tailwind+ apps/mobileExpo + NativeWind",
platform: "both", 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", init_commands: "pnpm install",
variants: None, variants: None,
files: vec![ 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 8pnpm 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 2pnpm 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 { Tpl {
id: "builtin-knowledge-loop", id: "builtin-knowledge-loop",
@ -1709,9 +2396,15 @@ cases/ 是检验层:概念卡晋升 CORE 的依据来自真实案例。
for t in &templates { for t in &templates {
let _ = conn.execute( let _ = conn.execute(
"INSERT OR IGNORE INTO project_templates "INSERT INTO project_templates
(id, name, description, platform, tech_stack, init_commands, is_builtin, variants) (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![ params![
t.id, t.id,
t.name, t.name,
@ -1722,16 +2415,11 @@ cases/ 是检验层:概念卡晋升 CORE 的依据来自真实案例。
t.variants, t.variants,
], ],
); );
// 已存在的模板也补充 variantsINSERT 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 { for (path, content) in &t.files {
let file_id = format!("{}-{}", t.id, path.replace('/', "-")); let file_id = format!("{}-{}", t.id, path.replace('/', "-"));
let _ = conn.execute( 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)", VALUES (?1, ?2, ?3, ?4)",
params![file_id, t.id, path, content], params![file_id, t.id, path, content],
); );

View File

@ -12,6 +12,7 @@ import { PriorityBadge, StatusBadge } from "../ui/Badge";
import { Dialog } from "../ui/Dialog"; import { Dialog } from "../ui/Dialog";
import { TemplateEditor } from "./TemplateEditor"; import { TemplateEditor } from "./TemplateEditor";
import { NewProjectModal } from "./NewProjectModal"; import { NewProjectModal } from "./NewProjectModal";
import { renderTechStack } from "./techStackUtils";
import { TagsManager } from "./TagsManager"; import { TagsManager } from "./TagsManager";
import { RegistryTab } from "../dashboard/RepoRegistryModal"; import { RegistryTab } from "../dashboard/RepoRegistryModal";
@ -849,15 +850,7 @@ function TemplatesTab({ spaceId: _spaceId }: { spaceId?: string }) {
)} )}
</div> </div>
</div> </div>
{t.techStack && ( {t.techStack && renderTechStack(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.initCommands && ( {t.initCommands && (
<code className="text-[11px] text-gray-400 font-mono"> <code className="text-[11px] text-gray-400 font-mono">
$ {t.initCommands.split("\n")[0]} $ {t.initCommands.split("\n")[0]}

View File

@ -5,6 +5,7 @@ import {
listTemplates, createProjectFromTemplate, TemplateVariant, listTemplates, createProjectFromTemplate, TemplateVariant,
} from "../../lib/commands"; } from "../../lib/commands";
import { useUIStore } from "../../store/ui"; import { useUIStore } from "../../store/ui";
import { renderTechStack } from "./techStackUtils";
interface Props { interface Props {
spaceId?: string; spaceId?: string;
@ -286,15 +287,7 @@ export function NewProjectModal({ spaceId, onClose }: Props) {
{t.description && ( {t.description && (
<p className="text-xs text-gray-400 mt-0.5">{t.description}</p> <p className="text-xs text-gray-400 mt-0.5">{t.description}</p>
)} )}
{t.techStack && ( {t.techStack && <div className="mt-1.5">{renderTechStack(t.techStack)}</div>}
<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>
)}
{variants.length === 0 && t.initCommands && ( {variants.length === 0 && t.initCommands && (
<p className="text-[11px] text-gray-300 mt-1 font-mono">$ {t.initCommands.split("\n")[0]}</p> <p className="text-[11px] text-gray-300 mt-1 font-mono">$ {t.initCommands.split("\n")[0]}</p>
)} )}

View File

@ -149,13 +149,13 @@ export function TemplateEditor({ templateId, onClose }: Props) {
</select> </select>
</div> </div>
<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 <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" 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} value={techStack}
onChange={(e) => setTechStack(e.target.value)} onChange={(e) => setTechStack(e.target.value)}
disabled={isBuiltin} disabled={isBuiltin}
placeholder="Node,pnpm,TypeScript" placeholder='{"客户端":["Web"],"语言":["TypeScript"],"框架/工具":["React","Vite"]}'
/> />
</div> </div>
<div className="col-span-2"> <div className="col-span-2">

View 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>
);
}
}

View File

@ -614,6 +614,7 @@ export interface ProjectTemplate {
name: string; name: string;
description?: string; description?: string;
platform: string; // "win" | "wsl" | "both" platform: string; // "win" | "wsl" | "both"
/** 逗号字符串(旧格式)或分类 JSON `{"客户端":[],"语言":[],"框架/工具":[]}` */
techStack?: string; techStack?: string;
initCommands?: string; initCommands?: string;
isBuiltin: boolean; isBuiltin: boolean;