diff --git a/src-tauri/src/commands/templates.rs b/src-tauri/src/commands/templates.rs index 1616adf..2678a3f 100644 --- a/src-tauri/src/commands/templates.rs +++ b/src-tauri/src/commands/templates.rs @@ -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#" + + + + + {{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", @@ -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], ); diff --git a/src/components/manage/ManagePage.tsx b/src/components/manage/ManagePage.tsx index e11de66..5911868 100644 --- a/src/components/manage/ManagePage.tsx +++ b/src/components/manage/ManagePage.tsx @@ -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 }) { )} - {t.techStack && ( -
- {t.techStack.split(",").map((tag) => ( - - {tag.trim()} - - ))} -
- )} + {t.techStack && renderTechStack(t.techStack)} {t.initCommands && ( $ {t.initCommands.split("\n")[0]} diff --git a/src/components/manage/NewProjectModal.tsx b/src/components/manage/NewProjectModal.tsx index 95e012e..a693669 100644 --- a/src/components/manage/NewProjectModal.tsx +++ b/src/components/manage/NewProjectModal.tsx @@ -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 && (

{t.description}

)} - {t.techStack && ( -
- {t.techStack.split(",").map((tag) => ( - - {tag.trim()} - - ))} -
- )} + {t.techStack &&
{renderTechStack(t.techStack)}
} {variants.length === 0 && t.initCommands && (

$ {t.initCommands.split("\n")[0]}

)} diff --git a/src/components/manage/TemplateEditor.tsx b/src/components/manage/TemplateEditor.tsx index 9e06f74..f98162a 100644 --- a/src/components/manage/TemplateEditor.tsx +++ b/src/components/manage/TemplateEditor.tsx @@ -149,13 +149,13 @@ export function TemplateEditor({ templateId, onClose }: Props) {
- + setTechStack(e.target.value)} disabled={isBuiltin} - placeholder="Node,pnpm,TypeScript" + placeholder='{"客户端":["Web"],"语言":["TypeScript"],"框架/工具":["React","Vite"]}' />
diff --git a/src/components/manage/techStackUtils.tsx b/src/components/manage/techStackUtils.tsx new file mode 100644 index 0000000..0ae2597 --- /dev/null +++ b/src/components/manage/techStackUtils.tsx @@ -0,0 +1,41 @@ +const TECH_STACK_COLORS: Record = { + "体系": "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; + return ( +
+ {Object.entries(parsed).map(([cat, tags]) => + tags.length > 0 ? ( +
+ {cat} + {tags.map((tag) => ( + + {tag} + + ))} +
+ ) : null + )} +
+ ); + } catch { + return ( +
+ {techStack.split(",").map((tag) => ( + + {tag.trim()} + + ))} +
+ ); + } +} diff --git a/src/lib/commands.ts b/src/lib/commands.ts index c1d48c1..18263b3 100644 --- a/src/lib/commands.ts +++ b/src/lib/commands.ts @@ -614,6 +614,7 @@ export interface ProjectTemplate { name: string; description?: string; platform: string; // "win" | "wsl" | "both" + /** 逗号字符串(旧格式)或分类 JSON `{"客户端":[],"语言":[],"框架/工具":[]}` */ techStack?: string; initCommands?: string; isBuiltin: boolean;