├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── README_CN.md ├── SECURITY.md ├── app ├── dashboard │ ├── keys │ │ └── page.tsx │ ├── layout.tsx │ ├── page.tsx │ └── settings │ │ └── page.tsx ├── globals.css ├── layout.tsx ├── login │ └── page.tsx ├── page.tsx └── register │ └── layout.tsx ├── components.json ├── components ├── api-balance-card.tsx ├── api-key-list.tsx ├── dashboard-layout.tsx ├── error-notification.tsx ├── language-switcher.tsx ├── login-form.tsx ├── settings-form.tsx ├── theme-provider.tsx └── ui │ ├── accordion.tsx │ ├── alert-dialog.tsx │ ├── alert.tsx │ ├── aspect-ratio.tsx │ ├── avatar.tsx │ ├── badge.tsx │ ├── breadcrumb.tsx │ ├── button.tsx │ ├── calendar.tsx │ ├── card.tsx │ ├── carousel.tsx │ ├── chart.tsx │ ├── checkbox.tsx │ ├── collapsible.tsx │ ├── command.tsx │ ├── context-menu.tsx │ ├── dialog.tsx │ ├── drawer.tsx │ ├── dropdown-menu.tsx │ ├── form.tsx │ ├── hover-card.tsx │ ├── input-otp.tsx │ ├── input.tsx │ ├── label.tsx │ ├── menubar.tsx │ ├── navigation-menu.tsx │ ├── pagination.tsx │ ├── popover.tsx │ ├── progress.tsx │ ├── radio-group.tsx │ ├── resizable.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── sidebar.tsx │ ├── skeleton.tsx │ ├── slider.tsx │ ├── sonner.tsx │ ├── switch.tsx │ ├── table.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ ├── toast.tsx │ ├── toaster.tsx │ ├── toggle-group.tsx │ ├── toggle.tsx │ ├── tooltip.tsx │ ├── use-mobile.tsx │ └── use-toast.ts ├── hooks ├── use-mobile.tsx └── use-toast.ts ├── lib ├── actions │ └── auth-actions.ts ├── api-connection.ts ├── api-key-storage.ts ├── encryption.ts ├── error-handler.ts ├── i18n │ ├── language-context.tsx │ └── translations.ts ├── storage.ts ├── storage │ └── settings-storage.ts └── utils.ts ├── next.config.mjs ├── package.json ├── postcss.config.mjs ├── public ├── image │ ├── Dashboard-cn.png │ ├── Dashboard.png │ ├── api-key-cn.png │ ├── api-key.png │ ├── complex-cn.png │ ├── newkey-complex.png │ ├── newkey.png │ ├── newkeyi-cn.png │ ├── sys-set-cn.png │ └── sys-set.png ├── placeholder-logo.png ├── placeholder-logo.svg ├── placeholder-user.jpg ├── placeholder.jpg └── placeholder.svg ├── styles └── globals.css ├── tailwind.config.ts └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '[BUG] ' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment (please complete the following information):** 27 | - OS: [e.g. Windows, macOS] 28 | - Browser: [e.g. Chrome, Safari] 29 | - Version: [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '[FEATURE] ' 5 | labels: 'enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. 3 | 4 | Fixes # (issue) 5 | 6 | ## Type of change 7 | Please delete options that are not relevant. 8 | 9 | - [ ] Bug fix (non-breaking change which fixes an issue) 10 | - [ ] New feature (non-breaking change which adds functionality) 11 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 12 | - [ ] Documentation update 13 | 14 | ## How Has This Been Tested? 15 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. 16 | 17 | ## Checklist: 18 | - [ ] My code follows the style guidelines of this project 19 | - [ ] I have performed a self-review of my own code 20 | - [ ] I have commented my code, particularly in hard-to-understand areas 21 | - [ ] I have made corresponding changes to the documentation 22 | - [ ] My changes generate no new warnings 23 | - [ ] I have added tests that prove my fix is effective or that my feature works 24 | - [ ] New and existing unit tests pass locally with my changes 25 | 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | # 移除缓存选项,因为没有锁文件 25 | # cache: 'npm' 26 | 27 | - name: Generate package-lock.json 28 | run: npm install --package-lock-only 29 | 30 | - name: Install dependencies 31 | run: npm ci || npm install 32 | # npm ci 需要 package-lock.json,如果失败则回退到 npm install 33 | 34 | - name: Build 35 | run: npm run build 36 | env: 37 | CI: true 38 | 39 | - name: Lint 40 | run: npm run lint || echo "Linting skipped" 41 | 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # next.js 10 | /.next/ 11 | /out/ 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # local env files 26 | .env*.local 27 | .env 28 | 29 | # vercel 30 | .vercel 31 | 32 | # typescript 33 | *.tsbuildinfo 34 | next-env.d.ts 35 | 36 | # IDE 37 | .idea 38 | .vscode/* 39 | !.vscode/extensions.json 40 | !.vscode/settings.json 41 | 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to NanMeng API Key Manager 2 | 3 | Thank you for considering contributing to the NanMeng API Key Manager! This document provides guidelines and instructions for contributing. 4 | 5 | ## Code of Conduct 6 | 7 | By participating in this project, you agree to abide by our Code of Conduct. Please be respectful and considerate of others. 8 | 9 | ## How Can I Contribute? 10 | 11 | ### Reporting Bugs 12 | 13 | - Check if the bug has already been reported in the Issues section 14 | - Use the bug report template when creating a new issue 15 | - Include detailed steps to reproduce the bug 16 | - Include screenshots if applicable 17 | - Describe what you expected to happen and what actually happened 18 | 19 | ### Suggesting Enhancements 20 | 21 | - Check if the enhancement has already been suggested in the Issues section 22 | - Use the feature request template when creating a new issue 23 | - Provide a clear description of the enhancement 24 | - Explain why this enhancement would be useful to most users 25 | 26 | ### Pull Requests 27 | 28 | 1. Fork the repository 29 | 2. Create a new branch (`git checkout -b feature/your-feature-name`) 30 | 3. Make your changes 31 | 4. Run tests if available 32 | 5. Commit your changes (`git commit -m 'Add some feature'`) 33 | 6. Push to the branch (`git push origin feature/your-feature-name`) 34 | 7. Open a Pull Request 35 | 36 | ## Development Setup 37 | 38 | 1. Clone the repository 39 | 2. Install dependencies with `npm install` 40 | 3. Run the development server with `npm run dev` 41 | 4. Visit `http://localhost:3000` to see the application 42 | 43 | ## Coding Guidelines 44 | 45 | - Follow the existing code style 46 | - Write clear, readable, and maintainable code 47 | - Add comments for complex logic 48 | - Update documentation when necessary 49 | 50 | ## Commit Message Guidelines 51 | 52 | - Use the present tense ("Add feature" not "Added feature") 53 | - Use the imperative mood ("Move cursor to..." not "Moves cursor to...") 54 | - Limit the first line to 72 characters or less 55 | - Reference issues and pull requests liberally after the first line 56 | 57 | Thank you for contributing! 58 | 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 NanMeng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # API Key Manager 2 | 3 |

4 | NanMeng API Key Manager Logo 5 |

6 | 7 |

8 | English | 9 | 简体中文 10 |

11 | 12 | A secure and efficient API key management system that helps developers and teams easily manage API keys for various AI models. 13 | 14 | ## Preface 15 | The API Key Manager aims to provide AI developers and enterprises with a one-stop solution for API key management. In today's diverse AI service environment, managing API keys from multiple providers is both cumbersome and poses security risks. This system helps users efficiently organize and utilize various service keys through secure encrypted storage, status monitoring, and convenient management features, reducing the risk of leakage and improving development efficiency. The system employs local storage technology, ensuring your sensitive key information is not uploaded to the cloud, further enhancing data security and privacy protection. Whether you are an individual developer or an enterprise user, the API Key Manager can meet your API key management needs. Of course, not only can it manage AI-related keys, but other related keys can also be uniformly managed using this manager. 16 | 17 | For example, when using the [dify](https://github.com/langgenius/dify) project, you need to fill in multiple API keys for large AI models, which can be a headache. This is also one of the starting points of this project. 18 | 19 | ## ✨ Features 20 | 21 | - [x] 🔑 **API Key Management**: Securely store and manage API keys from multiple AI service providers 22 | - [x] 🔒 **Security Encryption**: Protect your API keys with advanced encryption technology 23 | - [x] ⚡ **Status Monitoring**: Monitor API key connection status in real time 24 | - [x] 🌐 **Multi-language Support**: Switch between Chinese and English interfaces 25 | - [x] ⚙️ **Custom Settings**: Customize system settings and preferences according to your needs 26 | - [x] 🔄 **Connection Testing**: Test API connections directly from the dashboard 27 | - [ ] 📊 **Usage Monitoring**: Monitor API key usage and token consumption (Coming soon) 28 | 29 | ## 📋 Table of Contents 30 | 31 | - [Features](#-features) 32 | - [Demo](#-demo) 33 | - [Installation](#-installation) 34 | - [Vercel Deployment](#vercel-deployment) 35 | - [Local Installation](#local-installation) 36 | - [Docker Deployment](#docker-deployment) 37 | - [Usage](#-usage) 38 | - [Configuration](#-configuration) 39 | 40 | ## 📸 Demo 41 | 42 | Visit our [demo site](https://www.a888.online) to experience the application. 43 | 44 | API Key Management Page: Key Overview 45 | 46 | ![API Key Overview](public/image/api-key.png) 47 | 48 | Adding Keys: Basic Key + Composite Key 49 | 50 | ![New Key Complex](public/image/newkey-complex.png) ![New Key](public/image/newkey.png) 51 | 52 | Dashboard: Monitor Connection Status 53 | 54 | ![Dashboard](public/image/Dashboard.png) 55 | 56 | System Settings: Password Changes and Other Operations 57 | 58 | ![System Settings](public/image/sys-set.png) 59 | 60 | 61 | Default login credentials: 62 | - Username: `admin` 63 | - Password: `password` 64 | 65 | After logging in, it is recommended to change the password and remember it for long-term use. 66 | 67 | ## 📥 Installation 68 | 69 | ### Vercel Deployment 70 | 71 | Deploying the NanMeng API Key Manager with Vercel is the simplest method: 72 | 73 | 1. Fork this repository to your GitHub account 74 | 2. Sign up or log in to [Vercel](https://vercel.com) 75 | 3. Click "New Project" and import your forked repository 76 | 4. Configure the following environment variables: 77 | - `ENCRYPTION_KEY`: A secure random string for encrypting API keys 78 | - `ALLOW_REGISTRATION`: Set to "true" or "false" to enable/disable user registration (registration feature has been deprecated) 79 | 5. Click "Deploy" 80 | 81 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fyourusername%2Fapi-key-manager) 82 | 83 | ### Local Installation 84 | 85 | #### Prerequisites 86 | 87 | - Node.js 18.x or higher 88 | - npm or yarn 89 | - Git 90 | 91 | #### Steps 92 | 93 | 1. Clone the repository: 94 | ```bash 95 | git clone https://github.com/randomAndre/api-key-manager.git 96 | cd api-key-manager 97 | ``` 98 | 2. **Install Dependencies** 99 | 100 | ```bash 101 | npm install 102 | # or 103 | yarn install 104 | ``` 105 | 106 | 107 | 3. **Environment Setup** 108 | 109 | Create a `.env.local` file in the root directory with the following variables: 110 | 111 | ```plaintext 112 | ENCRYPTION_KEY=your-secure-random-string 113 | ``` 114 | 115 | 116 | 4. **Development Mode** 117 | 118 | To run the application in development mode: 119 | 120 | ```bash 121 | npm run dev 122 | # or 123 | yarn dev 124 | ``` 125 | 126 | The application will be available at `http://localhost:3000`. 127 | 128 | 129 | 5. **Production Build** 130 | 131 | To create a production build: 132 | 133 | ```bash 134 | npm run build 135 | npm start 136 | # or 137 | yarn build 138 | yarn start 139 | ``` 140 | 141 | 142 | 6. ### Docker Deployment 143 | 144 | You can also deploy using Docker: 145 | 146 | ```bash 147 | # Build the Docker image 148 | docker build -t nanmeng-api-key-manager . 149 | 150 | # Run the container 151 | docker run -p 3000:3000 -e ENCRYPTION_KEY=your-secure-key -e ALLOW_REGISTRATION=false nanmeng-api-key-manager 152 | ``` 153 | 154 | 155 | 156 | 157 | ## 🚀 Usage 158 | 159 | ### First-time Setup 160 | 161 | 1. Access the application using the default credentials: 162 | - 用户名: `admin` 163 | - 密码: `password` 164 | 165 | 2. After logging in, go to the Settings page and change the default password. 166 | 167 | 168 | ### Managing API Keys 169 | 170 | 1. Navigate to the "API Keys" section from the dashboard. 171 | 172 | 2. Click "Add Key" to add a new API key. 173 | 174 | 3. Fill in the required information: 175 | 176 | - Name: Descriptive name for the key 177 | - Provider: Service provider (e.g., OpenAI, Anthropic) 178 | - Key Type: Simple API key or Composite key 179 | - API Key: Your actual API key Additional fields for Composite key (AppID, Secret Key) 180 | - Base URL: API endpoint URL (default values for known providers) 181 | - Billing URL: Link to the provider's billing page (recommended) 182 | 183 | 4. To test a key's connection, use the "Test" button on the dashboard. 184 | 185 | Note: Not only AI keys, but all related keys can be configured and stored. 186 | 187 | 188 | ## ⚠️ Security Recommendations 189 | 190 | 1. Change the default admin password immediately after first login. 191 | 2. Use a strong, unique `ENCRYPTION_KEY` for production deployments. 192 | 3. If self-hosting, ensure your server has HTTPS enabled. 193 | 4. Regularly backup your data. 194 | 5. Regularly rotate your API keys to minimize risk. 195 | 6. Store sensitive information such as API keys and encryption keys in environment variables. 196 | 197 | 198 | ## 🛠️ Configuration 199 | 200 | ### Environment Variables 201 | 202 | | Variable | Description | Default | 203 | | ---------------- | -------------------------------- | -------- | 204 | | `ENCRYPTION_KEY` | Key used for encrypting API keys | Required | 205 | 206 | 207 | ### Language Settings 208 | 209 | The application supports English and Chinese. Users can switch languages using the language selector in the interface. 210 | 211 | ## Contributing 212 | 213 | We welcome contributions from the community! To contribute, please follow these steps: 214 | 215 | 1. Fork the repository. 216 | 2. Create a new branch (`git checkout -b feature-branch`). 217 | 3. Make your changes and commit them (`git commit -m 'Add new feature'`). 218 | 4. Push to the branch (`git push origin feature-branch`). 219 | 5. Create a new Pull Request. 220 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # API密钥管理器 2 | 3 |

4 | 南梦API密钥管理器 Logo 5 |

6 | 7 |

8 | English | 9 | 简体中文 10 |

11 | 12 | 一个安全且高效的API密钥管理系统,帮助开发者和团队轻松管理各种AI模型的API密钥。 13 | ## 前言 14 | API秘钥管理器旨在为AI开发者和企业提供一站式API密钥管理解决方案。在当今多样化的AI服务环境中,管理多个提供商的API密钥既繁琐又存在安全风险。本系统通过安全加密存储、状态监控和便捷管理功能,帮助用户高效组织和使用各类AI服务密钥,降低泄露风险,提升开发效率。系统采用本地存储技术,确保您的敏感密钥信息不会上传至云端,进一步增强数据安全性和隐私保护。无论您是个人开发者还是企业用户,API秘钥管理器都能满足您的API密钥管理需求,不仅限于AI密钥,其他相关密钥也可通过该管理器进行统一管理。 15 | 16 | 例如:[dify](https://github.com/langgenius/dify) 这个项目在使用时需要填写多个AI大模型的key,这是一个头疼的事,这也是这个项目的一个出发点 17 | 18 | ## ✨ 功能 19 | 20 | - [x] 🔑 **API密钥管理**: 安全地存储和管理来自多个AI服务提供商的API密钥 21 | - [x] 🔒 **安全加密**: 使用高级加密技术保护您的API密钥 22 | - [x] ⚡ **状态监控**: 实时监控API密钥的连接状态 23 | - [x] 🌐 **多语言支持**: 在中文和英文界面之间切换 24 | - [x] ⚙️ **自定义设置**: 根据您的需求自定义系统设置和偏好 25 | - [x] 🔄 **连接测试**: 直接从仪表盘测试API连接 26 | - [ ] 📊 **使用监控**: 监控API密钥的使用量和token消耗(待开发) 27 | 28 | ## 📋 目录 29 | 30 | - [功能](#-功能) 31 | - [演示](#-演示) 32 | - [安装](#-安装) 33 | - [Vercel部署](#vercel部署) 34 | - [本地安装](#本地安装) 35 | - [Docker部署](#docker部署) 36 | - [使用](#-使用指南) 37 | - [配置](#-配置选项) 38 | 39 | ## 📸 演示 40 | 41 | 访问我们的[演示站点](https://www.a888.online)体验应用程序。 42 | 43 | 密钥管理页面:密钥总览 44 | 45 | api-key-cn 46 | 47 | 添加密钥:基础密钥 + 复合密钥 48 | 49 | complex-cnnewkeyi-cn 50 | 51 | 仪表盘:监控连接情况 52 | 53 | Dashboard-cn 54 | 55 | 系统设置:密码更改等操作 56 | 57 | sys-set-cn 58 | 59 | 60 | 默认登录凭据: 61 | 62 | - 用户名: `admin` 63 | - 密码: `password` 64 | 65 | 登录后若需长期使用建议更改密码并记住密码 66 | 67 | ## 📥 安装 68 | 69 | ### Vercel部署 70 | 71 | 使用Vercel部署API密钥管理器是最简单的方法: 72 | 73 | 1. 将此仓库 fork 到您的 GitHub 账户 74 | 2. 注册或登录 [Vercel](https://vercel.com) 75 | 3. 点击 "New Project" 并导入您的 forked 仓库 76 | 4. 配置以下环境变量: 77 | - `ENCRYPTION_KEY`: 用于加密API密钥的安全随机字符串 78 | - `ALLOW_REGISTRATION`: 设置为 "true" 或 "false" 以启用/禁用用户注册,注册功能已经弃用 79 | 5. 点击 "Deploy" 80 | 81 | [![使用Vercel部署](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fyourusername%2Fapi-key-manager) 82 | 83 | ### 本地安装 84 | 85 | #### 前提条件 86 | 87 | - Node.js 18.x 或更高版本 88 | - npm 或 yarn 89 | - Git 90 | 91 | #### 步骤 92 | 93 | 1. **克隆仓库**: 94 | 95 | ```bash 96 | git clone https://github.com/randomAndre/api-key-manager.git 97 | cd api-key-manager 98 | ``` 99 | 2. **安装依赖** 100 | 101 | ```bash 102 | npm install 103 | # 或 104 | yarn install 105 | ``` 106 | 107 | 108 | 3. **环境设置** 109 | 110 | 在根目录创建一个`.env.local`文件,包含以下变量: 111 | 112 | ```plaintext 113 | ENCRYPTION_KEY=your-secure-random-string 114 | ``` 115 | 116 | 117 | 4. **开发模式** 118 | 119 | 要在开发模式下运行应用程序: 120 | 121 | ```bash 122 | npm run dev 123 | # 或 124 | yarn dev 125 | ``` 126 | 127 | 应用程序将在`http://localhost:3000`上可用。 128 | 129 | 130 | 5. **生产构建** 131 | 132 | 要创建生产构建: 133 | 134 | ```bash 135 | npm run build 136 | npm start 137 | # 或 138 | yarn build 139 | yarn start 140 | ``` 141 | 142 | ### **Docker部署** 143 | 144 | 您也可以使用Docker部署: 145 | 146 | ```bash 147 | # 构建Docker镜像 148 | docker build -t nanmeng-api-key-manager . 149 | 150 | # 运行容器 151 | docker run -p 3000:3000 -e ENCRYPTION_KEY=your-secure-key -e ALLOW_REGISTRATION=false nanmeng-api-key-manager 152 | ``` 153 | 154 | 155 | 156 | 157 | ## 🚀 使用指南 158 | 159 | ### 首次设置 160 | 161 | 1. 使用默认凭据访问应用程序: 162 | - 用户名: `admin` 163 | - 密码: `password` 164 | 165 | 2. 登录后,转到设置页面并更改默认密码。 166 | 167 | 168 | ### 管理API密钥 169 | 170 | 1. 从仪表板导航到"API密钥"部分。 171 | 172 | 2. 点击"添加密钥"添加新的API密钥。 173 | 174 | 3. 填写所需信息: 175 | 176 | - 名称:密钥的描述性名称 177 | - 提供商:服务提供商(OpenAI、Anthropic等) 178 | - 密钥类型:简单API密钥或复合密钥 179 | - API密钥:您的实际API密钥/复合密钥的附加字段(AppID、Secret Key) 180 | - 基础URL:API端点URL(为已知提供商提供默认值) 181 | - 充值URL:提供商的账单页面链接(建议填写) 182 | 183 | 4. 要测试密钥的连接,请使用仪表板上的"测试"按钮。 184 | 185 | 注:不止是AI密钥,所有相关密钥皆可配置存储 186 | 187 | 188 | ### ⚠️ 安全建议 189 | 190 | 1. 首次登录后立即更改默认管理员密码。 191 | 2. 为生产部署使用强大且唯一的`ENCRYPTION_KEY`。 192 | 3. 如果自托管,确保您的服务器启用了HTTPS。 193 | 4. 定期备份您的数据。 194 | 5. 定期更换您的API密钥以降低风险。 195 | 6. 使用环境变量存储敏感信息,如API密钥和加密密钥。 196 | 197 | ## 🛠️ 配置选项 198 | 199 | ### 环境变量 200 | 201 | | 变量 | 描述 | 默认值 | 202 | | ---------------- | --------------------- | ------ | 203 | | `ENCRYPTION_KEY` | 用于加密API密钥的密钥 | 必填 | 204 | 205 | ## 语言设置 206 | 207 | 应用程序支持中文和英文。用户可以使用界面中的语言选择器切换语言。 208 | 209 | ## 贡献 210 | 211 | 我们欢迎社区的贡献!要贡献,请按照以下步骤操作: 212 | 213 | 1. Fork 仓库。 214 | 2. 创建一个新分支 (`git checkout -b feature-branch`)。 215 | 3. 进行更改并提交 (`git commit -m 'Add new feature'`)。 216 | 4. 推送到分支 (`git push origin feature-branch`)。 217 | 5. 创建一个新的 Pull Request。 218 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Currently, we are providing security updates for the following versions: 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 1.0.x | :white_check_mark: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | We take the security of API Key Manager seriously. If you believe you've found a security vulnerability, please follow these steps: 14 | 15 | 1. **Do not disclose the vulnerability publicly** 16 | 2. **Email us at [security@nanmengtech.com](mailto:security@nanmengtech.com)** with details about the vulnerability 17 | 3. Include steps to reproduce, if possible 18 | 4. Include potential impact of the vulnerability 19 | 20 | We will acknowledge receipt of your vulnerability report as soon as possible and will send you regular updates about our progress. 21 | 22 | ## What to Expect 23 | 24 | After you've submitted a vulnerability report: 25 | 26 | 1. We will acknowledge your report within 48 hours 27 | 2. We will provide an initial assessment of the report within 1 week 28 | 3. We will work on addressing the vulnerability and will keep you updated on our progress 29 | 4. Once the vulnerability is fixed, we will publicly acknowledge your responsible disclosure (unless you prefer to remain anonymous) 30 | 31 | Thank you for helping keep API Key Manager and its users safe! 32 | 33 | -------------------------------------------------------------------------------- /app/dashboard/keys/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import ApiKeyList from "@/components/api-key-list" 3 | import { useLanguage } from "@/lib/i18n/language-context" 4 | 5 | export default function ApiKeysPage() { 6 | const { t } = useLanguage() 7 | 8 | return ( 9 |
10 |

{t("apiKeys.title")}

11 | 12 |
13 | ) 14 | } 15 | 16 | -------------------------------------------------------------------------------- /app/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import type React from "react" 4 | import DashboardLayout from "@/components/dashboard-layout" 5 | import { LanguageProvider } from "@/lib/i18n/language-context" 6 | import ErrorNotification from "@/components/error-notification" 7 | 8 | export default function DashboardRootLayout({ 9 | children, 10 | }: { 11 | children: React.ReactNode 12 | }) { 13 | return ( 14 | 15 | 16 | {children} 17 | 18 | ) 19 | } 20 | 21 | -------------------------------------------------------------------------------- /app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" 4 | import ApiStatusCard from "@/components/api-balance-card" 5 | import { useLanguage } from "@/lib/i18n/language-context" 6 | import { useState, useEffect } from "react" 7 | import { apiKeyStorage } from "@/lib/storage" 8 | import { Progress } from "@/components/ui/progress" 9 | import { Activity } from "lucide-react" 10 | 11 | export default function DashboardPage() { 12 | const { t } = useLanguage() 13 | const [activeKeys, setActiveKeys] = useState(0) 14 | const [apiAvailability, setApiAvailability] = useState(0) 15 | 16 | useEffect(() => { 17 | const calculateApiStats = () => { 18 | // Get all API keys 19 | const keys = apiKeyStorage.getApiKeysByUserId(1) 20 | 21 | // Set the active keys count (total number of keys) 22 | setActiveKeys(keys.length) 23 | 24 | // Get connection test results from cache 25 | const cacheJson = localStorage.getItem("api_connection_test_results") 26 | if (!cacheJson) { 27 | setApiAvailability(0) 28 | return 29 | } 30 | 31 | const cache = JSON.parse(cacheJson) 32 | 33 | // Count available APIs (status 200-299) 34 | let availableCount = 0 35 | let totalTestedCount = 0 36 | 37 | keys.forEach((key) => { 38 | // Skip custom APIs for availability calculation 39 | if (key.provider === "Custom") return 40 | 41 | const testResult = cache[key.id] 42 | if (testResult) { 43 | totalTestedCount++ 44 | if (testResult.status >= 200 && testResult.status < 300) { 45 | availableCount++ 46 | } 47 | } 48 | }) 49 | 50 | // Calculate availability percentage 51 | const availability = totalTestedCount > 0 ? Math.round((availableCount / totalTestedCount) * 100) : 0 52 | 53 | setApiAvailability(availability) 54 | } 55 | 56 | calculateApiStats() 57 | }, []) 58 | 59 | // Add an event listener to update stats when API statuses are refreshed 60 | useEffect(() => { 61 | const handleApiStatusUpdate = () => { 62 | // Recalculate API stats when statuses are updated 63 | const calculateApiStats = () => { 64 | // Get all API keys 65 | const keys = apiKeyStorage.getApiKeysByUserId(1) 66 | 67 | // Set the active keys count (total number of keys) 68 | setActiveKeys(keys.length) 69 | 70 | // Get connection test results from cache 71 | const cacheJson = localStorage.getItem("api_connection_test_results") 72 | if (!cacheJson) { 73 | setApiAvailability(0) 74 | return 75 | } 76 | 77 | const cache = JSON.parse(cacheJson) 78 | 79 | // Count available APIs (status 200-299) 80 | let availableCount = 0 81 | let totalTestedCount = 0 82 | 83 | keys.forEach((key) => { 84 | // Skip custom APIs for availability calculation 85 | if (key.provider === "Custom") return 86 | 87 | const testResult = cache[key.id] 88 | if (testResult) { 89 | totalTestedCount++ 90 | if (testResult.status >= 200 && testResult.status < 300) { 91 | availableCount++ 92 | } 93 | } 94 | }) 95 | 96 | // Calculate availability percentage 97 | const availability = totalTestedCount > 0 ? Math.round((availableCount / totalTestedCount) * 100) : 0 98 | 99 | setApiAvailability(availability) 100 | } 101 | 102 | calculateApiStats() 103 | } 104 | 105 | // Add event listener for API status updates 106 | window.addEventListener("api-status-updated", handleApiStatusUpdate) 107 | 108 | // Clean up event listener 109 | return () => { 110 | window.removeEventListener("api-status-updated", handleApiStatusUpdate) 111 | } 112 | }, []) 113 | 114 | return ( 115 |
116 |

