├── bin └── starProject ├── web ├── public │ └── icon.webp ├── postcss.config.js ├── src │ ├── App.jsx │ ├── main.jsx │ ├── components │ │ ├── layout │ │ │ ├── Layout.jsx │ │ │ ├── Footer.jsx │ │ │ └── Header.jsx │ │ ├── ui │ │ │ ├── Loading.jsx │ │ │ ├── Card.jsx │ │ │ ├── Button.jsx │ │ │ ├── Input.jsx │ │ │ └── SearchAndFilter.jsx │ │ └── project │ │ │ └── ProjectCard.jsx │ ├── App.css │ ├── index.css │ ├── assets │ │ └── react.svg │ └── pages │ │ └── HomePage.jsx ├── vite.config.js ├── .gitignore ├── index.html ├── README.md ├── package.json ├── eslint.config.js └── tailwind.config.js ├── .gitignore ├── README_CN.md ├── go.mod ├── README.md ├── model └── star_model.go ├── go.sum ├── .github └── workflows │ └── auto-compile-and-commit.yml ├── .trae └── documents │ ├── notion-style-github-showcase-prd.md │ └── notion-style-technical-architecture.md └── main.go /bin/starProject: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mran/githubstartog/HEAD/bin/starProject -------------------------------------------------------------------------------- /web/public/icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mran/githubstartog/HEAD/web/public/icon.webp -------------------------------------------------------------------------------- /web/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /main_test.go 2 | /.idea/.gitignore 3 | /.idea/deployment.xml 4 | /.idea/githubstartog.iml 5 | *.exe 6 | /.idea/modules.xml 7 | /.idea/vcs.xml 8 | -------------------------------------------------------------------------------- /web/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import HomePage from './pages/HomePage'; 3 | 4 | const App = () => { 5 | return ; 6 | }; 7 | 8 | export default App; 9 | -------------------------------------------------------------------------------- /web/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /web/src/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.jsx' 5 | 6 | createRoot(document.getElementById('root')).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | My Github start page 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 |

2 | DEMO 3 |

4 | 5 | [中文]("./README_CN.md") 6 | 7 | ## 项目描述 8 | 9 | GithubStartog 通过 Github API 获取用户的标星项目,根据项目的标签,并生成一个 Markdown 文件和前端页面。 10 | [DEMO](https://githubstartog.pages.dev/) 11 | 12 | ## 如何使用 13 | 14 | 1. fork 本项目 15 | 2. 在 `settings` > `Actions` > `General` 中,启用 Github Actions,允许读写权限 16 | 3. 手动启动 Github Actions 完成构建 17 | 4. 使用 Cloudflare Pages 部署 -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module githubstartog 2 | 3 | go 1.21 4 | 5 | toolchain go1.23.4 6 | 7 | require ( 8 | github.com/go-resty/resty/v2 v2.16.2 9 | github.com/openai/openai-go v0.1.0-alpha.39 10 | github.com/sashabaranov/go-openai v1.36.0 11 | ) 12 | 13 | require ( 14 | github.com/tidwall/gjson v1.14.4 // indirect 15 | github.com/tidwall/match v1.1.1 // indirect 16 | github.com/tidwall/pretty v1.2.1 // indirect 17 | github.com/tidwall/sjson v1.2.5 // indirect 18 | golang.org/x/net v0.27.0 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | DEMO 3 |

