diff --git a/apps/server/README-TC39-decorators.md b/apps/server/README-TC39-decorators.md index f3d2e73..28fcfcb 100644 --- a/apps/server/README-TC39-decorators.md +++ b/apps/server/README-TC39-decorators.md @@ -80,6 +80,7 @@ this.routeScanner.registerControllers([ ## TC39 装饰器特性 ### 1. 标准语法 + ```typescript // TC39 标准装饰器使用 addInitializer @Get('/users') @@ -89,6 +90,7 @@ async getUsers(ctx: Context) { ``` ### 2. 类型安全 + ```typescript // 完整的 TypeScript 类型检查 @Controller('/api') @@ -101,6 +103,7 @@ export class ApiController { ``` ### 3. 无外部依赖 + ```typescript // 不再需要 reflect-metadata // 使用内置的 WeakMap 存储元数据 @@ -136,6 +139,7 @@ export class ApiController { 最终的API路径 = 全局前缀 + 控制器前缀 + 方法路径 例如: + - 全局前缀:`/api` - 控制器前缀:`/user` - 方法路径:`/list` @@ -176,56 +180,11 @@ async getUser(ctx: Context) { ## 现有路由 -项目中已注册的路由: - -### ApplicationController -- `GET /api/application/list` - 获取应用列表 -- `GET /api/application/detail/:id` - 获取应用详情 - ### UserController + - `GET /api/user/list` - 获取用户列表 - `GET /api/user/detail/:id` - 获取用户详情 - `POST /api/user` - 创建用户 - `PUT /api/user/:id` - 更新用户 - `DELETE /api/user/:id` - 删除用户 - `GET /api/user/search` - 搜索用户 - -## 与旧版本装饰器的区别 - -| 特性 | 实验性装饰器 | TC39 标准装饰器 | -|------|-------------|----------------| -| 标准化 | ❌ TypeScript 特有 | ✅ ECMAScript 标准 | -| 依赖 | ❌ 需要 reflect-metadata | ✅ 零依赖 | -| 性能 | ❌ 运行时反射 | ✅ 编译时优化 | -| 类型安全 | ⚠️ 部分支持 | ✅ 完整支持 | -| 未来兼容 | ❌ 可能被废弃 | ✅ 持续演进 | - -## 迁移指南 - -从实验性装饰器迁移到 TC39 标准装饰器: - -1. **更新 tsconfig.json** - ```json - { - "experimentalDecorators": false, - "emitDecoratorMetadata": false - } - ``` - -2. **移除依赖** - ```bash - pnpm remove reflect-metadata - ``` - -3. **代码无需修改** - - 装饰器语法保持不变 - - 控制器代码无需修改 - - 自动兼容新标准 - -## 注意事项 - -1. 需要 TypeScript 5.0+ 支持 -2. 需要 Node.js 16+ 运行环境 -3. 控制器类需要导出并在路由中间件中注册 -4. 控制器方法应该返回数据而不是直接操作 `ctx.body` -5. TC39 装饰器使用 `addInitializer` 进行初始化,性能更优 diff --git a/apps/server/controllers/deployment/dto.ts b/apps/server/controllers/deployment/dto.ts index aa80269..37f52d8 100644 --- a/apps/server/controllers/deployment/dto.ts +++ b/apps/server/controllers/deployment/dto.ts @@ -1,8 +1,8 @@ import { z } from 'zod'; export const listDeploymentsQuerySchema = z.object({ - page: z.coerce.number().int().min(1).optional().default(1), - pageSize: z.coerce.number().int().min(1).max(100).optional().default(10), + page: z.coerce.number().int().min(1).optional(), + pageSize: z.coerce.number().int().min(1).max(100).optional(), projectId: z.coerce.number().int().positive().optional(), }); diff --git a/apps/server/controllers/deployment/index.ts b/apps/server/controllers/deployment/index.ts index 699dc38..b42456d 100644 --- a/apps/server/controllers/deployment/index.ts +++ b/apps/server/controllers/deployment/index.ts @@ -20,22 +20,28 @@ export class DeploymentController { where.projectId = projectId; } + const isPagination = page !== undefined && pageSize !== undefined; + const result = await prisma.deployment.findMany({ where, - take: pageSize, - skip: (page - 1) * pageSize, + take: isPagination ? pageSize : undefined, + skip: isPagination ? (page! - 1) * pageSize! : 0, orderBy: { createdAt: 'desc', }, }); const total = await prisma.deployment.count({ where }); - return { - data: result, - page, - pageSize, - total, - }; + if (isPagination) { + return { + list: result, + page, + pageSize, + total, + }; + } + + return result; } @Post('') diff --git a/apps/server/controllers/project/dto.ts b/apps/server/controllers/project/dto.ts index 1d775ae..a49435f 100644 --- a/apps/server/controllers/project/dto.ts +++ b/apps/server/controllers/project/dto.ts @@ -66,19 +66,8 @@ export const updateProjectSchema = z.object({ */ export const listProjectQuerySchema = z .object({ - page: z.coerce - .number() - .int() - .min(1, { message: '页码必须大于0' }) - .optional() - .default(1), - limit: z.coerce - .number() - .int() - .min(1, { message: '每页数量必须大于0' }) - .max(100, { message: '每页数量不能超过100' }) - .optional() - .default(10), + page: z.coerce.number().int().min(1).optional(), + pageSize: z.coerce.number().int().min(1).max(100).optional(), name: z.string().optional(), }) .optional(); diff --git a/apps/server/controllers/project/index.ts b/apps/server/controllers/project/index.ts index 755159f..bb8b159 100644 --- a/apps/server/controllers/project/index.ts +++ b/apps/server/controllers/project/index.ts @@ -29,27 +29,30 @@ export class ProjectController { }; } + const isPagination = query?.page !== undefined && query?.pageSize !== undefined; + const [total, projects] = await Promise.all([ prisma.project.count({ where: whereCondition }), prisma.project.findMany({ where: whereCondition, - skip: query ? (query.page - 1) * query.limit : 0, - take: query?.limit, + skip: isPagination ? (query.page! - 1) * query.pageSize! : 0, + take: isPagination ? query.pageSize : undefined, orderBy: { createdAt: 'desc', }, }), ]); - return { - data: projects, - pagination: { - page: query?.page || 1, - limit: query?.limit || 10, + if (isPagination) { + return { + list: projects, + page: query.page, + pageSize: query.pageSize, total, - totalPages: Math.ceil(total / (query?.limit || 10)), - }, - }; + }; + } + + return projects; } // GET /api/projects/:id - 获取单个项目 diff --git a/apps/server/controllers/step/dto.ts b/apps/server/controllers/step/dto.ts index cc1d63e..5b576bc 100644 --- a/apps/server/controllers/step/dto.ts +++ b/apps/server/controllers/step/dto.ts @@ -84,15 +84,13 @@ export const listStepsQuerySchema = z .number() .int() .min(1, { message: '页码必须大于0' }) - .optional() - .default(1), - limit: z.coerce + .optional(), + pageSize: z.coerce .number() .int() .min(1, { message: '每页数量必须大于0' }) .max(100, { message: '每页数量不能超过100' }) - .optional() - .default(10), + .optional(), }) .optional(); diff --git a/apps/server/controllers/step/index.ts b/apps/server/controllers/step/index.ts index 4442650..cebdad4 100644 --- a/apps/server/controllers/step/index.ts +++ b/apps/server/controllers/step/index.ts @@ -26,27 +26,30 @@ export class StepController { whereCondition.pipelineId = query.pipelineId; } + const isPagination = query?.page !== undefined && query?.pageSize !== undefined; + const [total, steps] = await Promise.all([ prisma.step.count({ where: whereCondition }), prisma.step.findMany({ where: whereCondition, - skip: query ? (query.page - 1) * query.limit : 0, - take: query?.limit, + skip: isPagination ? (query.page! - 1) * query.pageSize! : 0, + take: isPagination ? query.pageSize : undefined, orderBy: { order: 'asc', }, }), ]); - return { - data: steps, - pagination: { - page: query?.page || 1, - limit: query?.limit || 10, + if (isPagination) { + return { + list: steps, + page: query.page, + pageSize: query.pageSize, total, - totalPages: Math.ceil(total / (query?.limit || 10)), - }, - }; + }; + } + + return steps; } // GET /api/steps/:id - 获取单个步骤 diff --git a/apps/server/controllers/user/index.ts b/apps/server/controllers/user/index.ts index a945870..e61a292 100644 --- a/apps/server/controllers/user/index.ts +++ b/apps/server/controllers/user/index.ts @@ -118,11 +118,6 @@ export class UserController { results = results.filter((user) => user.status === status); } - return { - keyword, - status, - total: results.length, - results, - }; + return results; } } diff --git a/apps/server/prisma/data/dev.db b/apps/server/prisma/data/dev.db index f443b4d..19b07a7 100644 Binary files a/apps/server/prisma/data/dev.db and b/apps/server/prisma/data/dev.db differ diff --git a/apps/web/src/hooks/useAsyncEffect.ts b/apps/web/src/hooks/useAsyncEffect.ts index 2037d79..ebbb845 100644 --- a/apps/web/src/hooks/useAsyncEffect.ts +++ b/apps/web/src/hooks/useAsyncEffect.ts @@ -2,7 +2,7 @@ import type React from 'react'; import { useCallback, useEffect } from 'react'; export function useAsyncEffect( - effect: () => Promise void)>, + effect: () => Promise void)>, deps: React.DependencyList, ) { const callback = useCallback(effect, [...deps]); diff --git a/apps/web/src/pages/App.tsx b/apps/web/src/pages/App.tsx index 77bf89f..33822b3 100644 --- a/apps/web/src/pages/App.tsx +++ b/apps/web/src/pages/App.tsx @@ -1,4 +1,4 @@ -import Env from '@pages/env'; + import Home from '@pages/home'; import Login from '@pages/login'; import ProjectDetail from '@pages/project/detail'; @@ -13,7 +13,7 @@ const App = () => { } /> } /> } /> - } /> + } /> diff --git a/apps/web/src/pages/env/index.tsx b/apps/web/src/pages/env/index.tsx deleted file mode 100644 index 2ce4447..0000000 --- a/apps/web/src/pages/env/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -function Env() { - return
env page
; -} - -export default Env; diff --git a/apps/web/src/pages/home/index.tsx b/apps/web/src/pages/home/index.tsx index 8549d88..194d72b 100644 --- a/apps/web/src/pages/home/index.tsx +++ b/apps/web/src/pages/home/index.tsx @@ -4,7 +4,7 @@ import { IconExport, IconMenuFold, IconMenuUnfold, - IconRobot, + } from '@arco-design/web-react/icon'; import Logo from '@assets/images/logo.svg?react'; import { loginService } from '@pages/login/service'; @@ -45,12 +45,7 @@ function Home() { 项目管理 - - - - 环境管理 - - + diff --git a/apps/web/src/pages/login/service.ts b/apps/web/src/pages/login/service.ts index 5b7b6f1..6426afb 100644 --- a/apps/web/src/pages/login/service.ts +++ b/apps/web/src/pages/login/service.ts @@ -2,11 +2,11 @@ import { Message, Notification } from '@arco-design/web-react'; import { net } from '../../utils'; import type { NavigateFunction } from 'react-router'; import { useGlobalStore } from '../../stores/global'; -import type { AuthLoginResponse, AuthURLResponse } from './types'; +import type { AuthURL, User } from './types'; class LoginService { async getAuthUrl() { - const { code, data } = await net.request({ + const { code, data } = await net.request({ method: 'GET', url: '/api/auth/url', params: { @@ -19,7 +19,7 @@ class LoginService { } async login(authCode: string, navigate: NavigateFunction) { - const { data, code } = await net.request({ + const { data, code } = await net.request({ method: 'POST', url: '/api/auth/login', data: { @@ -37,7 +37,7 @@ class LoginService { } async logout() { - const { code } = await net.request({ + const { code } = await net.request({ method: 'GET', url: '/api/auth/logout', }); diff --git a/apps/web/src/pages/login/types.ts b/apps/web/src/pages/login/types.ts index 238bb9d..b3bae49 100644 --- a/apps/web/src/pages/login/types.ts +++ b/apps/web/src/pages/login/types.ts @@ -1,5 +1,3 @@ -import type { APIResponse } from '../../utils'; - export interface User { id: string; username: string; @@ -8,8 +6,6 @@ export interface User { active: boolean; } -export type AuthURLResponse = APIResponse<{ +export interface AuthURL { url: string; -}>; - -export type AuthLoginResponse = APIResponse; +}; diff --git a/apps/web/src/pages/project/detail/index.tsx b/apps/web/src/pages/project/detail/index.tsx index d2d2dc6..78c0a57 100644 --- a/apps/web/src/pages/project/detail/index.tsx +++ b/apps/web/src/pages/project/detail/index.tsx @@ -10,6 +10,7 @@ import { Menu, Message, Modal, + Pagination, Select, Space, Switch, @@ -94,6 +95,11 @@ function ProjectDetailPage() { const [pipelineForm] = Form.useForm(); const [deployRecords, setDeployRecords] = useState([]); const [deployModalVisible, setDeployModalVisible] = useState(false); + const [pagination, setPagination] = useState({ + current: 1, + pageSize: 10, + total: 0, + }); // 流水线模板相关状态 const [isCreatingFromTemplate, setIsCreatingFromTemplate] = useState(false); @@ -153,10 +159,15 @@ function ProjectDetailPage() { // 获取部署记录 try { - const records = await detailService.getDeployments(Number(id)); - setDeployRecords(records); - if (records.length > 0) { - setSelectedRecordId(records[0].id); + const res = await detailService.getDeployments( + Number(id), + 1, + pagination.pageSize, + ); + setDeployRecords(res.list); + setPagination((prev) => ({ ...prev, total: res.total, current: 1 })); + if (res.list.length > 0) { + setSelectedRecordId(res.list[0].id); } } catch (error) { console.error('获取部署记录失败:', error); @@ -175,32 +186,40 @@ function ProjectDetailPage() { }; // 定期轮询部署记录以更新状态和日志 - useAsyncEffect(async () => { - const interval = setInterval(async () => { - if (id) { - try { - const records = await detailService.getDeployments(Number(id)); - setDeployRecords(records); + useEffect(() => { + if (!id) return; + + const poll = async () => { + try { + const res = await detailService.getDeployments( + Number(id), + pagination.current, + pagination.pageSize, + ); + setDeployRecords(res.list); + setPagination((prev) => ({ ...prev, total: res.total })); - // 如果当前选中的记录正在运行,则更新选中记录 - const selectedRecord = records.find( - (r: Deployment) => r.id === selectedRecordId, - ); - if ( - selectedRecord && - (selectedRecord.status === 'running' || - selectedRecord.status === 'pending') - ) { - // 保持当前选中状态,但更新数据 - } - } catch (error) { - console.error('轮询部署记录失败:', error); + // 如果当前选中的记录正在运行,则更新选中记录 + const selectedRecord = res.list.find( + (r: Deployment) => r.id === selectedRecordId, + ); + if ( + selectedRecord && + (selectedRecord.status === 'running' || + selectedRecord.status === 'pending') + ) { + // 保持当前选中状态,但更新数据 } + } catch (error) { + console.error('轮询部署记录失败:', error); } - }, 3000); // 每3秒轮询一次 + }; + + poll(); // 立即执行一次 + const interval = setInterval(poll, 3000); // 每3秒轮询一次 return () => clearInterval(interval); - }, [id, selectedRecordId]); + }, [id, selectedRecordId, pagination.current, pagination.pageSize]); // 触发部署 const handleDeploy = () => { @@ -601,8 +620,13 @@ function ProjectDetailPage() { // 刷新部署记录 if (id) { - const records = await detailService.getDeployments(Number(id)); - setDeployRecords(records); + const res = await detailService.getDeployments( + Number(id), + pagination.current, + pagination.pageSize, + ); + setDeployRecords(res.list); + setPagination((prev) => ({ ...prev, total: res.total })); } } catch (error) { console.error('重新执行部署失败:', error); @@ -841,19 +865,33 @@ function ProjectDetailPage() { 刷新 -
- {deployRecords.length > 0 ? ( - +
+ {deployRecords.length > 0 ? ( + + ) : ( +
+ +
+ )} +
+
+ + setPagination((prev) => ({ ...prev, current: page })) + } /> - ) : ( -
- -
- )} +
@@ -1383,14 +1421,7 @@ function ProjectDetailPage() { style={{ fontFamily: 'Monaco, Consolas, monospace' }} /> -
- - 可用环境变量: -
• $PROJECT_NAME - 项目名称 -
• $BUILD_NUMBER - 构建编号 -
• $REGISTRY - 镜像仓库地址 -
-
+ @@ -1401,12 +1432,19 @@ function ProjectDetailPage() { setDeployModalVisible(false); // 刷新部署记录 if (id) { - detailService.getDeployments(Number(id)).then((records) => { - setDeployRecords(records); - if (records.length > 0) { - setSelectedRecordId(records[0].id); - } - }); + detailService + .getDeployments(Number(id), 1, pagination.pageSize) + .then((res) => { + setDeployRecords(res.list); + setPagination((prev) => ({ + ...prev, + total: res.total, + current: 1, + })); + if (res.list.length > 0) { + setSelectedRecordId(res.list[0].id); + } + }); } }} pipelines={pipelines} diff --git a/apps/web/src/pages/project/detail/service.ts b/apps/web/src/pages/project/detail/service.ts index 36691eb..d516797 100644 --- a/apps/web/src/pages/project/detail/service.ts +++ b/apps/web/src/pages/project/detail/service.ts @@ -1,4 +1,4 @@ -import { type APIResponse, net } from '../../../utils'; +import { net } from '../../../utils'; import type { Branch, Commit, @@ -11,7 +11,7 @@ import type { class DetailService { async getProject(id: string) { - const { data } = await net.request>({ + const { data } = await net.request({ url: `/api/projects/${id}`, }); return data; @@ -19,28 +19,32 @@ class DetailService { // 获取项目的所有流水线 async getPipelines(projectId: number) { - const { data } = await net.request>({ + const { data } = await net.request({ url: `/api/pipelines?projectId=${projectId}`, }); - return data; + return Array.isArray(data) ? data : data.list; } // 获取可用的流水线模板 async getPipelineTemplates() { const { data } = await net.request< - APIResponse<{ id: number; name: string; description: string }[]> + | { id: number; name: string; description: string }[] + | { list: { id: number; name: string; description: string }[] } >({ url: '/api/pipelines/templates', }); - return data; + return Array.isArray(data) ? data : data.list; } - // 获取项目的部署记录 - async getDeployments(projectId: number) { - const { data } = await net.request({ - url: `/api/deployments?projectId=${projectId}`, + async getDeployments( + projectId: number, + page: number = 1, + pageSize: number = 10, + ) { + const { data } = await net.request({ + url: `/api/deployments?projectId=${projectId}&page=${page}&pageSize=${pageSize}`, }); - return data.data; + return data; } // 创建流水线 @@ -56,7 +60,7 @@ class DetailService { | 'steps' >, ) { - const { data } = await net.request>({ + const { data } = await net.request({ url: '/api/pipelines', method: 'POST', data: pipeline, @@ -71,7 +75,7 @@ class DetailService { name: string, description?: string, ) { - const { data } = await net.request>({ + const { data } = await net.request({ url: '/api/pipelines/from-template', method: 'POST', data: { @@ -100,7 +104,7 @@ class DetailService { > >, ) { - const { data } = await net.request>({ + const { data } = await net.request({ url: `/api/pipelines/${id}`, method: 'PUT', data: pipeline, @@ -110,7 +114,7 @@ class DetailService { // 删除流水线 async deletePipeline(id: number) { - const { data } = await net.request>({ + const { data } = await net.request({ url: `/api/pipelines/${id}`, method: 'DELETE', }); @@ -119,10 +123,10 @@ class DetailService { // 获取流水线的所有步骤 async getSteps(pipelineId: number) { - const { data } = await net.request>({ + const { data } = await net.request({ url: `/api/steps?pipelineId=${pipelineId}`, }); - return data; + return Array.isArray(data) ? data : data.list; } // 创建步骤 @@ -132,7 +136,7 @@ class DetailService { 'id' | 'createdAt' | 'updatedAt' | 'createdBy' | 'updatedBy' | 'valid' >, ) { - const { data } = await net.request>({ + const { data } = await net.request({ url: '/api/steps', method: 'POST', data: step, @@ -150,7 +154,7 @@ class DetailService { > >, ) { - const { data } = await net.request>({ + const { data } = await net.request({ url: `/api/steps/${id}`, method: 'PUT', data: step, @@ -161,7 +165,7 @@ class DetailService { // 删除步骤 async deleteStep(id: number) { // DELETE请求返回204状态码,通过拦截器处理为成功响应 - const { data } = await net.request>({ + const { data } = await net.request({ url: `/api/steps/${id}`, method: 'DELETE', }); @@ -170,23 +174,23 @@ class DetailService { // 获取项目的提交记录 async getCommits(projectId: number, branch?: string) { - const { data } = await net.request>({ + const { data } = await net.request({ url: `/api/git/commits?projectId=${projectId}${branch ? `&branch=${branch}` : ''}`, }); - return data; + return Array.isArray(data) ? data : data.list; } // 获取项目的分支列表 async getBranches(projectId: number) { - const { data } = await net.request>({ + const { data } = await net.request({ url: `/api/git/branches?projectId=${projectId}`, }); - return data; + return Array.isArray(data) ? data : data.list; } // 创建部署 async createDeployment(deployment: CreateDeploymentRequest) { - const { data } = await net.request>({ + const { data } = await net.request({ url: '/api/deployments', method: 'POST', data: deployment, @@ -196,7 +200,7 @@ class DetailService { // 重新执行部署 async retryDeployment(deploymentId: number) { - const { data } = await net.request>({ + const { data } = await net.request({ url: `/api/deployments/${deploymentId}/retry`, method: 'POST', }); @@ -205,7 +209,7 @@ class DetailService { // 获取项目详情(包含工作目录状态) async getProjectDetail(id: number) { - const { data } = await net.request>({ + const { data } = await net.request({ url: `/api/projects/${id}`, }); return data; @@ -213,7 +217,7 @@ class DetailService { // 更新项目 async updateProject(id: number, project: Partial) { - const { data } = await net.request>({ + const { data } = await net.request({ url: `/api/projects/${id}`, method: 'PUT', data: project, @@ -231,3 +235,10 @@ class DetailService { } export const detailService = new DetailService(); + +export interface DeploymentListResponse { + list: Deployment[]; + page: number; + pageSize: number; + total: number; +} diff --git a/apps/web/src/pages/project/list/index.tsx b/apps/web/src/pages/project/list/index.tsx index 322f26a..c168197 100644 --- a/apps/web/src/pages/project/list/index.tsx +++ b/apps/web/src/pages/project/list/index.tsx @@ -15,7 +15,7 @@ function ProjectPage() { useAsyncEffect(async () => { const response = await projectService.list(); - setProjects(response.data); + setProjects(response.list); }, []); const handleCreateProject = () => { diff --git a/apps/web/src/pages/project/list/service.ts b/apps/web/src/pages/project/list/service.ts index 243d312..bc1647c 100644 --- a/apps/web/src/pages/project/list/service.ts +++ b/apps/web/src/pages/project/list/service.ts @@ -1,18 +1,20 @@ -import { type APIResponse, net } from '../../../utils'; +import { net } from '../../../utils'; import type { Project } from '../types'; class ProjectService { async list(params?: ProjectQueryParams) { - const { data } = await net.request>({ + const { data } = await net.request({ method: 'GET', url: '/api/projects', params, }); - return data; + return Array.isArray(data) + ? { list: data, page: 1, pageSize: data.length, total: data.length } + : data; } async show(id: string) { - const { data } = await net.request>({ + const { data } = await net.request({ method: 'GET', url: `/api/projects/${id}`, }); @@ -24,7 +26,7 @@ class ProjectService { description?: string; repository: string; }) { - const { data } = await net.request>({ + const { data } = await net.request({ method: 'POST', url: '/api/projects', data: project, @@ -36,7 +38,7 @@ class ProjectService { id: string, project: Partial<{ name: string; description: string; repository: string }>, ) { - const { data } = await net.request>({ + const { data } = await net.request({ method: 'PUT', url: `/api/projects/${id}`, data: project, @@ -56,17 +58,14 @@ class ProjectService { export const projectService = new ProjectService(); interface ProjectListResponse { - data: Project[]; - pagination: { - page: number; - limit: number; - total: number; - totalPages: number; - }; + list: Project[]; + page: number; + pageSize: number; + total: number; } interface ProjectQueryParams { page?: number; - limit?: number; + pageSize?: number; name?: string; } diff --git a/apps/web/src/stores/global.tsx b/apps/web/src/stores/global.tsx index b629834..6c58bb0 100644 --- a/apps/web/src/stores/global.tsx +++ b/apps/web/src/stores/global.tsx @@ -1,25 +1,13 @@ -import { type APIResponse, net } from '../utils'; +import { net } from '@utils'; import { create } from 'zustand'; - -interface User { - id: string; - username: string; - email: string; - avatar_url: string; - active: boolean; -} - -interface GlobalStore { - user: User | null; - setUser: (user: User) => void; - refreshUser: () => Promise; -} +import type { GlobalStore } from './types'; +import type { User } from '@pages/login/types'; export const useGlobalStore = create((set) => ({ user: null, setUser: (user: User) => set({ user }), async refreshUser() { - const { data } = await net.request>({ + const { data } = await net.request({ method: 'GET', url: '/api/auth/info', }); diff --git a/apps/web/src/stores/types.ts b/apps/web/src/stores/types.ts new file mode 100644 index 0000000..310954d --- /dev/null +++ b/apps/web/src/stores/types.ts @@ -0,0 +1,13 @@ +interface User { + id: string; + username: string; + email: string; + avatar_url: string; + active: boolean; +} + +export interface GlobalStore { + user: User | null; + setUser: (user: User) => void; + refreshUser: () => Promise; +} \ No newline at end of file diff --git a/apps/web/src/utils/request.ts b/apps/web/src/utils/request.ts index 83d31ba..0241490 100644 --- a/apps/web/src/utils/request.ts +++ b/apps/web/src/utils/request.ts @@ -42,9 +42,9 @@ class Net { ); } - async request(config: AxiosRequestConfig): Promise { + async request(config: AxiosRequestConfig): Promise> { try { - const response = await this.instance.request(config); + const response = await this.instance.request>(config); if (!response || !response.data) { throw new Error('Invalid response'); } @@ -56,6 +56,13 @@ class Net { } } +export interface APIPagination { + list: T[]; + total: number; + page: number; + pageSize: number; +} + export interface APIResponse { code: number; data: T; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 9a0e08d..1d20568 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -24,7 +24,8 @@ "@pages/*": ["./src/pages/*"], "@styles/*": ["./src/styles/*"], "@assets/*": ["./src/assets/*"], - "@utils/*": ["./src/utils/*"] + "@utils/*": ["./src/utils/*"], + "@utils": ["./src/utils"] } }, "include": ["src"] diff --git a/docs/conventions.md b/docs/conventions.md index 5372f62..82fe8a1 100644 --- a/docs/conventions.md +++ b/docs/conventions.md @@ -32,16 +32,48 @@ web 项目代码组织如下: - 注释符合 jsdoc 规范 - 代码简洁,避免冗余,移除无用的代码引用、变量、函数和css样式 +- 禁止使用 any 类型 -## 4. 响应格式 +## 4. 前端发送net请求示例 -- 后端统一返回 `APIResponse` 结构: +- 分页 - ```json - { "code": 0, "data": {}, "message": "success", "timestamp": 12345678 } - ``` +```typescript +import {net} from '@utils'; +import type {APIPagination} from '@utils/net'; -- 由 `RouteScanner` 中的 `wrapControllerMethod` 自动封装。 +const data = await net.request>({ + method: 'GET', + url: '/api/deployments', + // 注意:查询参数使用 params 传递,不要手动拼接到 url 上 + params: { + projectId: 1, + page: 1, + pageSize: 10, + }, +}) +``` + +- 其他 + +```typescript +import {net} from '@utils'; + +const data = await net.request({ + method: 'POST', + url: '/api/deployment', + data: { + name: 'xxx', + description: 'xxx', + repository: 'https://a.com', + } +}) +if (data.code === 0) { + console.log("创建成功") +} else { + console.log("创建失败") +} +``` ## 5. 异步处理 diff --git a/docs/requirements/0001-Fix-Response-structure.md b/docs/requirements/0001-Fix-Response-structure.md new file mode 100644 index 0000000..b43a5cf --- /dev/null +++ b/docs/requirements/0001-Fix-Response-structure.md @@ -0,0 +1,33 @@ +# 标准化响应结构 + +1. 需要标准化接口响应的结构,修改后端接口中不符合规范的代码。 +2. 前端接口类型定义需要与后端接口响应结构保持一致。 + +## 响应示例 + +- 列表分页响应 + +```json +{ + "code": 0, // 响应状态码,0 表示成功,其他值表示失败 + "message": "success", // 响应消息 + "data": { // 响应数据 + "list": [], // 列表数据 + "page": 1, // 当前页码 + "pageSize": 10, // 每页显示数量 + "total": 10 // 总数量 + }, + "timestamp": "12346579" // 响应时间戳 +} +``` + +- 其他响应 + +```json +{ + "code": 0, + "message": "success", + "data": {}, + "timestamp": "12346579" +} +``` diff --git a/docs/status.md b/docs/status.md index 2e81c89..5db845f 100644 --- a/docs/status.md +++ b/docs/status.md @@ -7,15 +7,17 @@ - 项目管理 (CRUD) - 基础流水线执行流程 (Git Clone -> zx Run -> Log Update) - 前端项目列表与详情页预览 +- 优化: 移除菜单环境管理及页面(目前无用) +- 优化:移除创建 Step 弹窗可用环境变量区域 +- 优化: 部署记录的分页查询每页 10 条 ## 进行中 🚧 -- 优化: 移除菜单环境管理及页面(目前无用) -- 优化: 部署记录的分页查询 -- 修复: 表单必填项,*号和 label 不在一行 -- 修复:项目详情页,未选中 tab【部署记录】还会拉取日志信息 +- [标准化接口响应结构](./requirements/0001-Fix-Response-structure.md) ## 待办 📅 - [ ] Gitea Webhook 自动触发 - [ ] 用户权限管理 (RBAC) +- [ ] 修复: 表单必填项,*号和 label 不在一行 +- [ ] 修复:项目详情页,未选中 tab【部署记录】还会拉取日志信息