{t("dashboard.title")}

117 | 118 |
119 | 120 | 121 | {t("dashboard.activeKeys")} 122 | 123 | 124 |
{activeKeys}
125 |

{t("dashboard.comparedToLastMonth")}

126 |
127 |
128 | 129 | 130 | {t("dashboard.encryptedKeys")} 131 | 132 | 133 |
100%
134 |

{t("dashboard.allKeysEncrypted")}

135 |
136 |
137 | 138 | 139 | {t("dashboard.apiAvailability")} 140 | 141 | 142 | 143 |
{apiAvailability}%
144 |
145 | 146 |
147 |

148 | {apiAvailability >= 90 149 | ? t("dashboard.apiAvailabilityNormal") 150 | : apiAvailability >= 70 151 | ? t("dashboard.apiAvailabilityDelayed") 152 | : t("dashboard.apiAvailabilityIssues")} 153 |

154 |
155 |
156 |
157 | 158 | {/* API状态卡片 */} 159 |
160 | 161 |
162 |
163 | ) 164 | } 165 | 166 | -------------------------------------------------------------------------------- /app/dashboard/settings/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import SettingsForm from "@/components/settings-form" 3 | import { useLanguage } from "@/lib/i18n/language-context" 4 | 5 | export default function SettingsPage() { 6 | const { t } = useLanguage() 7 | 8 | return ( 9 |
10 |

{t("settings.title")}

11 | 12 |
13 | ) 14 | } 15 | 16 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 221.2 83.2% 53.3%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 221.2 83.2% 53.3%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 217.2 91.2% 59.8%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 224.3 76.3% 48%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } 77 | 78 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type React from "react" 2 | import type { Metadata } from "next" 3 | import { Inter } from "next/font/google" 4 | import "./globals.css" 5 | import { ThemeProvider } from "@/components/theme-provider" 6 | 7 | const inter = Inter({ subsets: ["latin"] }) 8 | 9 | export const metadata: Metadata = { 10 | title: "南梦API秘钥管理", 11 | description: "安全管理各种 AI 模型的 API 密钥", 12 | icons: { 13 | icon: [ 14 | { 15 | url: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/75cda7542df94f9e8534d82f9e31cae3_3-1flDTgHIuogz0ioIUPjC3BE9xbsIdP.png", 16 | sizes: "32x32", 17 | type: "image/png", 18 | }, 19 | { 20 | url: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/75cda7542df94f9e8534d82f9e31cae3_3-1flDTgHIuogz0ioIUPjC3BE9xbsIdP.png", 21 | sizes: "16x16", 22 | type: "image/png", 23 | }, 24 | ], 25 | apple: [ 26 | { 27 | url: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/75cda7542df94f9e8534d82f9e31cae3_3-1flDTgHIuogz0ioIUPjC3BE9xbsIdP.png", 28 | sizes: "180x180", 29 | type: "image/png", 30 | }, 31 | ], 32 | }, 33 | generator: 'v0.dev' 34 | } 35 | 36 | export default function RootLayout({ 37 | children, 38 | }: Readonly<{ 39 | children: React.ReactNode 40 | }>) { 41 | return ( 42 | 43 | 44 | 45 | {children} 46 | 47 | 48 | 49 | ) 50 | } 51 | 52 | 53 | 54 | import './globals.css' -------------------------------------------------------------------------------- /app/login/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import LoginForm from "@/components/login-form" 4 | import { useLanguage } from "@/lib/i18n/language-context" 5 | import LanguageSwitcher from "@/components/language-switcher" 6 | import { LanguageProvider } from "@/lib/i18n/language-context" 7 | 8 | export default function LoginPage() { 9 | return ( 10 | 11 | 12 | 13 | ) 14 | } 15 | 16 | function LoginContent() { 17 | const { t } = useLanguage() 18 | 19 | return ( 20 |
21 | {/* 添加语言切换器到右上角 */} 22 |
23 | 24 |
25 | 26 |
27 |

{t("app.title")}

28 | 29 |
30 |
31 | ) 32 | } 33 | 34 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Button } from "@/components/ui/button" 4 | import { Card, CardContent } from "@/components/ui/card" 5 | import { useLanguage } from "@/lib/i18n/language-context" 6 | import LanguageSwitcher from "@/components/language-switcher" 7 | import { LanguageProvider } from "@/lib/i18n/language-context" 8 | import Link from "next/link" 9 | import { Mail, Key, Shield, BarChart, Settings, Github } from "lucide-react" 10 | 11 | export default function Home() { 12 | return ( 13 | 14 | 15 | 16 | ) 17 | } 18 | 19 | function HomeContent() { 20 | const { t } = useLanguage() 21 | 22 | return ( 23 |
24 | {/* 导航栏 */} 25 | 40 | 41 | {/* 英雄区域 */} 42 |
43 |

44 | {t("app.title")} 45 |

46 |

{t("home.hero.subtitle")}

47 |
48 | 49 | 52 | 53 |
54 |
55 | 56 | {/* 功能特点 */} 57 |
58 |
59 |
60 |

61 | {t("home.features.title")} 62 |

63 |

64 | {t("home.features.subtitle")} 65 |

66 |
67 | 68 |
69 | } 71 | title={t("home.features.keyManagement.title")} 72 | description={t("home.features.keyManagement.description")} 73 | /> 74 | } 76 | title={t("home.features.security.title")} 77 | description={t("home.features.security.description")} 78 | /> 79 | } 81 | title={t("home.features.monitoring.title")} 82 | description={t("home.features.monitoring.description")} 83 | /> 84 | } 86 | title={t("home.features.settings.title")} 87 | description={t("home.features.settings.description")} 88 | /> 89 |
90 |
91 |
92 | 93 | {/* 联系方式 */} 94 |
95 |
96 |