4 | 5 | ## Project Description 6 | 7 | GithubStartog uses the Github API to get the starred projects of a user, generate a Markdown file and a frontend page based on the tags of the projects. 8 | [DEMO](https://githubstartog.pages.dev/) 9 | 10 | ## How to use 11 | 12 | 1. Fork this project 13 | 2. Enable Github Actions in `settings` > `Actions` > `General`>`Read and write permissions` 14 | 3. Wait for Github Actions to finish building 15 | 4. Deploy with Cloudflare Pages -------------------------------------------------------------------------------- /web/src/components/layout/Layout.jsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | import Header from './Header'; 3 | import Footer from './Footer'; 4 | 5 | const Layout = ({ children }) => { 6 | return ( 7 |
8 |
9 | 15 | {children} 16 | 17 |
18 |
19 | ); 20 | }; 21 | 22 | export default Layout; -------------------------------------------------------------------------------- /web/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | padding: 2rem; 3 | text-align: center; 4 | } 5 | 6 | .logo { 7 | height: 6em; 8 | padding: 1.5em; 9 | will-change: filter; 10 | transition: filter 300ms; 11 | } 12 | .logo:hover { 13 | filter: drop-shadow(0 0 2em #646cffaa); 14 | } 15 | .logo.react:hover { 16 | filter: drop-shadow(0 0 2em #61dafbaa); 17 | } 18 | 19 | @keyframes logo-spin { 20 | from { 21 | transform: rotate(0deg); 22 | } 23 | to { 24 | transform: rotate(360deg); 25 | } 26 | } 27 | 28 | @media (prefers-reduced-motion: no-preference) { 29 | a:nth-of-type(2) .logo { 30 | animation: logo-spin infinite 20s linear; 31 | } 32 | } 33 | 34 | .card { 35 | padding: 2em; 36 | } 37 | 38 | .read-the-docs { 39 | color: #888; 40 | } 41 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "framer-motion": "^12.23.12", 14 | "fuse.js": "^7.1.0", 15 | "lucide-react": "^0.542.0", 16 | "react": "^18.3.1", 17 | "react-dom": "^18.3.1", 18 | "react-router-dom": "^7.8.2", 19 | "typescript": "^5.9.2" 20 | }, 21 | "devDependencies": { 22 | "@eslint/js": "^9.15.0", 23 | "@types/react": "^18.3.24", 24 | "@types/react-dom": "^18.3.7", 25 | "@vitejs/plugin-react": "^4.3.4", 26 | "autoprefixer": "^10.4.20", 27 | "eslint": "^9.15.0", 28 | "eslint-plugin-react": "^7.37.2", 29 | "eslint-plugin-react-hooks": "^5.0.0", 30 | "eslint-plugin-react-refresh": "^0.4.14", 31 | "globals": "^15.12.0", 32 | "postcss": "^8.4.49", 33 | "tailwindcss": "^3.4.16", 34 | "vite": "^6.0.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /web/src/components/ui/Loading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { motion } from 'framer-motion'; 3 | 4 | const Loading = ({ size = 'medium', text = '加载中...' }) => { 5 | const sizeClasses = { 6 | small: 'w-6 h-6', 7 | medium: 'w-12 h-12', 8 | large: 'w-16 h-16' 9 | }; 10 | 11 | return ( 12 |
13 | 22 | {text && ( 23 | 29 | {text} 30 | 31 | )} 32 |
33 | ); 34 | }; 35 | 36 | export default Loading; -------------------------------------------------------------------------------- /web/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import reactHooks from 'eslint-plugin-react-hooks' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default [ 8 | { ignores: ['dist'] }, 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/jsx-no-target-blank': 'off', 32 | 'react-refresh/only-export-components': [ 33 | 'warn', 34 | { allowConstantExport: true }, 35 | ], 36 | }, 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /model/star_model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | type StarInfo struct { 6 | Name string `json:"name"` 7 | FullName string `json:"full_name"` 8 | Url string `json:"url"` 9 | HtmlUrl string `json:"html_url"` 10 | Description *string `json:"description"` 11 | Homepage *string `json:"homepage"` 12 | Language *string `json:"language"` 13 | Topics []string `json:"topics"` 14 | StargazersCount int `json:"stargazers_count"` 15 | UpdatedAt time.Time `json:"updated_at"` 16 | AI_tag AI_tag `json:"ai_tag"` 17 | } 18 | type ReadmeData struct { 19 | Name string `json:"name"` 20 | Path string `json:"path"` 21 | Sha string `json:"sha"` 22 | Size int `json:"size"` 23 | Url string `json:"url"` 24 | HtmlUrl string `json:"html_url"` 25 | GitUrl string `json:"git_url"` 26 | DownloadUrl string `json:"download_url"` 27 | Type string `json:"type"` 28 | Content string `json:"content"` 29 | RawContent string 30 | 31 | Encoding string `json:"encoding"` 32 | Links struct { 33 | Self string `json:"self"` 34 | Git string `json:"git"` 35 | Html string `json:"html"` 36 | } `json:"_links"` 37 | } 38 | type AI_tag struct { 39 | Group string `json:"group"` 40 | Tags []string `json:"tags"` 41 | Desc string `json:"desc"` 42 | } 43 | -------------------------------------------------------------------------------- /web/src/components/ui/Card.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { motion } from 'framer-motion'; 3 | 4 | const Card = ({ 5 | children, 6 | className = '', 7 | hover = true, 8 | padding = 'medium', 9 | shadow = 'medium', 10 | onClick, 11 | ...props 12 | }) => { 13 | const baseClasses = 'bg-white border border-notion-gray-200 rounded-lg transition-all duration-200'; 14 | 15 | const paddingClasses = { 16 | none: '', 17 | small: 'p-4', 18 | medium: 'p-6', 19 | large: 'p-8' 20 | }; 21 | 22 | const shadowClasses = { 23 | none: '', 24 | small: 'shadow-sm', 25 | medium: 'shadow-notion-card', 26 | large: 'shadow-lg' 27 | }; 28 | 29 | const hoverClasses = hover ? 'hover:shadow-notion-card-hover hover:border-notion-gray-300' : ''; 30 | const clickableClasses = onClick ? 'cursor-pointer' : ''; 31 | 32 | const cardClasses = ` 33 | ${baseClasses} 34 | ${paddingClasses[padding]} 35 | ${shadowClasses[shadow]} 36 | ${hoverClasses} 37 | ${clickableClasses} 38 | ${className} 39 | `.trim(); 40 | 41 | const cardVariants = { 42 | initial: { opacity: 0, y: 20 }, 43 | animate: { opacity: 1, y: 0 }, 44 | hover: hover ? { y: -2, scale: 1.01 } : {}, 45 | tap: onClick ? { scale: 0.98 } : {} 46 | }; 47 | 48 | return ( 49 | 60 | {children} 61 | 62 | ); 63 | }; 64 | 65 | export default Card; -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg= 2 | github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= 3 | github.com/openai/openai-go v0.1.0-alpha.39 h1:FvoNWy7BPhA0TjGOK5huRGU5sAUEx2jeubLXz34K9LE= 4 | github.com/openai/openai-go v0.1.0-alpha.39/go.mod h1:3SdE6BffOX9HPEQv8IL/fi3LYZ5TUpRYaqGQZbyk11A= 5 | github.com/sashabaranov/go-openai v1.36.0 h1:fcSrn8uGuorzPWCBp8L0aCR95Zjb/Dd+ZSML0YZy9EI= 6 | github.com/sashabaranov/go-openai v1.36.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= 7 | github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 8 | github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= 9 | github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 10 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 11 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 12 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 13 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 14 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 15 | github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= 16 | github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 17 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 18 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 19 | golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= 20 | golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 21 | -------------------------------------------------------------------------------- /web/src/components/ui/Button.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { motion } from 'framer-motion'; 3 | 4 | const Button = ({ 5 | children, 6 | variant = 'primary', 7 | size = 'medium', 8 | onClick, 9 | disabled = false, 10 | className = '', 11 | ...props 12 | }) => { 13 | const baseClasses = 'inline-flex items-center justify-center font-medium rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2'; 14 | 15 | const variants = { 16 | primary: 'bg-notion-blue-600 text-white hover:bg-notion-blue-700 focus:ring-notion-blue-500 shadow-sm', 17 | secondary: 'bg-notion-gray-100 text-notion-gray-700 hover:bg-notion-gray-200 focus:ring-notion-gray-500 border border-notion-gray-300', 18 | ghost: 'text-notion-gray-600 hover:bg-notion-gray-100 hover:text-notion-gray-900 focus:ring-notion-gray-500', 19 | danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500 shadow-sm' 20 | }; 21 | 22 | const sizes = { 23 | small: 'px-3 py-1.5 text-sm', 24 | medium: 'px-4 py-2 text-sm', 25 | large: 'px-6 py-3 text-base' 26 | }; 27 | 28 | const disabledClasses = 'opacity-50 cursor-not-allowed'; 29 | 30 | const buttonClasses = ` 31 | ${baseClasses} 32 | ${variants[variant]} 33 | ${sizes[size]} 34 | ${disabled ? disabledClasses : ''} 35 | ${className} 36 | `.trim(); 37 | 38 | return ( 39 | 48 | {children} 49 | 50 | ); 51 | }; 52 | 53 | export default Button; -------------------------------------------------------------------------------- /web/src/components/ui/Input.jsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | import { motion } from 'framer-motion'; 3 | 4 | const Input = forwardRef(({ 5 | type = 'text', 6 | placeholder, 7 | value, 8 | onChange, 9 | onFocus, 10 | onBlur, 11 | disabled = false, 12 | error = false, 13 | className = '', 14 | icon: Icon, 15 | ...props 16 | }, ref) => { 17 | const baseClasses = 'w-full px-4 py-2.5 text-notion-gray-900 bg-white border rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-0'; 18 | 19 | const stateClasses = error 20 | ? 'border-red-300 focus:border-red-500 focus:ring-red-200' 21 | : 'border-notion-gray-300 focus:border-notion-blue-500 focus:ring-notion-blue-200 hover:border-notion-gray-400'; 22 | 23 | const disabledClasses = 'opacity-50 cursor-not-allowed bg-notion-gray-50'; 24 | 25 | const inputClasses = ` 26 | ${baseClasses} 27 | ${stateClasses} 28 | ${disabled ? disabledClasses : ''} 29 | ${Icon ? 'pl-10' : ''} 30 | ${className} 31 | `.trim(); 32 | 33 | return ( 34 |
35 | {Icon && ( 36 |
37 | 38 |
39 | )} 40 | 54 |
55 | ); 56 | }); 57 | 58 | Input.displayName = 'Input'; 59 | 60 | export default Input; -------------------------------------------------------------------------------- /web/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: { 9 | fontFamily: { 10 | 'sans': ['Inter', 'system-ui', 'sans-serif'], 11 | 'mono': ['JetBrains Mono', 'monospace'], 12 | }, 13 | colors: { 14 | notion: { 15 | blue: '#2383E2', 16 | orange: '#FF6B35', 17 | gray: { 18 | 50: '#F7F6F3', 19 | 100: '#E9E9E7', 20 | 200: '#CBCAC8', 21 | 300: '#A0A0A0', 22 | 400: '#6F6F6F', 23 | 500: '#2F2F2F', 24 | } 25 | } 26 | }, 27 | animation: { 28 | 'fade-in': 'fadeIn 0.3s ease-out', 29 | 'slide-up': 'slideUp 0.3s ease-out', 30 | 'scale-in': 'scaleIn 0.2s ease-out', 31 | }, 32 | keyframes: { 33 | fadeIn: { 34 | '0%': { opacity: '0' }, 35 | '100%': { opacity: '1' }, 36 | }, 37 | slideUp: { 38 | '0%': { transform: 'translateY(10px)', opacity: '0' }, 39 | '100%': { transform: 'translateY(0)', opacity: '1' }, 40 | }, 41 | scaleIn: { 42 | '0%': { transform: 'scale(0.95)', opacity: '0' }, 43 | '100%': { transform: 'scale(1)', opacity: '1' }, 44 | }, 45 | }, 46 | boxShadow: { 47 | 'notion': '0 8px 32px rgba(0, 0, 0, 0.12)', 48 | 'notion-hover': '0 12px 48px rgba(0, 0, 0, 0.18)', 49 | }, 50 | borderRadius: { 51 | 'notion-sm': '8px', 52 | 'notion-md': '12px', 53 | 'notion-lg': '16px', 54 | }, 55 | spacing: { 56 | '18': '4.5rem', 57 | '88': '22rem', 58 | }, 59 | transitionDuration: { 60 | '250': '250ms', 61 | }, 62 | }, 63 | }, 64 | plugins: [], 65 | } 66 | 67 | -------------------------------------------------------------------------------- /.github/workflows/auto-compile-and-commit.yml: -------------------------------------------------------------------------------- 1 | name: Go Build and Compile 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | # 可以添加可选的输入参数 9 | inputs: 10 | reason: 11 | description: 'Reason for manual run' 12 | required: false 13 | default: 'Manual trigger' 14 | jobs: 15 | build-and-compile: 16 | runs-on: ubuntu-latest 17 | # 设置权限,允许 push 18 | permissions: 19 | contents: write 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 # 使用最新版本 24 | 25 | - name: Set up Go environment 26 | uses: actions/setup-go@v5 # 使用最新版本 27 | with: 28 | go-version: '1.23' 29 | check-latest: true 30 | 31 | # 使用环境变量 32 | - name: Build and compile 33 | env: 34 | LLM_BASEURL: ${{ secrets.LLM_BASEURL }} 35 | LLM_TOKEN: ${{ secrets.LLM_TOKEN }} 36 | run: | 37 | go build -o ./bin/starProject 38 | 39 | - name: Execute compiled binary 40 | env: 41 | LLM_BASEURL: ${{ secrets.LLM_BASEURL }} 42 | LLM_TOKEN: ${{ secrets.LLM_TOKEN }} 43 | USERNAME: ${{ github.repository_owner }} 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | run: | 46 | ./bin/starProject 47 | - name: Copy aiTagProcess.json to web/public 48 | run: | 49 | cp aiTagProcess.json web/public/projects.json 50 | 51 | - name: Commit all files 52 | # 使用 GITHUB_TOKEN 进行 git 操作 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | run: | 56 | git config --global user.name "github-actions[bot]" 57 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 58 | git add . 59 | git commit -m "Automated commit after execution" || echo "No changes to commit" 60 | git push -------------------------------------------------------------------------------- /.trae/documents/notion-style-github-showcase-prd.md: -------------------------------------------------------------------------------- 1 | # GitHub项目展示平台 - Notion风格重设计 2 | 3 | ## 1. 产品概述 4 | 5 | 这不是一个简单的GitHub项目列表,而是一个具有Notion级别设计标准的知识展示平台。我们要打造一个让开发者沉浸其中、优雅浏览项目的数字空间。 6 | 7 | 产品的核心使命是将冰冷的代码仓库转化为有温度的知识资产,通过极简主义的设计语言和精致的交互体验,让每个项目都能得到应有的展示。 8 | 9 | 目标是成为开发者个人品牌展示的新标杆,重新定义技术项目的呈现方式。 10 | 11 | ## 2. 核心功能 12 | 13 | ### 2.1 用户角色 14 | 15 | 本产品采用单一用户模式,专注于为项目所有者提供最佳的展示体验。 16 | 17 | ### 2.2 功能模块 18 | 19 | 我们的重设计包含以下核心页面: 20 | 21 | 1. **主展示页面**:沉浸式项目浏览体验,智能搜索与筛选系统 22 | 2. **项目详情页面**:深度项目信息展示,优雅的代码预览 23 | 3. **关于页面**:个人品牌故事展示 24 | 25 | ### 2.3 页面详情 26 | 27 | | 页面名称 | 模块名称 | 功能描述 | 28 | |---------|---------|----------| 29 | | 主展示页面 | 顶部导航栏 | 极简logo展示,搜索入口,主题切换。采用Notion式的悬浮设计 | 30 | | 主展示页面 | 智能搜索系统 | 实时模糊搜索,智能标签建议,搜索历史。支持快捷键操作 | 31 | | 主展示页面 | 项目卡片网格 | 响应式瀑布流布局,悬停微动效,渐进式加载。每个卡片都是艺术品 | 32 | | 主展示页面 | 高级筛选面板 | 侧边栏筛选器,技术栈分类,时间轴筛选,星标数排序 | 33 | | 主展示页面 | 统计仪表板 | 项目总览数据,技术栈分布图,活跃度热力图 | 34 | | 项目详情页面 | 项目头部信息 | 大标题展示,关键指标可视化,快速操作按钮组 | 35 | | 项目详情页面 | 代码预览模块 | 语法高亮代码展示,文件树导航,在线预览功能 | 36 | | 项目详情页面 | 项目时间线 | 提交历史可视化,里程碑展示,贡献者信息 | 37 | | 关于页面 | 个人简介区域 | 头像展示,技能标签云,联系方式,个人故事 | 38 | | 关于页面 | 技术栈展示 | 交互式技能图表,经验时长可视化,项目关联 | 39 | 40 | ## 3. 核心流程 41 | 42 | **主要用户操作流程:** 43 | 44 | 用户进入主页 → 被优雅的视觉设计吸引 → 通过智能搜索或浏览发现感兴趣的项目 → 点击项目卡片进入详情页 → 深度了解项目信息 → 通过外链访问GitHub仓库或在线演示 → 返回主页继续探索或访问关于页面了解作者 45 | 46 | **页面导航流程图:** 47 | 48 | ```mermaid 49 | graph TD 50 | A[主展示页面] --> B[项目详情页面] 51 | A --> C[关于页面] 52 | B --> A 53 | B --> D[GitHub仓库] 54 | B --> E[在线演示] 55 | C --> A 56 | C --> F[联系方式] 57 | ``` 58 | 59 | ## 4. 用户界面设计 60 | 61 | ### 4.1 设计风格 62 | 63 | **色彩系统:** 64 | - 主色调:纯净白色 (#FFFFFF) 和深邃黑色 (#000000) 65 | - 辅助色:温暖灰色系列 (#F7F6F3, #E9E9E7, #CBCAC8) 66 | - 强调色:Notion蓝 (#2383E2) 和警示橙 (#FF6B35) 67 | - 文字色彩:主文字 (#2F2F2F),次要文字 (#6F6F6F),辅助文字 (#A0A0A0) 68 | 69 | **字体系统:** 70 | - 主字体:Inter (现代无衬线,极佳的屏幕显示效果) 71 | - 代码字体:JetBrains Mono (等宽字体,代码展示专用) 72 | - 字号层级:12px/14px/16px/20px/24px/32px/48px 73 | 74 | **设计原则:** 75 | - 按钮风格:圆角8px,悬停状态微妙阴影,点击反馈 76 | - 布局风格:大量留白,内容居中,最大宽度1200px 77 | - 动效风格:缓动函数 ease-out,持续时间200-300ms 78 | - 图标风格:线性图标,2px描边,24px标准尺寸 79 | 80 | ### 4.2 页面设计概览 81 | 82 | | 页面名称 | 模块名称 | UI元素 | 83 | |---------|---------|--------| 84 | | 主展示页面 | 顶部导航栏 | 高度64px,背景模糊效果,固定定位。Logo使用Inter字体,搜索框圆角12px | 85 | | 主展示页面 | 项目卡片 | 白色背景,圆角16px,悬停阴影0 8px 32px rgba(0,0,0,0.12)。标题使用Inter Medium 18px | 86 | | 主展示页面 | 筛选面板 | 侧边栏宽度280px,背景#F7F6F3,分组标题Inter SemiBold 14px | 87 | | 项目详情页面 | 项目头部 | 大标题Inter Bold 32px,指标卡片使用圆角12px,间距24px | 88 | | 项目详情页面 | 代码预览 | 背景#1E1E1E,圆角12px,JetBrains Mono 14px,行高1.5 | 89 | | 关于页面 | 个人简介 | 头像圆形120px,技能标签圆角20px,背景渐变色 | 90 | 91 | ### 4.3 响应式设计 92 | 93 | 产品采用移动优先的响应式设计策略: 94 | - 桌面端:1200px+ 三列网格布局,侧边栏固定 95 | - 平板端:768px-1199px 两列网格,侧边栏可收起 96 | - 移动端:<768px 单列布局,底部导航,手势优化 97 | 98 | 支持触摸交互优化,包括滑动切换、长按菜单、双击缩放等移动端专属交互。 -------------------------------------------------------------------------------- /web/src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap'); 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | /* Notion风格的CSS变量 */ 7 | :root { 8 | --spacing-unit: 8px; 9 | --border-radius-sm: 8px; 10 | --border-radius-md: 12px; 11 | --border-radius-lg: 16px; 12 | --transition-fast: 0.15s ease-out; 13 | --transition-normal: 0.3s ease-out; 14 | --transition-slow: 0.5s ease-out; 15 | 16 | /* 字体设置 */ 17 | font-family: 'Inter', system-ui, -apple-system, sans-serif; 18 | line-height: 1.6; 19 | font-weight: 400; 20 | 21 | /* 渲染优化 */ 22 | font-synthesis: none; 23 | text-rendering: optimizeLegibility; 24 | -webkit-font-smoothing: antialiased; 25 | -moz-osx-font-smoothing: grayscale; 26 | 27 | /* 颜色主题 */ 28 | color: #2F2F2F; 29 | background-color: #FFFFFF; 30 | } 31 | 32 | /* 全局样式重置 */ 33 | * { 34 | box-sizing: border-box; 35 | } 36 | 37 | body { 38 | margin: 0; 39 | padding: 0; 40 | min-height: 100vh; 41 | background-color: #FFFFFF; 42 | color: #2F2F2F; 43 | } 44 | 45 | /* Notion风格的通用组件样式 */ 46 | @layer components { 47 | .notion-card { 48 | @apply bg-white rounded-notion-lg shadow-notion hover:shadow-notion-hover; 49 | @apply transition-all duration-300 ease-out; 50 | @apply border border-gray-100; 51 | } 52 | 53 | .notion-button { 54 | @apply px-4 py-2 rounded-notion-md font-medium; 55 | @apply transition-all duration-200 ease-out; 56 | @apply focus:outline-none focus:ring-2 focus:ring-notion-blue focus:ring-opacity-50; 57 | } 58 | 59 | .notion-input { 60 | @apply w-full px-4 py-3 rounded-notion-md border border-gray-200; 61 | @apply focus:border-notion-blue focus:ring-2 focus:ring-notion-blue focus:ring-opacity-20; 62 | @apply transition-all duration-200 ease-out; 63 | @apply font-sans placeholder-notion-gray-300; 64 | } 65 | 66 | .notion-text-primary { 67 | @apply text-notion-gray-500; 68 | } 69 | 70 | .notion-text-secondary { 71 | @apply text-notion-gray-400; 72 | } 73 | 74 | .notion-text-tertiary { 75 | @apply text-notion-gray-300; 76 | } 77 | } 78 | 79 | /* 链接样式 */ 80 | a { 81 | color: #2383E2; 82 | text-decoration: none; 83 | transition: color var(--transition-fast); 84 | } 85 | 86 | a:hover { 87 | color: #1a6bc7; 88 | } 89 | 90 | /* 标题样式 */ 91 | h1, h2, h3, h4, h5, h6 { 92 | font-family: 'Inter', sans-serif; 93 | font-weight: 600; 94 | line-height: 1.3; 95 | margin: 0; 96 | } 97 | 98 | /* 滚动条样式 */ 99 | ::-webkit-scrollbar { 100 | width: 6px; 101 | height: 6px; 102 | } 103 | 104 | ::-webkit-scrollbar-track { 105 | background: #F7F6F3; 106 | } 107 | 108 | ::-webkit-scrollbar-thumb { 109 | background: #CBCAC8; 110 | border-radius: 3px; 111 | } 112 | 113 | ::-webkit-scrollbar-thumb:hover { 114 | background: #A0A0A0; 115 | } 116 | -------------------------------------------------------------------------------- /web/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/components/layout/Footer.jsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | import { Github, Twitter, Linkedin, Mail } from 'lucide-react'; 3 | 4 | const Footer = () => { 5 | const socialLinks = [ 6 | { icon: Github, href: '#', label: 'GitHub' }, 7 | { icon: Twitter, href: '#', label: 'Twitter' }, 8 | { icon: Linkedin, href: '#', label: 'LinkedIn' }, 9 | { icon: Mail, href: '#', label: 'Email' }, 10 | ]; 11 | 12 | return ( 13 | 106 | ); 107 | }; 108 | 109 | export default Footer; -------------------------------------------------------------------------------- /web/src/components/layout/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { motion, AnimatePresence } from 'framer-motion'; 3 | import { Search, Menu, X, Github } from 'lucide-react'; 4 | import Button from '../ui/Button'; 5 | 6 | const Header = () => { 7 | const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); 8 | 9 | return ( 10 | 16 |
17 |
18 | {/* Logo */} 19 | 24 |
25 | 26 |
27 | 28 | Projects 29 | 30 |
31 | 32 | {/* Desktop Navigation */} 33 | 59 | 60 | {/* Search and Mobile Menu */} 61 |
62 | 69 | 70 | 82 |
83 |
84 | 85 |
86 | 87 | {/* Mobile Navigation Menu */} 88 | 89 | {isMobileMenuOpen && ( 90 | 97 | 123 | 124 | )} 125 | 126 |
127 | ); 128 | }; 129 | 130 | export default Header; -------------------------------------------------------------------------------- /web/src/components/project/ProjectCard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { motion } from 'framer-motion'; 3 | import { Star, GitFork, ExternalLink, Calendar, Github } from 'lucide-react'; 4 | import Card from '../ui/Card'; 5 | import Button from '../ui/Button'; 6 | 7 | const ProjectCard = ({ project, index = 0 }) => { 8 | // Merge tags from different sources 9 | const allTags = [ 10 | ...(project.topics || []), 11 | ...(project.ai_tag?.tags || []), 12 | project.language 13 | ].filter(Boolean); 14 | 15 | // Notion-style tag colors 16 | const getTagStyle = (tag) => { 17 | const tagStyles = { 18 | 'JavaScript': 'bg-yellow-50 text-yellow-700 border-yellow-200', 19 | 'TypeScript': 'bg-blue-50 text-blue-700 border-blue-200', 20 | 'React': 'bg-cyan-50 text-cyan-700 border-cyan-200', 21 | 'Vue': 'bg-green-50 text-green-700 border-green-200', 22 | 'Python': 'bg-emerald-50 text-emerald-700 border-emerald-200', 23 | 'Java': 'bg-red-50 text-red-700 border-red-200', 24 | 'CSS': 'bg-indigo-50 text-indigo-700 border-indigo-200', 25 | 'HTML': 'bg-orange-50 text-orange-700 border-orange-200', 26 | 'Node.js': 'bg-lime-50 text-lime-700 border-lime-200', 27 | 'default': 'bg-notion-gray-50 text-notion-gray-600 border-notion-gray-200' 28 | }; 29 | return tagStyles[tag] || tagStyles.default; 30 | }; 31 | 32 | // Format date 33 | const formatDate = (dateString) => { 34 | return new Date(dateString).toLocaleDateString('en-US', { 35 | year: 'numeric', 36 | month: 'short', 37 | day: 'numeric' 38 | }); 39 | }; 40 | 41 | return ( 42 | 47 | {/* Card Header */} 48 |
49 |
50 | 54 | 60 | {project.name || project.full_name} 61 | 62 | 63 | 64 | {/* Star Count */} 65 | {project.stargazers_count !== undefined && ( 66 | 70 | 71 | {project.stargazers_count} 72 | 73 | )} 74 |
75 | 76 | {/* Description */} 77 |

78 | {project.description || 'No description available'} 79 |

80 | 81 | {/* Tags */} 82 | {allTags.length > 0 && ( 83 |
84 | {allTags.slice(0, 4).map((tag) => ( 85 | 90 | {tag} 91 | 92 | ))} 93 | {allTags.length > 4 && ( 94 | 95 | +{allTags.length - 4} 96 | 97 | )} 98 |
99 | )} 100 |
101 | 102 | {/* Card Footer */} 103 |
104 |
105 | {/* Last Updated */} 106 |
107 | 108 | 109 | {project.updated_at ? formatDate(project.updated_at) : 'Unknown'} 110 | 111 |
112 | 113 | {/* Action Buttons */} 114 |
115 | 124 | {project.homepage && ( 125 | 134 | )} 135 |
136 |
137 |
138 |
139 | ); 140 | }; 141 | 142 | export default ProjectCard; -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "github.com/go-resty/resty/v2" 9 | "github.com/openai/openai-go" 10 | "github.com/openai/openai-go/option" 11 | "githubstartog/model" 12 | "os" 13 | "strconv" 14 | "strings" 15 | "sync" 16 | //"github.com/sashabaranov/go-openai" 17 | ) 18 | 19 | func FetchUserStar(user string, page int) *[]model.StarInfo { 20 | client := resty.New() 21 | // 请求https://api.github.com/users/mran/starred?page=10 22 | url := "https://api.github.com/users/" + user + "/starred" 23 | token := os.Getenv("GITHUB_TOKEN") 24 | resp, err := client.R(). 25 | SetQueryParam("page", strconv.Itoa(page)). 26 | SetHeader("Authorization", "token "+token). 27 | Get(url) 28 | if err != nil { 29 | panic(err) 30 | } 31 | body := resp.String() 32 | if len(body) == 0 { 33 | return nil 34 | } 35 | //转为json 对象 36 | var infos []model.StarInfo 37 | er := json.Unmarshal([]byte(body), &infos) 38 | if er != nil { 39 | fmt.Println(body) 40 | panic(er) 41 | } 42 | return &infos 43 | } 44 | 45 | // openai 通用请求代码 46 | func OpenaiRequest(prompt string) string { 47 | // 如果 prompt 的长度(以字节计)大于 65536 则截断(注意中文字符) 48 | if len([]rune(prompt)) > 10000 { 49 | prompt = string([]rune(prompt)[:10000]) 50 | } 51 | a := []option.RequestOption{ 52 | option.WithAPIKey(LLMTOKEN), 53 | option.WithBaseURL(LLMBASEURL), 54 | } 55 | client := openai.NewClient(a...) 56 | chatCompletion, err := client.Chat.Completions.New( 57 | context.TODO(), 58 | openai.ChatCompletionNewParams{ 59 | Messages: openai.F([]openai.ChatCompletionMessageParamUnion{ 60 | openai.UserMessage( 61 | `我有一个标过star的github库需要进行分类和标记,给出最相关的8个标记。我会给你它的的readme文件,请你分析后给出一个json结构的标签, 62 | 限制:只给出严格的json结果,其他内容比如json标签等不需要。 63 | 输出格式严格如下 64 | { 65 | "tags": [] 66 | }`, 67 | ), 68 | openai.UserMessage(prompt), 69 | }), 70 | Model: openai.F("deepseek-chat"), 71 | }, 72 | ) 73 | if err != nil { 74 | fmt.Println(prompt) 75 | return "" 76 | } 77 | return chatCompletion.Choices[0].Message.Content 78 | } 79 | 80 | // 获取指定的github项目的readme 内容 81 | func GetReadme(redmeUrl string) *model.ReadmeData { 82 | client := resty.New() 83 | //client.SetProxy("http://127.0.0.1:11081") 84 | url := redmeUrl + "/readme" 85 | resp, err := client.R(). 86 | SetHeader("Authorization", "token "+gtihubToken). 87 | Get(url) 88 | if err != nil { 89 | return nil 90 | 91 | } 92 | body := resp.String() 93 | var data model.ReadmeData 94 | err = json.Unmarshal([]byte(body), &data) 95 | if err != nil { 96 | return nil 97 | } 98 | decodedContent, _ := base64.StdEncoding.DecodeString(data.Content) 99 | data.RawContent = string(decodedContent) 100 | return &data 101 | } 102 | 103 | // 并行处理 104 | func AIParallelProcess(data []model.StarInfo) []model.StarInfo { 105 | 106 | var wg sync.WaitGroup 107 | results := make(chan model.StarInfo, len(data)) 108 | //增加一个限速 109 | limiter := make(chan struct{}, 30) 110 | for _, star := range data { 111 | wg.Add(1) 112 | go func(star model.StarInfo) { 113 | defer wg.Done() 114 | limiter <- struct{}{} 115 | defer func() { <-limiter }() 116 | // 获取readme 117 | readme := GetReadme(star.Url) 118 | if readme == nil { 119 | return 120 | } 121 | fmt.Println(star.FullName) 122 | // 调用openai接口 123 | result := OpenaiRequest(readme.RawContent) 124 | var ai_tag model.AI_tag 125 | err := json.Unmarshal([]byte(result), &ai_tag) 126 | if err != nil { 127 | return 128 | } 129 | star.AI_tag = ai_tag 130 | //还results需要添加对应的github项目标签 131 | results <- star 132 | }(star) 133 | } 134 | 135 | go func() { 136 | wg.Wait() 137 | close(results) 138 | }() 139 | //取出结果 140 | var resultlist []model.StarInfo 141 | for result := range results { 142 | resultlist = append(resultlist, result) 143 | fmt.Println("Result from OpenAI:", result) 144 | } 145 | return resultlist 146 | } 147 | 148 | func SaveMiddleResult(allStar interface{}, fileName string) { 149 | // 结果写入文件 150 | file, err := os.Create(fileName) 151 | if err != nil { 152 | panic(err) 153 | } 154 | defer file.Close() 155 | encoder := json.NewEncoder(file) 156 | encoder.SetIndent("", " ") 157 | err = encoder.Encode(allStar) 158 | if err != nil { 159 | panic(err) 160 | } 161 | } 162 | func readFileCache(filename string) []model.StarInfo { 163 | //读取allStar.json 164 | var allStar []model.StarInfo 165 | 166 | file, err := os.Open(filename) 167 | if err != nil { 168 | return allStar 169 | } 170 | defer file.Close() 171 | decoder := json.NewDecoder(file) 172 | err = decoder.Decode(&allStar) 173 | if err != nil { 174 | panic(err) 175 | } 176 | return allStar 177 | } 178 | func Json2md(aiTagProcess []model.StarInfo) string { 179 | var md string 180 | for _, star := range aiTagProcess { 181 | md += fmt.Sprintf("### [%s](%s)\n", star.FullName, star.HtmlUrl) 182 | if star.Description != nil { 183 | md += fmt.Sprintf("- **Description:** %s\n", *star.Description) 184 | } 185 | if len(star.AI_tag.Tags) > 0 { 186 | md += fmt.Sprintf("- **Tags:** %s\n", strings.Join(star.AI_tag.Tags, ", ")) 187 | } 188 | md += "\n" 189 | } 190 | //保存md到文件 191 | file, err := os.Create("starProject.md") 192 | if err != nil { 193 | panic(err) 194 | } 195 | defer file.Close() 196 | _, err = file.WriteString(md) 197 | if err != nil { 198 | panic(err) 199 | } 200 | return md 201 | 202 | } 203 | 204 | var gtihubToken = os.Getenv("GITHUB_TOKEN") 205 | var LLMTOKEN = os.Getenv("LLM_TOKEN") 206 | var LLMBASEURL = os.Getenv("LLM_BASEURL") 207 | var username = os.Getenv("USERNAME") 208 | 209 | // main 加入启动参数 210 | func main() { 211 | if len(username) == 0 { 212 | panic("need username") 213 | } 214 | if len(gtihubToken) == 0 { 215 | panic("need gtihubToken") 216 | 217 | } 218 | 219 | //分页获取用户所有的star,加入数组 220 | var allStar []model.StarInfo 221 | page := 1 222 | for { 223 | stars := FetchUserStar(username, page) 224 | if len(*stars) == 0 { 225 | break 226 | } 227 | for _, star := range *stars { 228 | println(star.FullName) 229 | allStar = append(allStar, star) 230 | } 231 | 232 | page++ 233 | } 234 | //保存两份 235 | SaveMiddleResult(allStar, "allStar.json") 236 | SaveMiddleResult(allStar, "aiTagProcess.json") 237 | 238 | var aiTagProcess []model.StarInfo 239 | aiTagProcess = readFileCache("aiTagProcess.json") 240 | //需要AI标签的处理 241 | if len(LLMTOKEN) != 0 && len(LLMBASEURL) != 0 { 242 | if len(allStar) != 0 { 243 | aiTagProcess := AIParallelProcess(allStar) 244 | SaveMiddleResult(aiTagProcess, "aiTagProcess.json") 245 | } 246 | } 247 | 248 | Json2md(aiTagProcess) 249 | } 250 | -------------------------------------------------------------------------------- /.trae/documents/notion-style-technical-architecture.md: -------------------------------------------------------------------------------- 1 | # GitHub项目展示平台 - 技术架构设计 2 | 3 | ## 1. 架构设计 4 | 5 | ```mermaid 6 | graph TD 7 | A[用户浏览器] --> B[React 18 前端应用] 8 | B --> C[Framer Motion 动画引擎] 9 | B --> D[Tailwind CSS 样式系统] 10 | B --> E[Fuse.js 搜索引擎] 11 | B --> F[静态JSON数据源] 12 | 13 | subgraph "前端层" 14 | B 15 | C 16 | D 17 | E 18 | end 19 | 20 | subgraph "数据层" 21 | F 22 | end 23 | 24 | subgraph "构建工具链" 25 | G[Vite 构建工具] 26 | H[PostCSS 处理器] 27 | end 28 | ``` 29 | 30 | ## 2. 技术描述 31 | 32 | * **前端框架**: React\@18 + TypeScript\@5 + Vite\@6 33 | 34 | * **样式系统**: Tailwind CSS\@3 + PostCSS\@8 + Autoprefixer\@10 35 | 36 | * **动画库**: Framer Motion\@11 (实现Notion级别的微交互) 37 | 38 | * **搜索引擎**: Fuse.js\@7 (模糊搜索和智能筛选) 39 | 40 | * **图标系统**: Lucide React\@0.400+ (一致的线性图标) 41 | 42 | * **字体**: Inter字体族 + JetBrains Mono (代码显示) 43 | 44 | * **构建优化**: Vite插件生态 + 代码分割 + 懒加载 45 | 46 | ## 3. 路由定义 47 | 48 | | 路由 | 用途 | 49 | | ------------ | ----------------- | 50 | | / | 主展示页面,项目网格展示和搜索功能 | 51 | | /project/:id | 项目详情页面,深度展示单个项目信息 | 52 | | /about | 关于页面,个人品牌和技能展示 | 53 | | /search | 高级搜索页面,复杂筛选和排序功能 | 54 | 55 | ## 4. 组件架构设计 56 | 57 | ### 4.1 核心组件层次 58 | 59 | ```mermaid 60 | graph TD 61 | A[App 根组件] --> B[Layout 布局组件] 62 | B --> C[Header 顶部导航] 63 | B --> D[Main 主内容区] 64 | B --> E[Footer 底部信息] 65 | 66 | D --> F[ProjectGrid 项目网格] 67 | D --> G[SearchPanel 搜索面板] 68 | D --> H[FilterSidebar 筛选侧栏] 69 | 70 | F --> I[ProjectCard 项目卡片] 71 | I --> J[ProjectMeta 项目元信息] 72 | I --> K[TechStack 技术栈标签] 73 | I --> L[ProjectStats 项目统计] 74 | 75 | subgraph "原子组件" 76 | M[Button 按钮] 77 | N[Input 输入框] 78 | O[Tag 标签] 79 | P[Avatar 头像] 80 | Q[Icon 图标] 81 | end 82 | ``` 83 | 84 | ### 4.2 状态管理架构 85 | 86 | ```typescript 87 | // 全局状态接口定义 88 | interface AppState { 89 | projects: Project[]; 90 | searchTerm: string; 91 | selectedTags: string[]; 92 | sortMethod: 'default' | 'stars' | 'updated' | 'name'; 93 | viewMode: 'grid' | 'list' | 'masonry'; 94 | theme: 'light' | 'dark' | 'auto'; 95 | isLoading: boolean; 96 | error: string | null; 97 | } 98 | 99 | // 项目数据接口 100 | interface Project { 101 | id: string; 102 | full_name: string; 103 | description: string; 104 | html_url: string; 105 | homepage?: string; 106 | stargazers_count: number; 107 | language: string; 108 | topics: string[]; 109 | updated_at: string; 110 | created_at: string; 111 | ai_tag?: { 112 | tags: string[]; 113 | category: string; 114 | complexity: 'beginner' | 'intermediate' | 'advanced'; 115 | }; 116 | } 117 | ``` 118 | 119 | ## 5. 样式系统架构 120 | 121 | ### 5.1 Tailwind配置扩展 122 | 123 | ```javascript 124 | // tailwind.config.js 核心配置 125 | module.exports = { 126 | content: ['./src/**/*.{js,jsx,ts,tsx}'], 127 | theme: { 128 | extend: { 129 | fontFamily: { 130 | 'sans': ['Inter', 'system-ui', 'sans-serif'], 131 | 'mono': ['JetBrains Mono', 'monospace'], 132 | }, 133 | colors: { 134 | notion: { 135 | blue: '#2383E2', 136 | gray: { 137 | 50: '#F7F6F3', 138 | 100: '#E9E9E7', 139 | 200: '#CBCAC8', 140 | 300: '#A0A0A0', 141 | 400: '#6F6F6F', 142 | 500: '#2F2F2F', 143 | } 144 | } 145 | }, 146 | animation: { 147 | 'fade-in': 'fadeIn 0.3s ease-out', 148 | 'slide-up': 'slideUp 0.3s ease-out', 149 | 'scale-in': 'scaleIn 0.2s ease-out', 150 | }, 151 | boxShadow: { 152 | 'notion': '0 8px 32px rgba(0, 0, 0, 0.12)', 153 | 'notion-hover': '0 12px 48px rgba(0, 0, 0, 0.18)', 154 | } 155 | } 156 | } 157 | } 158 | ``` 159 | 160 | ### 5.2 组件样式规范 161 | 162 | ```css 163 | /* 核心样式变量 */ 164 | :root { 165 | --spacing-unit: 8px; 166 | --border-radius-sm: 8px; 167 | --border-radius-md: 12px; 168 | --border-radius-lg: 16px; 169 | --transition-fast: 0.15s ease-out; 170 | --transition-normal: 0.3s ease-out; 171 | --transition-slow: 0.5s ease-out; 172 | } 173 | 174 | /* 通用组件基础样式 */ 175 | .notion-card { 176 | @apply bg-white rounded-lg shadow-notion hover:shadow-notion-hover; 177 | @apply transition-all duration-300 ease-out; 178 | @apply border border-gray-100; 179 | } 180 | 181 | .notion-button { 182 | @apply px-4 py-2 rounded-lg font-medium; 183 | @apply transition-all duration-200 ease-out; 184 | @apply focus:outline-none focus:ring-2 focus:ring-notion-blue focus:ring-opacity-50; 185 | } 186 | 187 | .notion-input { 188 | @apply w-full px-4 py-3 rounded-lg border border-gray-200; 189 | @apply focus:border-notion-blue focus:ring-2 focus:ring-notion-blue focus:ring-opacity-20; 190 | @apply transition-all duration-200 ease-out; 191 | } 192 | ``` 193 | 194 | ## 6. 性能优化策略 195 | 196 | ### 6.1 代码分割和懒加载 197 | 198 | ```typescript 199 | // 路由级别的代码分割 200 | const ProjectDetail = lazy(() => import('./pages/ProjectDetail')); 201 | const About = lazy(() => import('./pages/About')); 202 | const Search = lazy(() => import('./pages/Search')); 203 | 204 | // 组件级别的懒加载 205 | const HeavyChart = lazy(() => import('./components/HeavyChart')); 206 | ``` 207 | 208 | ### 6.2 图片和资源优化 209 | 210 | * 使用WebP格式图片,fallback到PNG 211 | 212 | * 实现图片懒加载和渐进式加载 213 | 214 | * SVG图标内联优化 215 | 216 | * 字体子集化,只加载需要的字符 217 | 218 | ### 6.3 搜索性能优化 219 | 220 | ```typescript 221 | // 搜索防抖和缓存策略 222 | const useOptimizedSearch = () => { 223 | const [searchResults, setSearchResults] = useState([]); 224 | const searchCache = useRef(new Map()); 225 | 226 | const debouncedSearch = useMemo( 227 | () => debounce((term: string) => { 228 | if (searchCache.current.has(term)) { 229 | setSearchResults(searchCache.current.get(term)); 230 | return; 231 | } 232 | 233 | const results = fuse.search(term); 234 | searchCache.current.set(term, results); 235 | setSearchResults(results); 236 | }, 300), 237 | [] 238 | ); 239 | 240 | return { searchResults, debouncedSearch }; 241 | }; 242 | ``` 243 | 244 | ## 7. 部署和构建配置 245 | 246 | ### 7.1 Vite构建优化 247 | 248 | ```javascript 249 | // vite.config.js 250 | export default defineConfig({ 251 | plugins: [react()], 252 | build: { 253 | rollupOptions: { 254 | output: { 255 | manualChunks: { 256 | vendor: ['react', 'react-dom'], 257 | animations: ['framer-motion'], 258 | search: ['fuse.js'], 259 | } 260 | } 261 | }, 262 | chunkSizeWarningLimit: 1000, 263 | }, 264 | optimizeDeps: { 265 | include: ['react', 'react-dom', 'framer-motion', 'fuse.js'] 266 | } 267 | }); 268 | ``` 269 | 270 | ### 7.2 静态资源处理 271 | 272 | * 项目数据通过静态JSON文件提供 273 | 274 | * 支持增量更新和版本控制 275 | 276 | * CDN部署优化,全球加速访问 277 | 278 | * Gzip压缩和Brotli压缩支持 279 | 280 | -------------------------------------------------------------------------------- /web/src/components/ui/SearchAndFilter.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo } from 'react'; 2 | import { motion, AnimatePresence } from 'framer-motion'; 3 | import { Search, Filter, X, ChevronDown, SortAsc, SortDesc } from 'lucide-react'; 4 | import Input from './Input'; 5 | import Button from './Button'; 6 | 7 | const SearchAndFilter = ({ 8 | searchTerm, 9 | onSearchChange, 10 | selectedTags, 11 | onTagsChange, 12 | sortMethod, 13 | onSortChange, 14 | tagFrequency, 15 | totalProjects, 16 | filteredCount 17 | }) => { 18 | const [isFilterOpen, setIsFilterOpen] = useState(false); 19 | const [isSortOpen, setIsSortOpen] = useState(false); 20 | 21 | // Get top tags for display 22 | const topTags = useMemo(() => { 23 | return Object.entries(tagFrequency) 24 | .sort((a, b) => b[1] - a[1]) 25 | .slice(0, 20) 26 | .map(([tag, count]) => ({ tag, count })); 27 | }, [tagFrequency]); 28 | 29 | const sortOptions = [ 30 | { value: 'default', label: 'Default Order', icon: null }, 31 | { value: 'stars', label: 'Most Stars', icon: SortDesc }, 32 | { value: 'updated-time', label: 'Recently Updated', icon: SortAsc }, 33 | ]; 34 | 35 | const handleTagToggle = (tag) => { 36 | const newTags = selectedTags.includes(tag) 37 | ? selectedTags.filter(t => t !== tag) 38 | : [...selectedTags, tag]; 39 | onTagsChange(newTags); 40 | }; 41 | 42 | const clearAllFilters = () => { 43 | onSearchChange(''); 44 | onTagsChange([]); 45 | onSortChange('default'); 46 | }; 47 | 48 | const hasActiveFilters = searchTerm || selectedTags.length > 0 || sortMethod !== 'default'; 49 | 50 | return ( 51 |
52 | {/* Search Bar */} 53 |
54 | 60 | onSearchChange(e.target.value)} 65 | icon={Search} 66 | /> 67 | {searchTerm && ( 68 | onSearchChange('')} 73 | className="absolute right-4 top-1/2 transform -translate-y-1/2 p-1 rounded-notion-sm hover:bg-notion-gray-100 text-notion-gray-400 hover:text-notion-gray-600 transition-colors duration-200" 74 | > 75 | 76 | 77 | )} 78 | 79 |
80 | 81 | {/* Filter and Sort Controls */} 82 | 88 |
89 | {/* Filter Button */} 90 |
91 | 110 |
111 | 112 | {/* Sort Button */} 113 |
114 | setIsSortOpen(!isSortOpen)} 118 | className={`flex items-center space-x-2 px-4 py-2 rounded-notion-md border transition-all duration-200 ${ 119 | isSortOpen || sortMethod !== 'default' 120 | ? 'bg-notion-blue text-white border-notion-blue' 121 | : 'bg-white text-notion-gray-600 border-notion-gray-200 hover:border-notion-gray-300' 122 | }`} 123 | > 124 | {sortOptions.find(opt => opt.value === sortMethod)?.icon && ( 125 | React.createElement(sortOptions.find(opt => opt.value === sortMethod).icon, { className: 'w-4 h-4' }) 126 | )} 127 | 128 | {sortOptions.find(opt => opt.value === sortMethod)?.label} 129 | 130 | 133 | 134 | 135 | {/* Sort Dropdown */} 136 | 137 | {isSortOpen && ( 138 | 145 | {sortOptions.map((option) => ( 146 | { 150 | onSortChange(option.value); 151 | setIsSortOpen(false); 152 | }} 153 | className={`w-full flex items-center space-x-3 px-4 py-3 text-left transition-colors duration-150 first:rounded-t-notion-md last:rounded-b-notion-md ${ 154 | sortMethod === option.value 155 | ? 'bg-notion-blue text-white' 156 | : 'text-notion-gray-700 hover:bg-notion-gray-50' 157 | }`} 158 | > 159 | {option.icon && } 160 | {option.label} 161 | 162 | ))} 163 | 164 | )} 165 | 166 |
167 | 168 | {/* Clear Filters */} 169 | {hasActiveFilters && ( 170 | 179 | 180 | Clear all 181 | 182 | )} 183 |
184 | 185 | {/* Results Count */} 186 |
187 | Showing {filteredCount} of{' '} 188 | {totalProjects} projects 189 |
190 |
191 | 192 | {/* Filter Tags Panel */} 193 | 194 | {isFilterOpen && ( 195 | 202 |

203 | Filter by Technology 204 |

205 |
206 | {topTags.map(({ tag, count }) => ( 207 | 223 | ))} 224 |
225 |
226 | )} 227 |
228 | 229 | {/* Active Filters Display */} 230 | {selectedTags.length > 0 && ( 231 | 236 | Active filters: 237 | {selectedTags.map((tag) => ( 238 | 245 | {tag} 246 | 252 | 253 | ))} 254 | 255 | )} 256 |
257 | ); 258 | }; 259 | 260 | export default SearchAndFilter; -------------------------------------------------------------------------------- /web/src/pages/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useMemo } from 'react'; 2 | import { motion, AnimatePresence } from 'framer-motion'; 3 | import Fuse from 'fuse.js'; 4 | import Layout from '../components/layout/Layout'; 5 | import ProjectCard from '../components/project/ProjectCard'; 6 | import SearchAndFilter from '../components/ui/SearchAndFilter'; 7 | import Loading from '../components/ui/Loading'; 8 | import Card from '../components/ui/Card'; 9 | import { Github, Sparkles } from 'lucide-react'; 10 | 11 | const HomePage = () => { 12 | const [projects, setProjects] = useState([]); 13 | const [isLoading, setIsLoading] = useState(true); 14 | const [searchTerm, setSearchTerm] = useState(''); 15 | const [selectedTags, setSelectedTags] = useState([]); 16 | const [sortMethod, setSortMethod] = useState('default'); 17 | 18 | // Load JSON data 19 | useEffect(() => { 20 | const loadProjects = async () => { 21 | try { 22 | const response = await fetch('/projects.json'); 23 | const data = await response.json(); 24 | setProjects(data); 25 | setIsLoading(false); 26 | } catch (error) { 27 | console.error('Error loading projects:', error); 28 | setIsLoading(false); 29 | } 30 | }; 31 | 32 | loadProjects(); 33 | }, []); 34 | 35 | // Calculate tag frequencies 36 | const tagFrequency = useMemo(() => { 37 | const frequencies = {}; 38 | projects.forEach(project => { 39 | const projectTags = [ 40 | ...(project.topics || []), 41 | ...(project.ai_tag?.tags || []), 42 | project.language 43 | ].filter(Boolean); 44 | 45 | projectTags.forEach(tag => { 46 | frequencies[tag] = (frequencies[tag] || 0) + 1; 47 | }); 48 | }); 49 | return frequencies; 50 | }, [projects]); 51 | 52 | // Fuzzy search setup 53 | const fuse = useMemo(() => { 54 | const options = { 55 | keys: ['full_name', 'name', 'description', 'ai_tag.tags', 'topics', 'language'], 56 | threshold: 0.3, 57 | includeScore: true, 58 | }; 59 | return new Fuse(projects, options); 60 | }, [projects]); 61 | 62 | // Filtered and sorted projects 63 | const filteredProjects = useMemo(() => { 64 | let result = projects; 65 | 66 | // Apply fuzzy search if there's a search term 67 | if (searchTerm.trim()) { 68 | result = fuse.search(searchTerm.trim()).map(r => r.item); 69 | } 70 | 71 | // Apply tag filtering 72 | if (selectedTags.length > 0) { 73 | result = result.filter(project => { 74 | const projectTags = [ 75 | ...(project.topics || []), 76 | ...(project.ai_tag?.tags || []), 77 | project.language 78 | ].filter(Boolean); 79 | 80 | return selectedTags.every(tag => projectTags.includes(tag)); 81 | }); 82 | } 83 | 84 | // Apply sorting 85 | if (sortMethod === 'stars') { 86 | result = [...result].sort((a, b) => 87 | (b.stargazers_count || 0) - (a.stargazers_count || 0) 88 | ); 89 | } else if (sortMethod === 'updated-time') { 90 | result = [...result].sort((a, b) => 91 | new Date(b.updated_at) - new Date(a.updated_at) 92 | ); 93 | } 94 | 95 | return result; 96 | }, [projects, searchTerm, selectedTags, sortMethod, fuse]); 97 | 98 | // Loading state 99 | if (isLoading) { 100 | return ( 101 | 102 |
103 | 104 |
105 |
106 | ); 107 | } 108 | 109 | return ( 110 | 111 | {/* GitHub Star Button - Fixed Position */} 112 | 123 | 124 | Star on GitHub 125 | 130 | 131 | 132 |
133 | {/* Hero Section */} 134 | 140 | 146 | 147 | Curated Project Collection 148 | 149 | 150 | 156 | My GitHub 157 |
158 | Project Showcase 159 |
160 | 161 | 167 | A carefully curated collection of my development projects, showcasing innovation, 168 |
169 | craftsmanship, and attention to detail in every line of code. 170 |
171 |
172 | 173 | {/* Search and Filter */} 174 | 180 | 191 | 192 | 193 | {/* Projects Grid */} 194 | 199 | {filteredProjects.length > 0 ? ( 200 |
201 | {filteredProjects.map((project, index) => ( 202 | 207 | ))} 208 |
209 | ) : ( 210 | 216 |
217 | 218 |
219 |

220 | No projects found 221 |

222 |

223 | Try adjusting your search terms or filters to find what you're looking for. 224 |

225 | { 229 | setSearchTerm(''); 230 | setSelectedTags([]); 231 | setSortMethod('default'); 232 | }} 233 | className="bg-notion-blue text-white px-6 py-3 rounded-notion-md font-medium hover:bg-notion-blue/90 transition-colors duration-200" 234 | > 235 | Clear all filters 236 | 237 |
238 | )} 239 |
240 | 241 | {/* Stats Section */} 242 | {filteredProjects.length > 0 && ( 243 | 249 |
250 | 251 | 257 | {projects.length} 258 | 259 |
260 | Total Projects 261 |
262 |
263 | 264 | 270 | {Object.keys(tagFrequency).length} 271 | 272 |
273 | Technologies 274 |
275 |
276 | 277 | 283 | {projects.reduce((sum, p) => sum + (p.stargazers_count || 0), 0)} 284 | 285 |
286 | Total Stars 287 |
288 |
289 |
290 |
291 | )} 292 |
293 |
294 | ); 295 | }; 296 | 297 | export default HomePage; --------------------------------------------------------------------------------