Spaces:
Sleeping
Sleeping
Commit ·
fa813a0
0
Parent(s):
initial commit: enrich functionality and configure for Hugging Face
Browse files- .gitignore +25 -0
- Dockerfile +37 -0
- README.md +74 -0
- api/app.ts +69 -0
- api/index.ts +9 -0
- api/routes/data.ts +40 -0
- api/routes/system.ts +44 -0
- api/server.ts +34 -0
- docker-compose.yml +11 -0
- eslint.config.js +28 -0
- index.html +24 -0
- nodemon.json +10 -0
- package-lock.json +0 -0
- package.json +62 -0
- postcss.config.js +10 -0
- public/favicon.svg +4 -0
- src/App.tsx +32 -0
- src/assets/react.svg +1 -0
- src/components/Empty.tsx +8 -0
- src/components/Navbar.tsx +81 -0
- src/hooks/useTheme.ts +29 -0
- src/index.css +32 -0
- src/lib/utils.ts +6 -0
- src/main.tsx +10 -0
- src/pages/About.tsx +90 -0
- src/pages/ApiDocs.tsx +92 -0
- src/pages/Home.tsx +126 -0
- src/pages/Tasks.tsx +145 -0
- src/vite-env.d.ts +1 -0
- tailwind.config.js +13 -0
- tsconfig.json +40 -0
- vercel.json +12 -0
- vite.config.ts +47 -0
.gitignore
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
.vite
|
Dockerfile
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 使用 Node.js 18 官方镜像
|
| 2 |
+
FROM node:18-alpine AS builder
|
| 3 |
+
|
| 4 |
+
# 设置工作目录
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# 复制 package.json 和 lock 文件
|
| 8 |
+
COPY package*.json ./
|
| 9 |
+
|
| 10 |
+
# 安装依赖
|
| 11 |
+
RUN npm install
|
| 12 |
+
|
| 13 |
+
# 复制所有源代码
|
| 14 |
+
COPY . .
|
| 15 |
+
|
| 16 |
+
# 构建前端应用
|
| 17 |
+
RUN npm run build
|
| 18 |
+
|
| 19 |
+
# 运行环境阶段
|
| 20 |
+
FROM node:18-alpine
|
| 21 |
+
|
| 22 |
+
WORKDIR /app
|
| 23 |
+
|
| 24 |
+
# 复制构建好的文件和必要文件
|
| 25 |
+
COPY --from=builder /app/dist ./dist
|
| 26 |
+
COPY --from=builder /app/package*.json ./
|
| 27 |
+
COPY --from=builder /app/node_modules ./node_modules
|
| 28 |
+
|
| 29 |
+
# 设置环境变量
|
| 30 |
+
ENV NODE_ENV=production
|
| 31 |
+
ENV PORT=7860
|
| 32 |
+
|
| 33 |
+
# 暴露端口
|
| 34 |
+
EXPOSE 7860
|
| 35 |
+
|
| 36 |
+
# 启动命令
|
| 37 |
+
CMD ["npm", "start"]
|
README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: "Koa 现代化中文 Web 应用"
|
| 3 |
+
emoji: 🚀
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
short_description: "Koa 现代化中文 Web 应用"
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# Koa Web 应用 🚀
|
| 11 |
+
|
| 12 |
+
这是一个基于 Koa 和 React 构建的高性能、全中文 Web 服务框架。本项目旨在为开发者提供一个开箱即用的 Web 应用模板,包含完整的后端 API 和前端界面。
|
| 13 |
+
|
| 14 |
+
## ✨ 特性
|
| 15 |
+
|
| 16 |
+
- 🌏 **全中文支持**:界面内容、代码注释及文档均采用中文。
|
| 17 |
+
- ⚡ **高性能**:利用 Koa 的轻量级异步中间件和 React 18 的高效渲染。
|
| 18 |
+
- 📱 **响应式设计**:完美适配手机、平板及桌面设备。
|
| 19 |
+
- 🐳 **容器化部署**:提供 Docker 和 docker-compose 配置文件。
|
| 20 |
+
- 🚀 **一键部署**:完美支持 Hugging Face Spaces 部署。
|
| 21 |
+
|
| 22 |
+
## 🛠️ 技术栈
|
| 23 |
+
|
| 24 |
+
- **前端**: React 18, Tailwind CSS, Lucide Icons, Vite
|
| 25 |
+
- **后端**: Koa 2, Koa-Router, Koa-Bodyparser
|
| 26 |
+
- **工具**: TypeScript, Docker, Node.js 18+
|
| 27 |
+
|
| 28 |
+
## 🚀 快速开始
|
| 29 |
+
|
| 30 |
+
### 1. 本地开发
|
| 31 |
+
|
| 32 |
+
```bash
|
| 33 |
+
# 安装依赖
|
| 34 |
+
npm install
|
| 35 |
+
|
| 36 |
+
# 启动开发环境 (前端: 5173, 后端: 3001)
|
| 37 |
+
npm run dev
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
### 2. 生产构建
|
| 41 |
+
|
| 42 |
+
```bash
|
| 43 |
+
# 构建前端并准备生产环境
|
| 44 |
+
npm run build
|
| 45 |
+
|
| 46 |
+
# 启动生产服务
|
| 47 |
+
npm start
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
### 3. Docker 部署
|
| 51 |
+
|
| 52 |
+
```bash
|
| 53 |
+
# 构建并运行
|
| 54 |
+
docker build -t koa-web-app .
|
| 55 |
+
docker run -p 3000:3000 koa-web-app
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
## 📂 项目结构
|
| 59 |
+
|
| 60 |
+
```text
|
| 61 |
+
koa-web-app/
|
| 62 |
+
├── api/ # Koa 后端服务
|
| 63 |
+
│ ├── routes/ # 路由定义
|
| 64 |
+
│ └── app.ts # Koa 应用核心
|
| 65 |
+
├── src/ # React 前端应用
|
| 66 |
+
│ ├── components/ # 组件
|
| 67 |
+
│ └── pages/ # 页面
|
| 68 |
+
├── Dockerfile # Docker 配置
|
| 69 |
+
└── README.md # 项目文档
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
## 📝 许可证
|
| 73 |
+
|
| 74 |
+
MIT License
|
api/app.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Koa from 'koa'
|
| 2 |
+
import bodyParser from 'koa-bodyparser'
|
| 3 |
+
import logger from 'koa-logger'
|
| 4 |
+
import cors from '@koa/cors'
|
| 5 |
+
import serve from 'koa-static'
|
| 6 |
+
import path from 'path'
|
| 7 |
+
import { fileURLToPath } from 'url'
|
| 8 |
+
import systemRouter from './routes/system.js'
|
| 9 |
+
import dataRouter from './routes/data.js'
|
| 10 |
+
|
| 11 |
+
const __filename = fileURLToPath(import.meta.url)
|
| 12 |
+
const __dirname = path.dirname(__filename)
|
| 13 |
+
|
| 14 |
+
const app = new Koa()
|
| 15 |
+
|
| 16 |
+
// 中间件配置
|
| 17 |
+
app.use(cors())
|
| 18 |
+
app.use(logger())
|
| 19 |
+
app.use(bodyParser({
|
| 20 |
+
jsonLimit: '10mb',
|
| 21 |
+
formLimit: '10mb'
|
| 22 |
+
}))
|
| 23 |
+
|
| 24 |
+
// 错误处理
|
| 25 |
+
app.use(async (ctx, next) => {
|
| 26 |
+
try {
|
| 27 |
+
await next()
|
| 28 |
+
} catch (err: unknown) {
|
| 29 |
+
const error = err as { status?: number; message?: string }
|
| 30 |
+
ctx.status = error.status || 500
|
| 31 |
+
ctx.body = {
|
| 32 |
+
success: false,
|
| 33 |
+
error: error.message || '服务器内部错误'
|
| 34 |
+
}
|
| 35 |
+
ctx.app.emit('error', err, ctx)
|
| 36 |
+
}
|
| 37 |
+
})
|
| 38 |
+
|
| 39 |
+
// 路由注册
|
| 40 |
+
app.use(systemRouter.routes()).use(systemRouter.allowedMethods())
|
| 41 |
+
app.use(dataRouter.routes()).use(dataRouter.allowedMethods())
|
| 42 |
+
|
| 43 |
+
// 生产环境下托管静态文件
|
| 44 |
+
if (process.env.NODE_ENV === 'production') {
|
| 45 |
+
const distPath = path.resolve(__dirname, '../dist')
|
| 46 |
+
app.use(serve(distPath))
|
| 47 |
+
|
| 48 |
+
// 处理 SPA 路由
|
| 49 |
+
app.use(async (ctx, next) => {
|
| 50 |
+
if (ctx.status === 404 && !ctx.path.startsWith('/api')) {
|
| 51 |
+
ctx.type = 'html'
|
| 52 |
+
ctx.body = await import('fs').then(fs => fs.readFileSync(path.join(distPath, 'index.html')))
|
| 53 |
+
} else {
|
| 54 |
+
await next()
|
| 55 |
+
}
|
| 56 |
+
})
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
// 404 处理
|
| 60 |
+
app.use(async (ctx) => {
|
| 61 |
+
if (ctx.status === 404) {
|
| 62 |
+
ctx.body = {
|
| 63 |
+
success: false,
|
| 64 |
+
error: '接口未找到'
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
})
|
| 68 |
+
|
| 69 |
+
export default app
|
api/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Vercel deploy entry handler, for serverless deployment
|
| 3 |
+
*/
|
| 4 |
+
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
| 5 |
+
import app from './app.js';
|
| 6 |
+
|
| 7 |
+
export default function handler(req: VercelRequest, res: VercelResponse) {
|
| 8 |
+
return app.callback()(req, res);
|
| 9 |
+
}
|
api/routes/data.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Router from 'koa-router'
|
| 2 |
+
|
| 3 |
+
const router = new Router({ prefix: '/api/data' })
|
| 4 |
+
|
| 5 |
+
// Mock 数据
|
| 6 |
+
const mockTasks = [
|
| 7 |
+
{ id: 1, title: '完成 Koa 后端开发', completed: true, category: '开发' },
|
| 8 |
+
{ id: 2, title: '配置 Hugging Face 部署', completed: false, category: '部署' },
|
| 9 |
+
{ id: 3, title: '编写项目中文文档', completed: true, category: '文档' },
|
| 10 |
+
{ id: 4, title: '优化 React 前端 UI', completed: false, category: '设计' },
|
| 11 |
+
{ id: 5, title: '集成 Docker 容器化', completed: true, category: '运维' },
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
/**
|
| 15 |
+
* 获取任务列表
|
| 16 |
+
*/
|
| 17 |
+
router.get('/tasks', async (ctx) => {
|
| 18 |
+
ctx.body = {
|
| 19 |
+
success: true,
|
| 20 |
+
data: mockTasks,
|
| 21 |
+
total: mockTasks.length
|
| 22 |
+
}
|
| 23 |
+
})
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* 获取系统状态
|
| 27 |
+
*/
|
| 28 |
+
router.get('/stats', async (ctx) => {
|
| 29 |
+
ctx.body = {
|
| 30 |
+
success: true,
|
| 31 |
+
data: {
|
| 32 |
+
activeUsers: 128,
|
| 33 |
+
totalRequests: 5432,
|
| 34 |
+
uptime: process.uptime(),
|
| 35 |
+
version: '1.0.0'
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
})
|
| 39 |
+
|
| 40 |
+
export default router
|
api/routes/system.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Router from 'koa-router'
|
| 2 |
+
import os from 'os'
|
| 3 |
+
|
| 4 |
+
const router = new Router({ prefix: '/api' })
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* 健康检查接口
|
| 8 |
+
*/
|
| 9 |
+
router.get('/health', async (ctx) => {
|
| 10 |
+
ctx.body = {
|
| 11 |
+
status: 'ok',
|
| 12 |
+
timestamp: Date.now(),
|
| 13 |
+
}
|
| 14 |
+
})
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* 系统信息接口
|
| 18 |
+
*/
|
| 19 |
+
router.get('/info', async (ctx) => {
|
| 20 |
+
const used = process.memoryUsage().heapUsed / 1024 / 1024
|
| 21 |
+
ctx.body = {
|
| 22 |
+
nodeVersion: process.version,
|
| 23 |
+
platform: process.platform,
|
| 24 |
+
memoryUsage: {
|
| 25 |
+
used: Math.round(used * 100) / 100,
|
| 26 |
+
total: Math.round(os.totalmem() / 1024 / 1024 * 100) / 100,
|
| 27 |
+
unit: 'MB',
|
| 28 |
+
},
|
| 29 |
+
}
|
| 30 |
+
})
|
| 31 |
+
|
| 32 |
+
/**
|
| 33 |
+
* 服务器时间接口
|
| 34 |
+
*/
|
| 35 |
+
router.get('/time', async (ctx) => {
|
| 36 |
+
const now = new Date()
|
| 37 |
+
ctx.body = {
|
| 38 |
+
serverTime: now.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
|
| 39 |
+
timezone: 'Asia/Shanghai',
|
| 40 |
+
utc: now.toISOString(),
|
| 41 |
+
}
|
| 42 |
+
})
|
| 43 |
+
|
| 44 |
+
export default router
|
api/server.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* local server entry file, for local development
|
| 3 |
+
*/
|
| 4 |
+
import app from './app.js';
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* start server with port
|
| 8 |
+
*/
|
| 9 |
+
const PORT = process.env.PORT || 7860;
|
| 10 |
+
|
| 11 |
+
const server = app.listen(PORT, () => {
|
| 12 |
+
console.log(`Server ready on port ${PORT}`);
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
/**
|
| 16 |
+
* close server
|
| 17 |
+
*/
|
| 18 |
+
process.on('SIGTERM', () => {
|
| 19 |
+
console.log('SIGTERM signal received');
|
| 20 |
+
server.close(() => {
|
| 21 |
+
console.log('Server closed');
|
| 22 |
+
process.exit(0);
|
| 23 |
+
});
|
| 24 |
+
});
|
| 25 |
+
|
| 26 |
+
process.on('SIGINT', () => {
|
| 27 |
+
console.log('SIGINT signal received');
|
| 28 |
+
server.close(() => {
|
| 29 |
+
console.log('Server closed');
|
| 30 |
+
process.exit(0);
|
| 31 |
+
});
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
export default app;
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
koa-app:
|
| 5 |
+
build: .
|
| 6 |
+
ports:
|
| 7 |
+
- "3000:3000"
|
| 8 |
+
environment:
|
| 9 |
+
- NODE_ENV=production
|
| 10 |
+
- PORT=3000
|
| 11 |
+
restart: unless-stopped
|
eslint.config.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import js from '@eslint/js'
|
| 2 |
+
import globals from 'globals'
|
| 3 |
+
import reactHooks from 'eslint-plugin-react-hooks'
|
| 4 |
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
| 5 |
+
import tseslint from 'typescript-eslint'
|
| 6 |
+
|
| 7 |
+
export default tseslint.config(
|
| 8 |
+
{ ignores: ['dist'] },
|
| 9 |
+
{
|
| 10 |
+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
| 11 |
+
files: ['**/*.{ts,tsx}'],
|
| 12 |
+
languageOptions: {
|
| 13 |
+
ecmaVersion: 2020,
|
| 14 |
+
globals: globals.browser,
|
| 15 |
+
},
|
| 16 |
+
plugins: {
|
| 17 |
+
'react-hooks': reactHooks,
|
| 18 |
+
'react-refresh': reactRefresh,
|
| 19 |
+
},
|
| 20 |
+
rules: {
|
| 21 |
+
...reactHooks.configs.recommended.rules,
|
| 22 |
+
'react-refresh/only-export-components': [
|
| 23 |
+
'warn',
|
| 24 |
+
{ allowConstantExport: true },
|
| 25 |
+
],
|
| 26 |
+
},
|
| 27 |
+
},
|
| 28 |
+
)
|
index.html
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>My Trae Project</title>
|
| 8 |
+
<script type="module">
|
| 9 |
+
if (import.meta.hot?.on) {
|
| 10 |
+
import.meta.hot.on('vite:error', (error) => {
|
| 11 |
+
if (error.err) {
|
| 12 |
+
console.error(
|
| 13 |
+
[error.err.message, error.err.frame].filter(Boolean).join('\n'),
|
| 14 |
+
)
|
| 15 |
+
}
|
| 16 |
+
})
|
| 17 |
+
}
|
| 18 |
+
</script>
|
| 19 |
+
</head>
|
| 20 |
+
<body>
|
| 21 |
+
<div id="root"></div>
|
| 22 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 23 |
+
</body>
|
| 24 |
+
</html>
|
nodemon.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"watch": ["api"],
|
| 3 |
+
"ext": "ts,mts,js,json",
|
| 4 |
+
"ignore": ["api/dist/*"],
|
| 5 |
+
"exec": "tsx api/server.ts",
|
| 6 |
+
"env": {
|
| 7 |
+
"NODE_ENV": "development"
|
| 8 |
+
},
|
| 9 |
+
"delay": 1000
|
| 10 |
+
}
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "koa-web-app",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"client:dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"start": "NODE_ENV=production tsx api/server.ts",
|
| 10 |
+
"lint": "eslint .",
|
| 11 |
+
"preview": "vite preview",
|
| 12 |
+
"check": "tsc --noEmit",
|
| 13 |
+
"server:dev": "nodemon",
|
| 14 |
+
"dev": "concurrently \"npm run client:dev\" \"npm run server:dev\""
|
| 15 |
+
},
|
| 16 |
+
"dependencies": {
|
| 17 |
+
"@koa/cors": "^5.0.0",
|
| 18 |
+
"@types/koa": "^3.0.1",
|
| 19 |
+
"@types/koa-bodyparser": "^4.3.13",
|
| 20 |
+
"@types/koa-cors": "^0.0.6",
|
| 21 |
+
"@types/koa-logger": "^3.1.5",
|
| 22 |
+
"@types/koa-router": "^7.4.9",
|
| 23 |
+
"@types/koa-static": "^4.0.4",
|
| 24 |
+
"clsx": "^2.1.1",
|
| 25 |
+
"dotenv": "^17.2.1",
|
| 26 |
+
"koa": "^3.1.2",
|
| 27 |
+
"koa-bodyparser": "^4.4.1",
|
| 28 |
+
"koa-logger": "^4.0.0",
|
| 29 |
+
"koa-router": "^14.0.0",
|
| 30 |
+
"koa-static": "^5.0.0",
|
| 31 |
+
"lucide-react": "^0.511.0",
|
| 32 |
+
"react": "^18.3.1",
|
| 33 |
+
"react-dom": "^18.3.1",
|
| 34 |
+
"react-router-dom": "^7.3.0",
|
| 35 |
+
"tailwind-merge": "^3.0.2",
|
| 36 |
+
"zustand": "^5.0.3"
|
| 37 |
+
},
|
| 38 |
+
"devDependencies": {
|
| 39 |
+
"@eslint/js": "^9.25.0",
|
| 40 |
+
"@types/node": "^22.15.30",
|
| 41 |
+
"@types/react": "^18.3.12",
|
| 42 |
+
"@types/react-dom": "^18.3.1",
|
| 43 |
+
"@vercel/node": "^5.3.6",
|
| 44 |
+
"@vitejs/plugin-react": "^4.4.1",
|
| 45 |
+
"autoprefixer": "^10.4.21",
|
| 46 |
+
"babel-plugin-react-dev-locator": "^1.0.0",
|
| 47 |
+
"concurrently": "^9.2.0",
|
| 48 |
+
"eslint": "^9.25.0",
|
| 49 |
+
"eslint-plugin-react-hooks": "^5.2.0",
|
| 50 |
+
"eslint-plugin-react-refresh": "^0.4.19",
|
| 51 |
+
"globals": "^16.0.0",
|
| 52 |
+
"nodemon": "^3.1.10",
|
| 53 |
+
"postcss": "^8.5.3",
|
| 54 |
+
"tailwindcss": "^3.4.17",
|
| 55 |
+
"tsx": "^4.20.3",
|
| 56 |
+
"typescript": "~5.8.3",
|
| 57 |
+
"typescript-eslint": "^8.30.1",
|
| 58 |
+
"vite": "^6.3.5",
|
| 59 |
+
"vite-plugin-trae-solo-badge": "^1.0.0",
|
| 60 |
+
"vite-tsconfig-paths": "^5.1.4"
|
| 61 |
+
}
|
| 62 |
+
}
|
postcss.config.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** WARNING: DON'T EDIT THIS FILE */
|
| 2 |
+
/** WARNING: DON'T EDIT THIS FILE */
|
| 3 |
+
/** WARNING: DON'T EDIT THIS FILE */
|
| 4 |
+
|
| 5 |
+
export default {
|
| 6 |
+
plugins: {
|
| 7 |
+
tailwindcss: {},
|
| 8 |
+
autoprefixer: {},
|
| 9 |
+
},
|
| 10 |
+
};
|
public/favicon.svg
ADDED
|
|
src/App.tsx
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
| 3 |
+
import Navbar from './components/Navbar';
|
| 4 |
+
import Home from './pages/Home';
|
| 5 |
+
import About from './pages/About';
|
| 6 |
+
import ApiDocs from './pages/ApiDocs';
|
| 7 |
+
import Tasks from './pages/Tasks';
|
| 8 |
+
|
| 9 |
+
const App: React.FC = () => {
|
| 10 |
+
return (
|
| 11 |
+
<Router>
|
| 12 |
+
<div className="min-h-screen bg-gray-50 flex flex-col">
|
| 13 |
+
<Navbar />
|
| 14 |
+
<main className="flex-grow container mx-auto px-4 py-8 sm:px-6 lg:px-8">
|
| 15 |
+
<Routes>
|
| 16 |
+
<Route path="/" element={<Home />} />
|
| 17 |
+
<Route path="/tasks" element={<Tasks />} />
|
| 18 |
+
<Route path="/about" element={<About />} />
|
| 19 |
+
<Route path="/api-docs" element={<ApiDocs />} />
|
| 20 |
+
</Routes>
|
| 21 |
+
</main>
|
| 22 |
+
<footer className="bg-white border-t border-gray-200 py-8">
|
| 23 |
+
<div className="container mx-auto px-4 text-center text-gray-500 text-sm">
|
| 24 |
+
<p>© {new Date().getFullYear()} Koa Web 应用 - 极致性能,全中文支持</p>
|
| 25 |
+
</div>
|
| 26 |
+
</footer>
|
| 27 |
+
</div>
|
| 28 |
+
</Router>
|
| 29 |
+
);
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
export default App;
|
src/assets/react.svg
ADDED
|
|
src/components/Empty.tsx
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { cn } from '@/lib/utils'
|
| 2 |
+
|
| 3 |
+
// Empty component
|
| 4 |
+
export default function Empty() {
|
| 5 |
+
return (
|
| 6 |
+
<div className={cn('flex h-full items-center justify-center')}>Empty</div>
|
| 7 |
+
)
|
| 8 |
+
}
|
src/components/Navbar.tsx
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import { Link, useLocation } from 'react-router-dom';
|
| 3 |
+
import { Menu, X, Activity } from 'lucide-react';
|
| 4 |
+
|
| 5 |
+
const Navbar: React.FC = () => {
|
| 6 |
+
const [isOpen, setIsOpen] = useState(false);
|
| 7 |
+
const location = useLocation();
|
| 8 |
+
|
| 9 |
+
const navItems = [
|
| 10 |
+
{ name: '首页', path: '/' },
|
| 11 |
+
{ name: '任务管理', path: '/tasks' },
|
| 12 |
+
{ name: '关于', path: '/about' },
|
| 13 |
+
{ name: 'API文档', path: '/api-docs' },
|
| 14 |
+
];
|
| 15 |
+
|
| 16 |
+
return (
|
| 17 |
+
<nav className="bg-blue-900 text-white shadow-lg sticky top-0 z-50">
|
| 18 |
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
| 19 |
+
<div className="flex items-center justify-between h-16">
|
| 20 |
+
<div className="flex items-center">
|
| 21 |
+
<Link to="/" className="flex items-center space-x-2">
|
| 22 |
+
<Activity className="h-8 w-8 text-blue-400" />
|
| 23 |
+
<span className="font-bold text-xl">Koa Web 应用</span>
|
| 24 |
+
</Link>
|
| 25 |
+
</div>
|
| 26 |
+
|
| 27 |
+
<div className="hidden md:block">
|
| 28 |
+
<div className="ml-10 flex items-baseline space-x-4">
|
| 29 |
+
{navItems.map((item) => (
|
| 30 |
+
<Link
|
| 31 |
+
key={item.path}
|
| 32 |
+
to={item.path}
|
| 33 |
+
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
| 34 |
+
location.pathname === item.path
|
| 35 |
+
? 'bg-blue-800 text-white'
|
| 36 |
+
: 'text-blue-100 hover:bg-blue-800 hover:text-white'
|
| 37 |
+
}`}
|
| 38 |
+
>
|
| 39 |
+
{item.name}
|
| 40 |
+
</Link>
|
| 41 |
+
))}
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
<div className="md:hidden">
|
| 46 |
+
<button
|
| 47 |
+
onClick={() => setIsOpen(!isOpen)}
|
| 48 |
+
className="inline-flex items-center justify-center p-2 rounded-md text-blue-100 hover:text-white hover:bg-blue-800 focus:outline-none"
|
| 49 |
+
>
|
| 50 |
+
{isOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
|
| 51 |
+
</button>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
|
| 56 |
+
{/* Mobile menu */}
|
| 57 |
+
{isOpen && (
|
| 58 |
+
<div className="md:hidden bg-blue-900 border-t border-blue-800">
|
| 59 |
+
<div className="px-2 pt-2 pb-3 space-y-1 sm:px-3">
|
| 60 |
+
{navItems.map((item) => (
|
| 61 |
+
<Link
|
| 62 |
+
key={item.path}
|
| 63 |
+
to={item.path}
|
| 64 |
+
onClick={() => setIsOpen(false)}
|
| 65 |
+
className={`block px-3 py-2 rounded-md text-base font-medium ${
|
| 66 |
+
location.pathname === item.path
|
| 67 |
+
? 'bg-blue-800 text-white'
|
| 68 |
+
: 'text-blue-100 hover:bg-blue-800 hover:text-white'
|
| 69 |
+
}`}
|
| 70 |
+
>
|
| 71 |
+
{item.name}
|
| 72 |
+
</Link>
|
| 73 |
+
))}
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
)}
|
| 77 |
+
</nav>
|
| 78 |
+
);
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
export default Navbar;
|
src/hooks/useTheme.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect } from 'react';
|
| 2 |
+
|
| 3 |
+
type Theme = 'light' | 'dark';
|
| 4 |
+
|
| 5 |
+
export function useTheme() {
|
| 6 |
+
const [theme, setTheme] = useState<Theme>(() => {
|
| 7 |
+
const savedTheme = localStorage.getItem('theme') as Theme;
|
| 8 |
+
if (savedTheme) {
|
| 9 |
+
return savedTheme;
|
| 10 |
+
}
|
| 11 |
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
| 12 |
+
});
|
| 13 |
+
|
| 14 |
+
useEffect(() => {
|
| 15 |
+
document.documentElement.classList.remove('light', 'dark');
|
| 16 |
+
document.documentElement.classList.add(theme);
|
| 17 |
+
localStorage.setItem('theme', theme);
|
| 18 |
+
}, [theme]);
|
| 19 |
+
|
| 20 |
+
const toggleTheme = () => {
|
| 21 |
+
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
return {
|
| 25 |
+
theme,
|
| 26 |
+
toggleTheme,
|
| 27 |
+
isDark: theme === 'dark'
|
| 28 |
+
};
|
| 29 |
+
}
|
src/index.css
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
| 4 |
+
|
| 5 |
+
:root {
|
| 6 |
+
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
| 7 |
+
line-height: 1.5;
|
| 8 |
+
font-weight: 400;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
body {
|
| 12 |
+
margin: 0;
|
| 13 |
+
min-width: 320px;
|
| 14 |
+
min-height: 100vh;
|
| 15 |
+
@apply bg-gray-50 text-gray-900;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
@layer components {
|
| 19 |
+
.btn-primary {
|
| 20 |
+
@apply bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg font-medium transition-all active:scale-95;
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/* 自定义动画 */
|
| 25 |
+
@keyframes fadeIn {
|
| 26 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 27 |
+
to { opacity: 1; transform: translateY(0); }
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.animate-in {
|
| 31 |
+
animation: fadeIn 0.5s ease-out forwards;
|
| 32 |
+
}
|
src/lib/utils.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { clsx, type ClassValue } from "clsx"
|
| 2 |
+
import { twMerge } from "tailwind-merge"
|
| 3 |
+
|
| 4 |
+
export function cn(...inputs: ClassValue[]) {
|
| 5 |
+
return twMerge(clsx(inputs))
|
| 6 |
+
}
|
src/main.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { StrictMode } from 'react'
|
| 2 |
+
import { createRoot } from 'react-dom/client'
|
| 3 |
+
import App from './App'
|
| 4 |
+
import './index.css'
|
| 5 |
+
|
| 6 |
+
createRoot(document.getElementById('root')!).render(
|
| 7 |
+
<StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</StrictMode>,
|
| 10 |
+
)
|
src/pages/About.tsx
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { Shield, Zap, Globe, Layout, Package, Code } from 'lucide-react';
|
| 3 |
+
|
| 4 |
+
const About: React.FC = () => {
|
| 5 |
+
const features = [
|
| 6 |
+
{
|
| 7 |
+
icon: <Globe className="h-6 w-6 text-blue-500" />,
|
| 8 |
+
title: '完全汉化',
|
| 9 |
+
description: '所有界面元素、注释和文档均使用中文,对国内开发者友好。'
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
icon: <Layout className="h-6 w-6 text-purple-500" />,
|
| 13 |
+
title: '响应式布局',
|
| 14 |
+
description: '基于 Tailwind CSS 构建,完美适配手机、平板和桌面设备。'
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
icon: <Zap className="h-6 w-6 text-yellow-500" />,
|
| 18 |
+
title: '高性能',
|
| 19 |
+
description: '利用 Koa 的异步中间件机制和 React 的高效渲染能力。'
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
icon: <Shield className="h-6 w-6 text-green-500" />,
|
| 23 |
+
title: '生产就绪',
|
| 24 |
+
description: '提供 Docker 和 Hugging Face 部署配置,支持快速上线。'
|
| 25 |
+
}
|
| 26 |
+
];
|
| 27 |
+
|
| 28 |
+
const techStack = [
|
| 29 |
+
{ name: 'React', version: '18.x', type: '前端框架' },
|
| 30 |
+
{ name: 'Koa', version: '2.x', type: '后端框架' },
|
| 31 |
+
{ name: 'TypeScript', version: '5.x', type: '开发语言' },
|
| 32 |
+
{ name: 'Tailwind CSS', version: '3.x', type: '样式框架' },
|
| 33 |
+
{ name: 'Vite', version: '5.x', type: '构建工具' },
|
| 34 |
+
{ name: 'Lucide React', version: '最新', type: '图标库' }
|
| 35 |
+
];
|
| 36 |
+
|
| 37 |
+
return (
|
| 38 |
+
<div className="space-y-12 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
| 39 |
+
<section className="text-center">
|
| 40 |
+
<h1 className="text-3xl font-bold text-blue-900 mb-4">关于项目</h1>
|
| 41 |
+
<p className="text-gray-600 max-w-3xl mx-auto">
|
| 42 |
+
Koa Web 应用是一个示例性质的完整 Web 项目,旨在展示如何结合现代前端技术与稳健的后端框架
|
| 43 |
+
来构建可扩展、易维护的应用程序。
|
| 44 |
+
</p>
|
| 45 |
+
</section>
|
| 46 |
+
|
| 47 |
+
<section>
|
| 48 |
+
<h2 className="text-2xl font-bold text-blue-900 mb-8 text-center">核心特性</h2>
|
| 49 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
| 50 |
+
{features.map((feature, index) => (
|
| 51 |
+
<div key={index} className="flex space-x-4 bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
|
| 52 |
+
<div className="flex-shrink-0">{feature.icon}</div>
|
| 53 |
+
<div>
|
| 54 |
+
<h3 className="text-lg font-bold text-gray-900 mb-1">{feature.title}</h3>
|
| 55 |
+
<p className="text-gray-600">{feature.description}</p>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
))}
|
| 59 |
+
</div>
|
| 60 |
+
</section>
|
| 61 |
+
|
| 62 |
+
<section>
|
| 63 |
+
<h2 className="text-2xl font-bold text-blue-900 mb-8 text-center">技术栈说明</h2>
|
| 64 |
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
| 65 |
+
{techStack.map((tech, index) => (
|
| 66 |
+
<div key={index} className="bg-blue-50 p-4 rounded-xl border border-blue-100 text-center">
|
| 67 |
+
<div className="text-blue-900 font-bold mb-1">{tech.name}</div>
|
| 68 |
+
<div className="text-blue-700 text-xs mb-1">{tech.version}</div>
|
| 69 |
+
<div className="text-gray-500 text-[10px] uppercase tracking-wider">{tech.type}</div>
|
| 70 |
+
</div>
|
| 71 |
+
))}
|
| 72 |
+
</div>
|
| 73 |
+
</section>
|
| 74 |
+
|
| 75 |
+
<section className="bg-gray-900 text-white p-12 rounded-3xl text-center">
|
| 76 |
+
<Package className="h-12 w-12 text-blue-400 mx-auto mb-6" />
|
| 77 |
+
<h2 className="text-2xl font-bold mb-4">想要贡献代码?</h2>
|
| 78 |
+
<p className="text-gray-400 mb-8 max-w-2xl mx-auto">
|
| 79 |
+
本项目是开源的,欢迎通过提交 Pull Request 或 Issue 来帮助我们改进。
|
| 80 |
+
我们重视每一位开发者的建议。
|
| 81 |
+
</p>
|
| 82 |
+
<button className="bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-full font-bold transition-colors">
|
| 83 |
+
访问 GitHub 仓库
|
| 84 |
+
</button>
|
| 85 |
+
</section>
|
| 86 |
+
</div>
|
| 87 |
+
);
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
export default About;
|
src/pages/ApiDocs.tsx
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { Copy, Terminal, ExternalLink } from 'lucide-react';
|
| 3 |
+
|
| 4 |
+
const ApiDocs: React.FC = () => {
|
| 5 |
+
const apis = [
|
| 6 |
+
{
|
| 7 |
+
method: 'GET',
|
| 8 |
+
path: '/api/health',
|
| 9 |
+
description: '健康检查接口,用于监控服务状态',
|
| 10 |
+
example: '{ "status": "ok", "timestamp": 1710123456789 }'
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
method: 'GET',
|
| 14 |
+
path: '/api/info',
|
| 15 |
+
description: '系统信息接口,返回运行平台、Node 版本和内存使用情况',
|
| 16 |
+
example: '{ "nodeVersion": "v18.17.0", "platform": "linux", "memoryUsage": { "used": 45.2, "total": 128, "unit": "MB" } }'
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
method: 'GET',
|
| 20 |
+
path: '/api/time',
|
| 21 |
+
description: '服务器时间接口,返回当前服务器的本地时间和 UTC 时间',
|
| 22 |
+
example: '{ "serverTime": "2024-03-11 10:00:00", "timezone": "Asia/Shanghai", "utc": "2024-03-11T02:00:00.000Z" }'
|
| 23 |
+
}
|
| 24 |
+
];
|
| 25 |
+
|
| 26 |
+
return (
|
| 27 |
+
<div className="max-w-4xl mx-auto space-y-12 animate-in fade-in duration-500">
|
| 28 |
+
<section>
|
| 29 |
+
<h1 className="text-3xl font-bold text-blue-900 mb-4">API 文档</h1>
|
| 30 |
+
<p className="text-gray-600">
|
| 31 |
+
后端服务提供以下 RESTful API 接口。所有接口均返回 JSON 格式数据。
|
| 32 |
+
默认 API 前缀为 <code className="bg-gray-100 px-1 rounded">/api</code>。
|
| 33 |
+
</p>
|
| 34 |
+
</section>
|
| 35 |
+
|
| 36 |
+
<section className="space-y-8">
|
| 37 |
+
{apis.map((api, index) => (
|
| 38 |
+
<div key={index} className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
|
| 39 |
+
<div className="p-6">
|
| 40 |
+
<div className="flex items-center justify-between mb-4">
|
| 41 |
+
<div className="flex items-center space-x-3">
|
| 42 |
+
<span className={`px-3 py-1 rounded-full text-xs font-bold ${
|
| 43 |
+
api.method === 'GET' ? 'bg-green-100 text-green-700' : 'bg-blue-100 text-blue-700'
|
| 44 |
+
}`}>
|
| 45 |
+
{api.method}
|
| 46 |
+
</span>
|
| 47 |
+
<code className="text-lg font-mono font-bold text-gray-900">{api.path}</code>
|
| 48 |
+
</div>
|
| 49 |
+
<button
|
| 50 |
+
onClick={() => navigator.clipboard.writeText(api.path)}
|
| 51 |
+
className="text-gray-400 hover:text-blue-600 transition-colors"
|
| 52 |
+
title="复制代码"
|
| 53 |
+
>
|
| 54 |
+
<Copy className="h-4 w-4" />
|
| 55 |
+
</button>
|
| 56 |
+
</div>
|
| 57 |
+
<p className="text-gray-600 mb-6">{api.description}</p>
|
| 58 |
+
|
| 59 |
+
<div className="space-y-3">
|
| 60 |
+
<div className="flex items-center space-x-2 text-sm font-medium text-gray-500">
|
| 61 |
+
<Terminal className="h-4 w-4" />
|
| 62 |
+
<span>响应示例</span>
|
| 63 |
+
</div>
|
| 64 |
+
<pre className="bg-gray-900 text-blue-400 p-4 rounded-xl overflow-x-auto font-mono text-sm leading-relaxed">
|
| 65 |
+
{JSON.stringify(JSON.parse(api.example), null, 2)}
|
| 66 |
+
</pre>
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
))}
|
| 71 |
+
</section>
|
| 72 |
+
|
| 73 |
+
<section className="bg-blue-900 text-white p-8 rounded-3xl">
|
| 74 |
+
<h2 className="text-xl font-bold mb-4 flex items-center">
|
| 75 |
+
<ExternalLink className="h-5 w-5 mr-2" />
|
| 76 |
+
错误处理
|
| 77 |
+
</h2>
|
| 78 |
+
<p className="text-blue-200 mb-4">
|
| 79 |
+
当发生错误时,API 会返回 4xx 或 5xx 状态码,并包含以下格式的错误信息:
|
| 80 |
+
</p>
|
| 81 |
+
<pre className="bg-blue-950 p-4 rounded-xl text-blue-300 font-mono text-sm">
|
| 82 |
+
{`{
|
| 83 |
+
"success": false,
|
| 84 |
+
"error": "错误描述信息"
|
| 85 |
+
}`}
|
| 86 |
+
</pre>
|
| 87 |
+
</section>
|
| 88 |
+
</div>
|
| 89 |
+
);
|
| 90 |
+
};
|
| 91 |
+
|
| 92 |
+
export default ApiDocs;
|
src/pages/Home.tsx
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useState } from 'react';
|
| 2 |
+
import { Cpu, Server, Clock, CheckCircle } from 'lucide-react';
|
| 3 |
+
|
| 4 |
+
interface SystemInfo {
|
| 5 |
+
nodeVersion: string;
|
| 6 |
+
platform: string;
|
| 7 |
+
memoryUsage: {
|
| 8 |
+
used: number;
|
| 9 |
+
total: number;
|
| 10 |
+
unit: string;
|
| 11 |
+
};
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
interface ServerTime {
|
| 15 |
+
serverTime: string;
|
| 16 |
+
timezone: string;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const Home: React.FC = () => {
|
| 20 |
+
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
|
| 21 |
+
const [serverTime, setServerTime] = useState<ServerTime | null>(null);
|
| 22 |
+
const [loading, setLoading] = useState(true);
|
| 23 |
+
|
| 24 |
+
const fetchData = async () => {
|
| 25 |
+
try {
|
| 26 |
+
const [infoRes, timeRes] = await Promise.all([
|
| 27 |
+
fetch('/api/info'),
|
| 28 |
+
fetch('/api/time')
|
| 29 |
+
]);
|
| 30 |
+
const infoData = await infoRes.json();
|
| 31 |
+
const timeData = await timeRes.json();
|
| 32 |
+
setSystemInfo(infoData);
|
| 33 |
+
setServerTime(timeData);
|
| 34 |
+
} catch (error) {
|
| 35 |
+
console.error('获取数据失败:', error);
|
| 36 |
+
} finally {
|
| 37 |
+
setLoading(false);
|
| 38 |
+
}
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
+
useEffect(() => {
|
| 42 |
+
fetchData();
|
| 43 |
+
const interval = setInterval(fetchData, 5000);
|
| 44 |
+
return () => clearInterval(interval);
|
| 45 |
+
}, []);
|
| 46 |
+
|
| 47 |
+
return (
|
| 48 |
+
<div className="space-y-8 animate-in fade-in duration-500">
|
| 49 |
+
<section className="text-center py-12 bg-white rounded-2xl shadow-sm border border-gray-100">
|
| 50 |
+
<h1 className="text-4xl font-extrabold text-blue-900 mb-4">
|
| 51 |
+
欢迎使用 Koa Web 应用
|
| 52 |
+
</h1>
|
| 53 |
+
<p className="text-lg text-gray-600 max-w-2xl mx-auto px-4">
|
| 54 |
+
这是一个基于 Koa 和 React 构建的高性能 Web 服务框架。
|
| 55 |
+
项目已完全汉化,支持移动端适配,并具备容器化部署能力。
|
| 56 |
+
</p>
|
| 57 |
+
</section>
|
| 58 |
+
|
| 59 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
| 60 |
+
{/* 系统状态卡片 */}
|
| 61 |
+
<StatusCard
|
| 62 |
+
icon={<CheckCircle className="h-6 w-6 text-green-500" />}
|
| 63 |
+
title="运行状态"
|
| 64 |
+
value="正常"
|
| 65 |
+
description="系统服务运行中"
|
| 66 |
+
/>
|
| 67 |
+
<StatusCard
|
| 68 |
+
icon={<Cpu className="h-6 w-6 text-blue-500" />}
|
| 69 |
+
title="内存使用"
|
| 70 |
+
value={loading ? '加载中...' : `${systemInfo?.memoryUsage.used} / ${systemInfo?.memoryUsage.total} ${systemInfo?.memoryUsage.unit}`}
|
| 71 |
+
description="当前堆内存占用"
|
| 72 |
+
/>
|
| 73 |
+
<StatusCard
|
| 74 |
+
icon={<Server className="h-6 w-6 text-purple-500" />}
|
| 75 |
+
title="运行平台"
|
| 76 |
+
value={loading ? '加载中...' : systemInfo?.platform || '未知'}
|
| 77 |
+
description={`Node.js ${systemInfo?.nodeVersion || ''}`}
|
| 78 |
+
/>
|
| 79 |
+
<StatusCard
|
| 80 |
+
icon={<Clock className="h-6 w-6 text-orange-500" />}
|
| 81 |
+
title="服务器时间"
|
| 82 |
+
value={loading ? '加载中...' : serverTime?.serverTime.split(' ')[1] || ''}
|
| 83 |
+
description={serverTime?.serverTime.split(' ')[0] || ''}
|
| 84 |
+
/>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<section className="bg-blue-50 p-8 rounded-2xl border border-blue-100">
|
| 88 |
+
<h2 className="text-2xl font-bold text-blue-900 mb-4">快速开始</h2>
|
| 89 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
| 90 |
+
<div className="flex flex-col space-y-2">
|
| 91 |
+
<span className="font-semibold text-blue-800">1. 安装依赖</span>
|
| 92 |
+
<code className="bg-blue-100 p-2 rounded text-sm text-blue-900">npm install</code>
|
| 93 |
+
</div>
|
| 94 |
+
<div className="flex flex-col space-y-2">
|
| 95 |
+
<span className="font-semibold text-blue-800">2. 启动开发环境</span>
|
| 96 |
+
<code className="bg-blue-100 p-2 rounded text-sm text-blue-900">npm run dev</code>
|
| 97 |
+
</div>
|
| 98 |
+
<div className="flex flex-col space-y-2">
|
| 99 |
+
<span className="font-semibold text-blue-800">3. 生产部署</span>
|
| 100 |
+
<code className="bg-blue-100 p-2 rounded text-sm text-blue-900">docker-compose up -d</code>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
</section>
|
| 104 |
+
</div>
|
| 105 |
+
);
|
| 106 |
+
};
|
| 107 |
+
|
| 108 |
+
interface StatusCardProps {
|
| 109 |
+
icon: React.ReactNode;
|
| 110 |
+
title: string;
|
| 111 |
+
value: string;
|
| 112 |
+
description: string;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
const StatusCard: React.FC<StatusCardProps> = ({ icon, title, value, description }) => (
|
| 116 |
+
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-100 hover:shadow-md transition-shadow">
|
| 117 |
+
<div className="flex items-center space-x-3 mb-4">
|
| 118 |
+
{icon}
|
| 119 |
+
<h3 className="text-sm font-medium text-gray-500 uppercase tracking-wider">{title}</h3>
|
| 120 |
+
</div>
|
| 121 |
+
<div className="text-2xl font-bold text-gray-900 mb-1">{value}</div>
|
| 122 |
+
<div className="text-xs text-gray-500">{description}</div>
|
| 123 |
+
</div>
|
| 124 |
+
);
|
| 125 |
+
|
| 126 |
+
export default Home;
|
src/pages/Tasks.tsx
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useState } from 'react';
|
| 2 |
+
import { CheckCircle2, Circle, ListTodo, Activity } from 'lucide-react';
|
| 3 |
+
|
| 4 |
+
interface Task {
|
| 5 |
+
id: number;
|
| 6 |
+
title: string;
|
| 7 |
+
completed: boolean;
|
| 8 |
+
category: string;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
interface Stats {
|
| 12 |
+
activeUsers: number;
|
| 13 |
+
totalRequests: number;
|
| 14 |
+
uptime: number;
|
| 15 |
+
version: string;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
const Tasks: React.FC = () => {
|
| 19 |
+
const [tasks, setTasks] = useState<Task[]>([]);
|
| 20 |
+
const [stats, setStats] = useState<Stats | null>(null);
|
| 21 |
+
const [loading, setLoading] = useState(true);
|
| 22 |
+
const [error, setError] = useState<string | null>(null);
|
| 23 |
+
|
| 24 |
+
useEffect(() => {
|
| 25 |
+
const fetchData = async () => {
|
| 26 |
+
try {
|
| 27 |
+
const [tasksRes, statsRes] = await Promise.all([
|
| 28 |
+
fetch('/api/data/tasks'),
|
| 29 |
+
fetch('/api/data/stats')
|
| 30 |
+
]);
|
| 31 |
+
|
| 32 |
+
if (!tasksRes.ok || !statsRes.ok) {
|
| 33 |
+
throw new Error('获取数据失败');
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
const tasksData = await tasksRes.json();
|
| 37 |
+
const statsData = await statsRes.json();
|
| 38 |
+
|
| 39 |
+
if (tasksData.success) setTasks(tasksData.data);
|
| 40 |
+
if (statsData.success) setStats(statsData.data);
|
| 41 |
+
} catch (err) {
|
| 42 |
+
setError(err instanceof Error ? err.message : '未知错误');
|
| 43 |
+
} finally {
|
| 44 |
+
setLoading(false);
|
| 45 |
+
}
|
| 46 |
+
};
|
| 47 |
+
|
| 48 |
+
fetchData();
|
| 49 |
+
}, []);
|
| 50 |
+
|
| 51 |
+
if (loading) {
|
| 52 |
+
return (
|
| 53 |
+
<div className="flex items-center justify-center min-h-[400px]">
|
| 54 |
+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
| 55 |
+
</div>
|
| 56 |
+
);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
if (error) {
|
| 60 |
+
return (
|
| 61 |
+
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg text-center my-8">
|
| 62 |
+
<p>出错了: {error}</p>
|
| 63 |
+
</div>
|
| 64 |
+
);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
return (
|
| 68 |
+
<div className="max-w-4xl mx-auto space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
| 69 |
+
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
| 70 |
+
<div>
|
| 71 |
+
<h1 className="text-3xl font-bold text-gray-900 flex items-center gap-2">
|
| 72 |
+
<ListTodo className="h-8 w-8 text-blue-600" />
|
| 73 |
+
任务管理
|
| 74 |
+
</h1>
|
| 75 |
+
<p className="mt-2 text-gray-600">展示从后端接口获取的实时数据示例</p>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
{/* 统计卡片 */}
|
| 80 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
| 81 |
+
<div className="bg-white p-6 rounded-xl border border-gray-200 shadow-sm">
|
| 82 |
+
<div className="flex items-center gap-3 text-blue-600 mb-2">
|
| 83 |
+
<Activity className="h-5 w-5" />
|
| 84 |
+
<span className="text-sm font-medium">活跃用户</span>
|
| 85 |
+
</div>
|
| 86 |
+
<p className="text-2xl font-bold text-gray-900">{stats?.activeUsers}</p>
|
| 87 |
+
</div>
|
| 88 |
+
<div className="bg-white p-6 rounded-xl border border-gray-200 shadow-sm">
|
| 89 |
+
<div className="flex items-center gap-3 text-green-600 mb-2">
|
| 90 |
+
<CheckCircle2 className="h-5 w-5" />
|
| 91 |
+
<span className="text-sm font-medium">总请求数</span>
|
| 92 |
+
</div>
|
| 93 |
+
<p className="text-2xl font-bold text-gray-900">{stats?.totalRequests}</p>
|
| 94 |
+
</div>
|
| 95 |
+
<div className="bg-white p-6 rounded-xl border border-gray-200 shadow-sm">
|
| 96 |
+
<div className="flex items-center gap-3 text-purple-600 mb-2">
|
| 97 |
+
<Activity className="h-5 w-5" />
|
| 98 |
+
<span className="text-sm font-medium">运行时长</span>
|
| 99 |
+
</div>
|
| 100 |
+
<p className="text-2xl font-bold text-gray-900">{Math.floor(stats?.uptime || 0)}s</p>
|
| 101 |
+
</div>
|
| 102 |
+
<div className="bg-white p-6 rounded-xl border border-gray-200 shadow-sm">
|
| 103 |
+
<div className="flex items-center gap-3 text-orange-600 mb-2">
|
| 104 |
+
<CheckCircle2 className="h-5 w-5" />
|
| 105 |
+
<span className="text-sm font-medium">版本</span>
|
| 106 |
+
</div>
|
| 107 |
+
<p className="text-2xl font-bold text-gray-900">{stats?.version}</p>
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
+
{/* 任务列表 */}
|
| 112 |
+
<div className="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden">
|
| 113 |
+
<div className="px-6 py-4 border-b border-gray-200 bg-gray-50/50">
|
| 114 |
+
<h2 className="font-semibold text-gray-900">项目进度</h2>
|
| 115 |
+
</div>
|
| 116 |
+
<div className="divide-y divide-gray-100">
|
| 117 |
+
{tasks.map((task) => (
|
| 118 |
+
<div key={task.id} className="px-6 py-4 flex items-center justify-between hover:bg-gray-50 transition-colors">
|
| 119 |
+
<div className="flex items-center gap-3">
|
| 120 |
+
{task.completed ? (
|
| 121 |
+
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
| 122 |
+
) : (
|
| 123 |
+
<Circle className="h-5 w-5 text-gray-300" />
|
| 124 |
+
)}
|
| 125 |
+
<div>
|
| 126 |
+
<p className={`font-medium ${task.completed ? 'text-gray-400 line-through' : 'text-gray-900'}`}>
|
| 127 |
+
{task.title}
|
| 128 |
+
</p>
|
| 129 |
+
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-0.5 rounded-full">{task.category}</span>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
<span className={`text-sm px-2.5 py-1 rounded-full ${
|
| 133 |
+
task.completed ? 'bg-green-100 text-green-700' : 'bg-blue-100 text-blue-700'
|
| 134 |
+
}`}>
|
| 135 |
+
{task.completed ? '已完成' : '进行中'}
|
| 136 |
+
</span>
|
| 137 |
+
</div>
|
| 138 |
+
))}
|
| 139 |
+
</div>
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
+
);
|
| 143 |
+
};
|
| 144 |
+
|
| 145 |
+
export default Tasks;
|
src/vite-env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="vite/client" />
|
tailwind.config.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
|
| 3 |
+
export default {
|
| 4 |
+
darkMode: "class",
|
| 5 |
+
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
| 6 |
+
theme: {
|
| 7 |
+
container: {
|
| 8 |
+
center: true,
|
| 9 |
+
},
|
| 10 |
+
extend: {},
|
| 11 |
+
},
|
| 12 |
+
plugins: [],
|
| 13 |
+
};
|
tsconfig.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
| 4 |
+
"target": "ES2020",
|
| 5 |
+
"useDefineForClassFields": true,
|
| 6 |
+
"lib": [
|
| 7 |
+
"ES2020",
|
| 8 |
+
"DOM",
|
| 9 |
+
"DOM.Iterable"
|
| 10 |
+
],
|
| 11 |
+
"module": "ESNext",
|
| 12 |
+
"skipLibCheck": true,
|
| 13 |
+
"moduleResolution": "bundler",
|
| 14 |
+
"allowImportingTsExtensions": true,
|
| 15 |
+
"verbatimModuleSyntax": false,
|
| 16 |
+
"moduleDetection": "force",
|
| 17 |
+
"noEmit": true,
|
| 18 |
+
"jsx": "react-jsx",
|
| 19 |
+
"strict": false,
|
| 20 |
+
"noUnusedLocals": false,
|
| 21 |
+
"noUnusedParameters": false,
|
| 22 |
+
"noFallthroughCasesInSwitch": false,
|
| 23 |
+
"noUncheckedSideEffectImports": false,
|
| 24 |
+
"forceConsistentCasingInFileNames": false,
|
| 25 |
+
"baseUrl": "./",
|
| 26 |
+
"paths": {
|
| 27 |
+
"@/*": [
|
| 28 |
+
"./src/*"
|
| 29 |
+
]
|
| 30 |
+
},
|
| 31 |
+
"types": [
|
| 32 |
+
"node",
|
| 33 |
+
"express"
|
| 34 |
+
]
|
| 35 |
+
},
|
| 36 |
+
"include": [
|
| 37 |
+
"src",
|
| 38 |
+
"api"
|
| 39 |
+
]
|
| 40 |
+
}
|
vercel.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"rewrites": [
|
| 3 |
+
{
|
| 4 |
+
"source": "/api/(.*)",
|
| 5 |
+
"destination": "/api/index"
|
| 6 |
+
},
|
| 7 |
+
{
|
| 8 |
+
"source": "/(.*)",
|
| 9 |
+
"destination": "/index.html"
|
| 10 |
+
}
|
| 11 |
+
]
|
| 12 |
+
}
|
vite.config.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
import tsconfigPaths from "vite-tsconfig-paths";
|
| 4 |
+
import { traeBadgePlugin } from 'vite-plugin-trae-solo-badge';
|
| 5 |
+
|
| 6 |
+
// https://vite.dev/config/
|
| 7 |
+
export default defineConfig({
|
| 8 |
+
plugins: [
|
| 9 |
+
react({
|
| 10 |
+
babel: {
|
| 11 |
+
plugins: [
|
| 12 |
+
'react-dev-locator',
|
| 13 |
+
],
|
| 14 |
+
},
|
| 15 |
+
}),
|
| 16 |
+
traeBadgePlugin({
|
| 17 |
+
variant: 'dark',
|
| 18 |
+
position: 'bottom-right',
|
| 19 |
+
prodOnly: true,
|
| 20 |
+
clickable: true,
|
| 21 |
+
clickUrl: 'https://www.trae.ai/solo?showJoin=1',
|
| 22 |
+
autoTheme: true,
|
| 23 |
+
autoThemeTarget: '#root'
|
| 24 |
+
}),
|
| 25 |
+
tsconfigPaths(),
|
| 26 |
+
],
|
| 27 |
+
server: {
|
| 28 |
+
proxy: {
|
| 29 |
+
'/api': {
|
| 30 |
+
target: 'http://localhost:3001',
|
| 31 |
+
changeOrigin: true,
|
| 32 |
+
secure: false,
|
| 33 |
+
configure: (proxy, _options) => {
|
| 34 |
+
proxy.on('error', (err, _req, _res) => {
|
| 35 |
+
console.log('proxy error', err);
|
| 36 |
+
});
|
| 37 |
+
proxy.on('proxyReq', (proxyReq, req, _res) => {
|
| 38 |
+
console.log('Sending Request to the Target:', req.method, req.url);
|
| 39 |
+
});
|
| 40 |
+
proxy.on('proxyRes', (proxyRes, req, _res) => {
|
| 41 |
+
console.log('Received Response from the Target:', proxyRes.statusCode, req.url);
|
| 42 |
+
});
|
| 43 |
+
},
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
})
|