{t("home.contact.title")}

97 |

{t("home.contact.subtitle")}

98 |
99 | 100 | nanmeng@nanmengtech.com 101 |
102 |
103 |
104 | 105 | {/* 页脚 */} 106 | 121 |
122 | ) 123 | } 124 | 125 | function FeatureCard({ icon, title, description }) { 126 | return ( 127 | 128 | 129 |
{icon}
130 |

{title}

131 |

{description}

132 |
133 |
134 | ) 135 | } 136 | 137 | -------------------------------------------------------------------------------- /app/register/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import type React from "react" 4 | 5 | import { LanguageProvider } from "@/lib/i18n/language-context" 6 | 7 | export default function RegisterLayout({ 8 | children, 9 | }: { 10 | children: React.ReactNode 11 | }) { 12 | return {children} 13 | } 14 | 15 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /components/dashboard-layout.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import type React from "react" 4 | 5 | import { useState } from "react" 6 | import Link from "next/link" 7 | import { usePathname } from "next/navigation" 8 | import { Button } from "@/components/ui/button" 9 | import { LayoutDashboard, Key, Settings, LogOut, Menu, X } from "lucide-react" 10 | import LanguageSwitcher from "@/components/language-switcher" 11 | import { useLanguage } from "@/lib/i18n/language-context" 12 | 13 | interface DashboardLayoutProps { 14 | children: React.ReactNode 15 | } 16 | 17 | export default function DashboardLayout({ children }: DashboardLayoutProps) { 18 | const pathname = usePathname() 19 | const [sidebarOpen, setSidebarOpen] = useState(false) 20 | const { t } = useLanguage() 21 | 22 | const toggleSidebar = () => { 23 | setSidebarOpen(!sidebarOpen) 24 | } 25 | 26 | const navItems = [ 27 | { href: "/dashboard", label: t("common.dashboard"), icon: }, 28 | { href: "/dashboard/keys", label: t("common.apiKeys"), icon: }, 29 | { href: "/dashboard/settings", label: t("common.settings"), icon: }, 30 | ] 31 | 32 | return ( 33 |
34 | {/* 移动端菜单按钮 */} 35 |
36 | 39 |
40 | 41 | {/* 语言切换器 - 移动端 */} 42 |
43 | 44 |
45 | 46 | {/* 侧边栏 - 减小宽度和间距 */} 47 |
53 |
54 |
55 |

{t("app.title")}

56 |
57 | 58 |
59 |
60 | 75 |
76 | 82 |
83 |
84 |
85 | 86 | {/* 主内容区 - 调整左边距并居中内容 */} 87 |
88 |
{children}
89 |
90 |
91 | ) 92 | } 93 | 94 | -------------------------------------------------------------------------------- /components/error-notification.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useEffect, useState } from "react" 4 | import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert" 5 | import { X } from "lucide-react" 6 | import { Button } from "@/components/ui/button" 7 | 8 | interface ErrorEvent extends CustomEvent { 9 | detail: { 10 | title: string 11 | message: string 12 | } 13 | } 14 | 15 | export default function ErrorNotification() { 16 | const [error, setError] = useState<{ title: string; message: string } | null>(null) 17 | const [visible, setVisible] = useState(false) 18 | 19 | useEffect(() => { 20 | const handleError = (event: Event) => { 21 | const errorEvent = event as ErrorEvent 22 | setError(errorEvent.detail) 23 | setVisible(true) 24 | 25 | // 自动5秒后隐藏 26 | setTimeout(() => { 27 | setVisible(false) 28 | }, 5000) 29 | } 30 | 31 | window.addEventListener("app-error", handleError) 32 | 33 | return () => { 34 | window.removeEventListener("app-error", handleError) 35 | } 36 | }, []) 37 | 38 | if (!error || !visible) return null 39 | 40 | return ( 41 |
42 | 43 |
44 |
45 | {error.title} 46 | {error.message} 47 |
48 | 56 |
57 |
58 |
59 | ) 60 | } 61 | 62 | -------------------------------------------------------------------------------- /components/language-switcher.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | import { Button } from "@/components/ui/button" 5 | import { Check, Globe } from "lucide-react" 6 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" 7 | import { useLanguage } from "@/lib/i18n/language-context" 8 | import { useToast } from "@/hooks/use-toast" 9 | 10 | export default function LanguageSwitcher() { 11 | const { language, setLanguage, t } = useLanguage() 12 | const { toast } = useToast() 13 | const [isOpen, setIsOpen] = useState(false) 14 | 15 | const languages = [ 16 | { code: "zh-CN", name: "中文简体" }, 17 | { code: "en-US", name: "English" }, 18 | ] 19 | 20 | const handleLanguageChange = (langCode: "zh-CN" | "en-US") => { 21 | setLanguage(langCode) 22 | setIsOpen(false) 23 | 24 | toast({ 25 | title: t("settings.languageChanged"), 26 | description: langCode === "zh-CN" ? "已切换到中文" : "Switched to English", 27 | }) 28 | } 29 | 30 | return ( 31 | 32 | 33 | 37 | 38 | 39 | {languages.map((lang) => ( 40 | handleLanguageChange(lang.code as "zh-CN" | "en-US")} 43 | className="flex items-center justify-between" 44 | > 45 | {lang.name} 46 | {language === lang.code && } 47 | 48 | ))} 49 | 50 | 51 | ) 52 | } 53 | 54 | -------------------------------------------------------------------------------- /components/login-form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import type React from "react" 4 | 5 | import { useState, useEffect } from "react" 6 | import { useRouter } from "next/navigation" 7 | import { Button } from "@/components/ui/button" 8 | import { Input } from "@/components/ui/input" 9 | import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" 10 | import { Label } from "@/components/ui/label" 11 | import { useLanguage } from "@/lib/i18n/language-context" 12 | import { userStorage, initStorage } from "@/lib/storage" 13 | 14 | export default function LoginForm() { 15 | const { t } = useLanguage() 16 | const [username, setUsername] = useState("") 17 | const [password, setPassword] = useState("") 18 | const [loading, setLoading] = useState(false) 19 | const [error, setError] = useState("") 20 | const router = useRouter() 21 | 22 | // 初始化本地存储 23 | useEffect(() => { 24 | initStorage() 25 | }, []) 26 | 27 | // 登录功能 28 | const handleLogin = async (e: React.FormEvent) => { 29 | e.preventDefault() 30 | setLoading(true) 31 | setError("") 32 | 33 | try { 34 | // 模拟延迟 35 | await new Promise((resolve) => setTimeout(resolve, 800)) 36 | 37 | // 验证用户 38 | const user = userStorage.getUserByUsername(username) 39 | 40 | if (user && user.password === password) { 41 | // 存储会话信息 42 | localStorage.setItem( 43 | "user", 44 | JSON.stringify({ 45 | id: user.id, 46 | username: user.username, 47 | email: user.email, 48 | }), 49 | ) 50 | 51 | router.push("/dashboard") 52 | } else { 53 | setError(t("login.error")) 54 | } 55 | } catch (err) { 56 | setError(t("error.loginFailed")) 57 | } finally { 58 | setLoading(false) 59 | } 60 | } 61 | 62 | return ( 63 | 64 | 65 | {t("login.title")} 66 | {t("login.description")} 67 | 68 | 69 |
70 |
71 | 72 | setUsername(e.target.value)} 78 | required 79 | /> 80 |
81 |
82 | 83 | setPassword(e.target.value)} 89 | required 90 | /> 91 |
92 | {error &&

{error}

} 93 |
94 |
95 | 96 | 99 |
100 |

101 |
102 |
103 |
104 | ) 105 | } 106 | 107 | -------------------------------------------------------------------------------- /components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { ThemeProvider as NextThemesProvider } from "next-themes" 3 | import type { ThemeProviderProps } from "next-themes" 4 | 5 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 6 | return {children} 7 | } 8 | 9 | -------------------------------------------------------------------------------- /components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 5 | import { ChevronDown } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Accordion = AccordionPrimitive.Root 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )) 21 | AccordionItem.displayName = "AccordionItem" 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180", 32 | className 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )) 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 52 |
{children}
53 |
54 | )) 55 | 56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 57 | 58 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 59 | -------------------------------------------------------------------------------- /components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" 5 | 6 | import { cn } from "@/lib/utils" 7 | import { buttonVariants } from "@/components/ui/button" 8 | 9 | const AlertDialog = AlertDialogPrimitive.Root 10 | 11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger 12 | 13 | const AlertDialogPortal = AlertDialogPrimitive.Portal 14 | 15 | const AlertDialogOverlay = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => ( 19 | 27 | )) 28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName 29 | 30 | const AlertDialogContent = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, ...props }, ref) => ( 34 | 35 | 36 | 44 | 45 | )) 46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName 47 | 48 | const AlertDialogHeader = ({ 49 | className, 50 | ...props 51 | }: React.HTMLAttributes) => ( 52 |
59 | ) 60 | AlertDialogHeader.displayName = "AlertDialogHeader" 61 | 62 | const AlertDialogFooter = ({ 63 | className, 64 | ...props 65 | }: React.HTMLAttributes) => ( 66 |
73 | ) 74 | AlertDialogFooter.displayName = "AlertDialogFooter" 75 | 76 | const AlertDialogTitle = React.forwardRef< 77 | React.ElementRef, 78 | React.ComponentPropsWithoutRef 79 | >(({ className, ...props }, ref) => ( 80 | 85 | )) 86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName 87 | 88 | const AlertDialogDescription = React.forwardRef< 89 | React.ElementRef, 90 | React.ComponentPropsWithoutRef 91 | >(({ className, ...props }, ref) => ( 92 | 97 | )) 98 | AlertDialogDescription.displayName = 99 | AlertDialogPrimitive.Description.displayName 100 | 101 | const AlertDialogAction = React.forwardRef< 102 | React.ElementRef, 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )) 111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName 112 | 113 | const AlertDialogCancel = React.forwardRef< 114 | React.ElementRef, 115 | React.ComponentPropsWithoutRef 116 | >(({ className, ...props }, ref) => ( 117 | 126 | )) 127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName 128 | 129 | export { 130 | AlertDialog, 131 | AlertDialogPortal, 132 | AlertDialogOverlay, 133 | AlertDialogTrigger, 134 | AlertDialogContent, 135 | AlertDialogHeader, 136 | AlertDialogFooter, 137 | AlertDialogTitle, 138 | AlertDialogDescription, 139 | AlertDialogAction, 140 | AlertDialogCancel, 141 | } 142 | -------------------------------------------------------------------------------- /components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 13 | }, 14 | }, 15 | defaultVariants: { 16 | variant: "default", 17 | }, 18 | }, 19 | ) 20 | 21 | const Alert = React.forwardRef< 22 | HTMLDivElement, 23 | React.HTMLAttributes & VariantProps 24 | >(({ className, variant, ...props }, ref) => ( 25 |
26 | )) 27 | Alert.displayName = "Alert" 28 | 29 | const AlertTitle = React.forwardRef>( 30 | ({ className, ...props }, ref) => ( 31 |
32 | ), 33 | ) 34 | AlertTitle.displayName = "AlertTitle" 35 | 36 | const AlertDescription = React.forwardRef>( 37 | ({ className, ...props }, ref) => ( 38 |
39 | ), 40 | ) 41 | AlertDescription.displayName = "AlertDescription" 42 | 43 | export { Alert, AlertTitle, AlertDescription } 44 | 45 | -------------------------------------------------------------------------------- /components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root 6 | 7 | export { AspectRatio } 8 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { ChevronRight, MoreHorizontal } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Breadcrumb = React.forwardRef< 8 | HTMLElement, 9 | React.ComponentPropsWithoutRef<"nav"> & { 10 | separator?: React.ReactNode 11 | } 12 | >(({ ...props }, ref) =>