feat: 标准化响应体
This commit is contained in:
@@ -80,6 +80,7 @@ this.routeScanner.registerControllers([
|
|||||||
## TC39 装饰器特性
|
## TC39 装饰器特性
|
||||||
|
|
||||||
### 1. 标准语法
|
### 1. 标准语法
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// TC39 标准装饰器使用 addInitializer
|
// TC39 标准装饰器使用 addInitializer
|
||||||
@Get('/users')
|
@Get('/users')
|
||||||
@@ -89,6 +90,7 @@ async getUsers(ctx: Context) {
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 2. 类型安全
|
### 2. 类型安全
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 完整的 TypeScript 类型检查
|
// 完整的 TypeScript 类型检查
|
||||||
@Controller('/api')
|
@Controller('/api')
|
||||||
@@ -101,6 +103,7 @@ export class ApiController {
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 3. 无外部依赖
|
### 3. 无外部依赖
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 不再需要 reflect-metadata
|
// 不再需要 reflect-metadata
|
||||||
// 使用内置的 WeakMap 存储元数据
|
// 使用内置的 WeakMap 存储元数据
|
||||||
@@ -136,6 +139,7 @@ export class ApiController {
|
|||||||
最终的API路径 = 全局前缀 + 控制器前缀 + 方法路径
|
最终的API路径 = 全局前缀 + 控制器前缀 + 方法路径
|
||||||
|
|
||||||
例如:
|
例如:
|
||||||
|
|
||||||
- 全局前缀:`/api`
|
- 全局前缀:`/api`
|
||||||
- 控制器前缀:`/user`
|
- 控制器前缀:`/user`
|
||||||
- 方法路径:`/list`
|
- 方法路径:`/list`
|
||||||
@@ -176,56 +180,11 @@ async getUser(ctx: Context) {
|
|||||||
|
|
||||||
## 现有路由
|
## 现有路由
|
||||||
|
|
||||||
项目中已注册的路由:
|
|
||||||
|
|
||||||
### ApplicationController
|
|
||||||
- `GET /api/application/list` - 获取应用列表
|
|
||||||
- `GET /api/application/detail/:id` - 获取应用详情
|
|
||||||
|
|
||||||
### UserController
|
### UserController
|
||||||
|
|
||||||
- `GET /api/user/list` - 获取用户列表
|
- `GET /api/user/list` - 获取用户列表
|
||||||
- `GET /api/user/detail/:id` - 获取用户详情
|
- `GET /api/user/detail/:id` - 获取用户详情
|
||||||
- `POST /api/user` - 创建用户
|
- `POST /api/user` - 创建用户
|
||||||
- `PUT /api/user/:id` - 更新用户
|
- `PUT /api/user/:id` - 更新用户
|
||||||
- `DELETE /api/user/:id` - 删除用户
|
- `DELETE /api/user/:id` - 删除用户
|
||||||
- `GET /api/user/search` - 搜索用户
|
- `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` 进行初始化,性能更优
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const listDeploymentsQuerySchema = z.object({
|
export const listDeploymentsQuerySchema = z.object({
|
||||||
page: z.coerce.number().int().min(1).optional().default(1),
|
page: z.coerce.number().int().min(1).optional(),
|
||||||
pageSize: z.coerce.number().int().min(1).max(100).optional().default(10),
|
pageSize: z.coerce.number().int().min(1).max(100).optional(),
|
||||||
projectId: z.coerce.number().int().positive().optional(),
|
projectId: z.coerce.number().int().positive().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,22 +20,28 @@ export class DeploymentController {
|
|||||||
where.projectId = projectId;
|
where.projectId = projectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isPagination = page !== undefined && pageSize !== undefined;
|
||||||
|
|
||||||
const result = await prisma.deployment.findMany({
|
const result = await prisma.deployment.findMany({
|
||||||
where,
|
where,
|
||||||
take: pageSize,
|
take: isPagination ? pageSize : undefined,
|
||||||
skip: (page - 1) * pageSize,
|
skip: isPagination ? (page! - 1) * pageSize! : 0,
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'desc',
|
createdAt: 'desc',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const total = await prisma.deployment.count({ where });
|
const total = await prisma.deployment.count({ where });
|
||||||
|
|
||||||
return {
|
if (isPagination) {
|
||||||
data: result,
|
return {
|
||||||
page,
|
list: result,
|
||||||
pageSize,
|
page,
|
||||||
total,
|
pageSize,
|
||||||
};
|
total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('')
|
@Post('')
|
||||||
|
|||||||
@@ -66,19 +66,8 @@ export const updateProjectSchema = z.object({
|
|||||||
*/
|
*/
|
||||||
export const listProjectQuerySchema = z
|
export const listProjectQuerySchema = z
|
||||||
.object({
|
.object({
|
||||||
page: z.coerce
|
page: z.coerce.number().int().min(1).optional(),
|
||||||
.number()
|
pageSize: z.coerce.number().int().min(1).max(100).optional(),
|
||||||
.int()
|
|
||||||
.min(1, { message: '页码必须大于0' })
|
|
||||||
.optional()
|
|
||||||
.default(1),
|
|
||||||
limit: z.coerce
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.min(1, { message: '每页数量必须大于0' })
|
|
||||||
.max(100, { message: '每页数量不能超过100' })
|
|
||||||
.optional()
|
|
||||||
.default(10),
|
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
})
|
})
|
||||||
.optional();
|
.optional();
|
||||||
|
|||||||
@@ -29,27 +29,30 @@ export class ProjectController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isPagination = query?.page !== undefined && query?.pageSize !== undefined;
|
||||||
|
|
||||||
const [total, projects] = await Promise.all([
|
const [total, projects] = await Promise.all([
|
||||||
prisma.project.count({ where: whereCondition }),
|
prisma.project.count({ where: whereCondition }),
|
||||||
prisma.project.findMany({
|
prisma.project.findMany({
|
||||||
where: whereCondition,
|
where: whereCondition,
|
||||||
skip: query ? (query.page - 1) * query.limit : 0,
|
skip: isPagination ? (query.page! - 1) * query.pageSize! : 0,
|
||||||
take: query?.limit,
|
take: isPagination ? query.pageSize : undefined,
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'desc',
|
createdAt: 'desc',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
if (isPagination) {
|
||||||
data: projects,
|
return {
|
||||||
pagination: {
|
list: projects,
|
||||||
page: query?.page || 1,
|
page: query.page,
|
||||||
limit: query?.limit || 10,
|
pageSize: query.pageSize,
|
||||||
total,
|
total,
|
||||||
totalPages: Math.ceil(total / (query?.limit || 10)),
|
};
|
||||||
},
|
}
|
||||||
};
|
|
||||||
|
return projects;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/projects/:id - 获取单个项目
|
// GET /api/projects/:id - 获取单个项目
|
||||||
|
|||||||
@@ -84,15 +84,13 @@ export const listStepsQuerySchema = z
|
|||||||
.number()
|
.number()
|
||||||
.int()
|
.int()
|
||||||
.min(1, { message: '页码必须大于0' })
|
.min(1, { message: '页码必须大于0' })
|
||||||
.optional()
|
.optional(),
|
||||||
.default(1),
|
pageSize: z.coerce
|
||||||
limit: z.coerce
|
|
||||||
.number()
|
.number()
|
||||||
.int()
|
.int()
|
||||||
.min(1, { message: '每页数量必须大于0' })
|
.min(1, { message: '每页数量必须大于0' })
|
||||||
.max(100, { message: '每页数量不能超过100' })
|
.max(100, { message: '每页数量不能超过100' })
|
||||||
.optional()
|
.optional(),
|
||||||
.default(10),
|
|
||||||
})
|
})
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
|
|||||||
@@ -26,27 +26,30 @@ export class StepController {
|
|||||||
whereCondition.pipelineId = query.pipelineId;
|
whereCondition.pipelineId = query.pipelineId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isPagination = query?.page !== undefined && query?.pageSize !== undefined;
|
||||||
|
|
||||||
const [total, steps] = await Promise.all([
|
const [total, steps] = await Promise.all([
|
||||||
prisma.step.count({ where: whereCondition }),
|
prisma.step.count({ where: whereCondition }),
|
||||||
prisma.step.findMany({
|
prisma.step.findMany({
|
||||||
where: whereCondition,
|
where: whereCondition,
|
||||||
skip: query ? (query.page - 1) * query.limit : 0,
|
skip: isPagination ? (query.page! - 1) * query.pageSize! : 0,
|
||||||
take: query?.limit,
|
take: isPagination ? query.pageSize : undefined,
|
||||||
orderBy: {
|
orderBy: {
|
||||||
order: 'asc',
|
order: 'asc',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
if (isPagination) {
|
||||||
data: steps,
|
return {
|
||||||
pagination: {
|
list: steps,
|
||||||
page: query?.page || 1,
|
page: query.page,
|
||||||
limit: query?.limit || 10,
|
pageSize: query.pageSize,
|
||||||
total,
|
total,
|
||||||
totalPages: Math.ceil(total / (query?.limit || 10)),
|
};
|
||||||
},
|
}
|
||||||
};
|
|
||||||
|
return steps;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/steps/:id - 获取单个步骤
|
// GET /api/steps/:id - 获取单个步骤
|
||||||
|
|||||||
@@ -118,11 +118,6 @@ export class UserController {
|
|||||||
results = results.filter((user) => user.status === status);
|
results = results.filter((user) => user.status === status);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return results;
|
||||||
keyword,
|
|
||||||
status,
|
|
||||||
total: results.length,
|
|
||||||
results,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -2,7 +2,7 @@ import type React from 'react';
|
|||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
export function useAsyncEffect(
|
export function useAsyncEffect(
|
||||||
effect: () => Promise<undefined | (() => void)>,
|
effect: () => Promise<any | (() => void)>,
|
||||||
deps: React.DependencyList,
|
deps: React.DependencyList,
|
||||||
) {
|
) {
|
||||||
const callback = useCallback(effect, [...deps]);
|
const callback = useCallback(effect, [...deps]);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Env from '@pages/env';
|
|
||||||
import Home from '@pages/home';
|
import Home from '@pages/home';
|
||||||
import Login from '@pages/login';
|
import Login from '@pages/login';
|
||||||
import ProjectDetail from '@pages/project/detail';
|
import ProjectDetail from '@pages/project/detail';
|
||||||
@@ -13,7 +13,7 @@ const App = () => {
|
|||||||
<Route index element={<Navigate to="project" replace />} />
|
<Route index element={<Navigate to="project" replace />} />
|
||||||
<Route path="project" element={<ProjectList />} />
|
<Route path="project" element={<ProjectList />} />
|
||||||
<Route path="project/:id" element={<ProjectDetail />} />
|
<Route path="project/:id" element={<ProjectDetail />} />
|
||||||
<Route path="env" element={<Env />} />
|
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
5
apps/web/src/pages/env/index.tsx
vendored
5
apps/web/src/pages/env/index.tsx
vendored
@@ -1,5 +0,0 @@
|
|||||||
function Env() {
|
|
||||||
return <div>env page</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Env;
|
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
IconExport,
|
IconExport,
|
||||||
IconMenuFold,
|
IconMenuFold,
|
||||||
IconMenuUnfold,
|
IconMenuUnfold,
|
||||||
IconRobot,
|
|
||||||
} from '@arco-design/web-react/icon';
|
} from '@arco-design/web-react/icon';
|
||||||
import Logo from '@assets/images/logo.svg?react';
|
import Logo from '@assets/images/logo.svg?react';
|
||||||
import { loginService } from '@pages/login/service';
|
import { loginService } from '@pages/login/service';
|
||||||
@@ -45,12 +45,7 @@ function Home() {
|
|||||||
<span>项目管理</span>
|
<span>项目管理</span>
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key="1">
|
|
||||||
<Link to="/env">
|
|
||||||
<IconRobot fontSize={16} />
|
|
||||||
环境管理
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu>
|
</Menu>
|
||||||
</Layout.Sider>
|
</Layout.Sider>
|
||||||
<Layout>
|
<Layout>
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { Message, Notification } from '@arco-design/web-react';
|
|||||||
import { net } from '../../utils';
|
import { net } from '../../utils';
|
||||||
import type { NavigateFunction } from 'react-router';
|
import type { NavigateFunction } from 'react-router';
|
||||||
import { useGlobalStore } from '../../stores/global';
|
import { useGlobalStore } from '../../stores/global';
|
||||||
import type { AuthLoginResponse, AuthURLResponse } from './types';
|
import type { AuthURL, User } from './types';
|
||||||
|
|
||||||
class LoginService {
|
class LoginService {
|
||||||
async getAuthUrl() {
|
async getAuthUrl() {
|
||||||
const { code, data } = await net.request<AuthURLResponse>({
|
const { code, data } = await net.request<AuthURL>({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/auth/url',
|
url: '/api/auth/url',
|
||||||
params: {
|
params: {
|
||||||
@@ -19,7 +19,7 @@ class LoginService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async login(authCode: string, navigate: NavigateFunction) {
|
async login(authCode: string, navigate: NavigateFunction) {
|
||||||
const { data, code } = await net.request<AuthLoginResponse>({
|
const { data, code } = await net.request<User>({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/api/auth/login',
|
url: '/api/auth/login',
|
||||||
data: {
|
data: {
|
||||||
@@ -37,7 +37,7 @@ class LoginService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async logout() {
|
async logout() {
|
||||||
const { code } = await net.request<AuthURLResponse>({
|
const { code } = await net.request<null>({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/auth/logout',
|
url: '/api/auth/logout',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { APIResponse } from '../../utils';
|
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
@@ -8,8 +6,6 @@ export interface User {
|
|||||||
active: boolean;
|
active: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuthURLResponse = APIResponse<{
|
export interface AuthURL {
|
||||||
url: string;
|
url: string;
|
||||||
}>;
|
};
|
||||||
|
|
||||||
export type AuthLoginResponse = APIResponse<User>;
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
Message,
|
Message,
|
||||||
Modal,
|
Modal,
|
||||||
|
Pagination,
|
||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
Switch,
|
Switch,
|
||||||
@@ -94,6 +95,11 @@ function ProjectDetailPage() {
|
|||||||
const [pipelineForm] = Form.useForm();
|
const [pipelineForm] = Form.useForm();
|
||||||
const [deployRecords, setDeployRecords] = useState<Deployment[]>([]);
|
const [deployRecords, setDeployRecords] = useState<Deployment[]>([]);
|
||||||
const [deployModalVisible, setDeployModalVisible] = useState(false);
|
const [deployModalVisible, setDeployModalVisible] = useState(false);
|
||||||
|
const [pagination, setPagination] = useState({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
// 流水线模板相关状态
|
// 流水线模板相关状态
|
||||||
const [isCreatingFromTemplate, setIsCreatingFromTemplate] = useState(false);
|
const [isCreatingFromTemplate, setIsCreatingFromTemplate] = useState(false);
|
||||||
@@ -153,10 +159,15 @@ function ProjectDetailPage() {
|
|||||||
|
|
||||||
// 获取部署记录
|
// 获取部署记录
|
||||||
try {
|
try {
|
||||||
const records = await detailService.getDeployments(Number(id));
|
const res = await detailService.getDeployments(
|
||||||
setDeployRecords(records);
|
Number(id),
|
||||||
if (records.length > 0) {
|
1,
|
||||||
setSelectedRecordId(records[0].id);
|
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) {
|
} catch (error) {
|
||||||
console.error('获取部署记录失败:', error);
|
console.error('获取部署记录失败:', error);
|
||||||
@@ -175,32 +186,40 @@ function ProjectDetailPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 定期轮询部署记录以更新状态和日志
|
// 定期轮询部署记录以更新状态和日志
|
||||||
useAsyncEffect(async () => {
|
useEffect(() => {
|
||||||
const interval = setInterval(async () => {
|
if (!id) return;
|
||||||
if (id) {
|
|
||||||
try {
|
const poll = async () => {
|
||||||
const records = await detailService.getDeployments(Number(id));
|
try {
|
||||||
setDeployRecords(records);
|
const res = await detailService.getDeployments(
|
||||||
|
Number(id),
|
||||||
|
pagination.current,
|
||||||
|
pagination.pageSize,
|
||||||
|
);
|
||||||
|
setDeployRecords(res.list);
|
||||||
|
setPagination((prev) => ({ ...prev, total: res.total }));
|
||||||
|
|
||||||
// 如果当前选中的记录正在运行,则更新选中记录
|
// 如果当前选中的记录正在运行,则更新选中记录
|
||||||
const selectedRecord = records.find(
|
const selectedRecord = res.list.find(
|
||||||
(r: Deployment) => r.id === selectedRecordId,
|
(r: Deployment) => r.id === selectedRecordId,
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
selectedRecord &&
|
selectedRecord &&
|
||||||
(selectedRecord.status === 'running' ||
|
(selectedRecord.status === 'running' ||
|
||||||
selectedRecord.status === 'pending')
|
selectedRecord.status === 'pending')
|
||||||
) {
|
) {
|
||||||
// 保持当前选中状态,但更新数据
|
// 保持当前选中状态,但更新数据
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('轮询部署记录失败:', error);
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('轮询部署记录失败:', error);
|
||||||
}
|
}
|
||||||
}, 3000); // 每3秒轮询一次
|
};
|
||||||
|
|
||||||
|
poll(); // 立即执行一次
|
||||||
|
const interval = setInterval(poll, 3000); // 每3秒轮询一次
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [id, selectedRecordId]);
|
}, [id, selectedRecordId, pagination.current, pagination.pageSize]);
|
||||||
|
|
||||||
// 触发部署
|
// 触发部署
|
||||||
const handleDeploy = () => {
|
const handleDeploy = () => {
|
||||||
@@ -601,8 +620,13 @@ function ProjectDetailPage() {
|
|||||||
|
|
||||||
// 刷新部署记录
|
// 刷新部署记录
|
||||||
if (id) {
|
if (id) {
|
||||||
const records = await detailService.getDeployments(Number(id));
|
const res = await detailService.getDeployments(
|
||||||
setDeployRecords(records);
|
Number(id),
|
||||||
|
pagination.current,
|
||||||
|
pagination.pageSize,
|
||||||
|
);
|
||||||
|
setDeployRecords(res.list);
|
||||||
|
setPagination((prev) => ({ ...prev, total: res.total }));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('重新执行部署失败:', error);
|
console.error('重新执行部署失败:', error);
|
||||||
@@ -841,19 +865,33 @@ function ProjectDetailPage() {
|
|||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto min-h-0">
|
<div className="flex-1 overflow-y-auto min-h-0 flex flex-col">
|
||||||
{deployRecords.length > 0 ? (
|
<div className="flex-1 overflow-y-auto">
|
||||||
<List
|
{deployRecords.length > 0 ? (
|
||||||
className="bg-white rounded-lg border"
|
<List
|
||||||
dataSource={deployRecords}
|
className="bg-white rounded-lg border"
|
||||||
render={renderDeployRecordItem}
|
dataSource={deployRecords}
|
||||||
split={true}
|
render={renderDeployRecordItem}
|
||||||
|
split={true}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<Empty description="暂无部署记录" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-right">
|
||||||
|
<Pagination
|
||||||
|
total={pagination.total}
|
||||||
|
current={pagination.current}
|
||||||
|
pageSize={pagination.pageSize}
|
||||||
|
size="small"
|
||||||
|
simple
|
||||||
|
onChange={(page) =>
|
||||||
|
setPagination((prev) => ({ ...prev, current: page }))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
</div>
|
||||||
<div className="text-center py-12">
|
|
||||||
<Empty description="暂无部署记录" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1383,14 +1421,7 @@ function ProjectDetailPage() {
|
|||||||
style={{ fontFamily: 'Monaco, Consolas, monospace' }}
|
style={{ fontFamily: 'Monaco, Consolas, monospace' }}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<div className="bg-blue-50 p-3 rounded text-sm">
|
|
||||||
<Typography.Text type="secondary">
|
|
||||||
<strong>可用环境变量:</strong>
|
|
||||||
<br />• $PROJECT_NAME - 项目名称
|
|
||||||
<br />• $BUILD_NUMBER - 构建编号
|
|
||||||
<br />• $REGISTRY - 镜像仓库地址
|
|
||||||
</Typography.Text>
|
|
||||||
</div>
|
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
@@ -1401,12 +1432,19 @@ function ProjectDetailPage() {
|
|||||||
setDeployModalVisible(false);
|
setDeployModalVisible(false);
|
||||||
// 刷新部署记录
|
// 刷新部署记录
|
||||||
if (id) {
|
if (id) {
|
||||||
detailService.getDeployments(Number(id)).then((records) => {
|
detailService
|
||||||
setDeployRecords(records);
|
.getDeployments(Number(id), 1, pagination.pageSize)
|
||||||
if (records.length > 0) {
|
.then((res) => {
|
||||||
setSelectedRecordId(records[0].id);
|
setDeployRecords(res.list);
|
||||||
}
|
setPagination((prev) => ({
|
||||||
});
|
...prev,
|
||||||
|
total: res.total,
|
||||||
|
current: 1,
|
||||||
|
}));
|
||||||
|
if (res.list.length > 0) {
|
||||||
|
setSelectedRecordId(res.list[0].id);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
pipelines={pipelines}
|
pipelines={pipelines}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type APIResponse, net } from '../../../utils';
|
import { net } from '../../../utils';
|
||||||
import type {
|
import type {
|
||||||
Branch,
|
Branch,
|
||||||
Commit,
|
Commit,
|
||||||
@@ -11,7 +11,7 @@ import type {
|
|||||||
|
|
||||||
class DetailService {
|
class DetailService {
|
||||||
async getProject(id: string) {
|
async getProject(id: string) {
|
||||||
const { data } = await net.request<APIResponse<Project>>({
|
const { data } = await net.request<Project>({
|
||||||
url: `/api/projects/${id}`,
|
url: `/api/projects/${id}`,
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
@@ -19,28 +19,32 @@ class DetailService {
|
|||||||
|
|
||||||
// 获取项目的所有流水线
|
// 获取项目的所有流水线
|
||||||
async getPipelines(projectId: number) {
|
async getPipelines(projectId: number) {
|
||||||
const { data } = await net.request<APIResponse<Pipeline[]>>({
|
const { data } = await net.request<Pipeline[] | { list: Pipeline[] }>({
|
||||||
url: `/api/pipelines?projectId=${projectId}`,
|
url: `/api/pipelines?projectId=${projectId}`,
|
||||||
});
|
});
|
||||||
return data;
|
return Array.isArray(data) ? data : data.list;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取可用的流水线模板
|
// 获取可用的流水线模板
|
||||||
async getPipelineTemplates() {
|
async getPipelineTemplates() {
|
||||||
const { data } = await net.request<
|
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',
|
url: '/api/pipelines/templates',
|
||||||
});
|
});
|
||||||
return data;
|
return Array.isArray(data) ? data : data.list;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取项目的部署记录
|
async getDeployments(
|
||||||
async getDeployments(projectId: number) {
|
projectId: number,
|
||||||
const { data } = await net.request<any>({
|
page: number = 1,
|
||||||
url: `/api/deployments?projectId=${projectId}`,
|
pageSize: number = 10,
|
||||||
|
) {
|
||||||
|
const { data } = await net.request<DeploymentListResponse>({
|
||||||
|
url: `/api/deployments?projectId=${projectId}&page=${page}&pageSize=${pageSize}`,
|
||||||
});
|
});
|
||||||
return data.data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建流水线
|
// 创建流水线
|
||||||
@@ -56,7 +60,7 @@ class DetailService {
|
|||||||
| 'steps'
|
| 'steps'
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
const { data } = await net.request<APIResponse<Pipeline>>({
|
const { data } = await net.request<Pipeline>({
|
||||||
url: '/api/pipelines',
|
url: '/api/pipelines',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: pipeline,
|
data: pipeline,
|
||||||
@@ -71,7 +75,7 @@ class DetailService {
|
|||||||
name: string,
|
name: string,
|
||||||
description?: string,
|
description?: string,
|
||||||
) {
|
) {
|
||||||
const { data } = await net.request<APIResponse<Pipeline>>({
|
const { data } = await net.request<Pipeline>({
|
||||||
url: '/api/pipelines/from-template',
|
url: '/api/pipelines/from-template',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
@@ -100,7 +104,7 @@ class DetailService {
|
|||||||
>
|
>
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
const { data } = await net.request<APIResponse<Pipeline>>({
|
const { data } = await net.request<Pipeline>({
|
||||||
url: `/api/pipelines/${id}`,
|
url: `/api/pipelines/${id}`,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
data: pipeline,
|
data: pipeline,
|
||||||
@@ -110,7 +114,7 @@ class DetailService {
|
|||||||
|
|
||||||
// 删除流水线
|
// 删除流水线
|
||||||
async deletePipeline(id: number) {
|
async deletePipeline(id: number) {
|
||||||
const { data } = await net.request<APIResponse<null>>({
|
const { data } = await net.request<null>({
|
||||||
url: `/api/pipelines/${id}`,
|
url: `/api/pipelines/${id}`,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
@@ -119,10 +123,10 @@ class DetailService {
|
|||||||
|
|
||||||
// 获取流水线的所有步骤
|
// 获取流水线的所有步骤
|
||||||
async getSteps(pipelineId: number) {
|
async getSteps(pipelineId: number) {
|
||||||
const { data } = await net.request<APIResponse<Step[]>>({
|
const { data } = await net.request<Step[] | { list: Step[] }>({
|
||||||
url: `/api/steps?pipelineId=${pipelineId}`,
|
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'
|
'id' | 'createdAt' | 'updatedAt' | 'createdBy' | 'updatedBy' | 'valid'
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
const { data } = await net.request<APIResponse<Step>>({
|
const { data } = await net.request<Step>({
|
||||||
url: '/api/steps',
|
url: '/api/steps',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: step,
|
data: step,
|
||||||
@@ -150,7 +154,7 @@ class DetailService {
|
|||||||
>
|
>
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
const { data } = await net.request<APIResponse<Step>>({
|
const { data } = await net.request<Step>({
|
||||||
url: `/api/steps/${id}`,
|
url: `/api/steps/${id}`,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
data: step,
|
data: step,
|
||||||
@@ -161,7 +165,7 @@ class DetailService {
|
|||||||
// 删除步骤
|
// 删除步骤
|
||||||
async deleteStep(id: number) {
|
async deleteStep(id: number) {
|
||||||
// DELETE请求返回204状态码,通过拦截器处理为成功响应
|
// DELETE请求返回204状态码,通过拦截器处理为成功响应
|
||||||
const { data } = await net.request<APIResponse<null>>({
|
const { data } = await net.request<null>({
|
||||||
url: `/api/steps/${id}`,
|
url: `/api/steps/${id}`,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
@@ -170,23 +174,23 @@ class DetailService {
|
|||||||
|
|
||||||
// 获取项目的提交记录
|
// 获取项目的提交记录
|
||||||
async getCommits(projectId: number, branch?: string) {
|
async getCommits(projectId: number, branch?: string) {
|
||||||
const { data } = await net.request<APIResponse<Commit[]>>({
|
const { data } = await net.request<Commit[] | { list: Commit[] }>({
|
||||||
url: `/api/git/commits?projectId=${projectId}${branch ? `&branch=${branch}` : ''}`,
|
url: `/api/git/commits?projectId=${projectId}${branch ? `&branch=${branch}` : ''}`,
|
||||||
});
|
});
|
||||||
return data;
|
return Array.isArray(data) ? data : data.list;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取项目的分支列表
|
// 获取项目的分支列表
|
||||||
async getBranches(projectId: number) {
|
async getBranches(projectId: number) {
|
||||||
const { data } = await net.request<APIResponse<Branch[]>>({
|
const { data } = await net.request<Branch[] | { list: Branch[] }>({
|
||||||
url: `/api/git/branches?projectId=${projectId}`,
|
url: `/api/git/branches?projectId=${projectId}`,
|
||||||
});
|
});
|
||||||
return data;
|
return Array.isArray(data) ? data : data.list;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建部署
|
// 创建部署
|
||||||
async createDeployment(deployment: CreateDeploymentRequest) {
|
async createDeployment(deployment: CreateDeploymentRequest) {
|
||||||
const { data } = await net.request<APIResponse<Deployment>>({
|
const { data } = await net.request<Deployment>({
|
||||||
url: '/api/deployments',
|
url: '/api/deployments',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: deployment,
|
data: deployment,
|
||||||
@@ -196,7 +200,7 @@ class DetailService {
|
|||||||
|
|
||||||
// 重新执行部署
|
// 重新执行部署
|
||||||
async retryDeployment(deploymentId: number) {
|
async retryDeployment(deploymentId: number) {
|
||||||
const { data } = await net.request<APIResponse<Deployment>>({
|
const { data } = await net.request<Deployment>({
|
||||||
url: `/api/deployments/${deploymentId}/retry`,
|
url: `/api/deployments/${deploymentId}/retry`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
});
|
||||||
@@ -205,7 +209,7 @@ class DetailService {
|
|||||||
|
|
||||||
// 获取项目详情(包含工作目录状态)
|
// 获取项目详情(包含工作目录状态)
|
||||||
async getProjectDetail(id: number) {
|
async getProjectDetail(id: number) {
|
||||||
const { data } = await net.request<APIResponse<Project>>({
|
const { data } = await net.request<Project>({
|
||||||
url: `/api/projects/${id}`,
|
url: `/api/projects/${id}`,
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
@@ -213,7 +217,7 @@ class DetailService {
|
|||||||
|
|
||||||
// 更新项目
|
// 更新项目
|
||||||
async updateProject(id: number, project: Partial<Project>) {
|
async updateProject(id: number, project: Partial<Project>) {
|
||||||
const { data } = await net.request<APIResponse<Project>>({
|
const { data } = await net.request<Project>({
|
||||||
url: `/api/projects/${id}`,
|
url: `/api/projects/${id}`,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
data: project,
|
data: project,
|
||||||
@@ -231,3 +235,10 @@ class DetailService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const detailService = new DetailService();
|
export const detailService = new DetailService();
|
||||||
|
|
||||||
|
export interface DeploymentListResponse {
|
||||||
|
list: Deployment[];
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ function ProjectPage() {
|
|||||||
|
|
||||||
useAsyncEffect(async () => {
|
useAsyncEffect(async () => {
|
||||||
const response = await projectService.list();
|
const response = await projectService.list();
|
||||||
setProjects(response.data);
|
setProjects(response.list);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCreateProject = () => {
|
const handleCreateProject = () => {
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import { type APIResponse, net } from '../../../utils';
|
import { net } from '../../../utils';
|
||||||
import type { Project } from '../types';
|
import type { Project } from '../types';
|
||||||
|
|
||||||
class ProjectService {
|
class ProjectService {
|
||||||
async list(params?: ProjectQueryParams) {
|
async list(params?: ProjectQueryParams) {
|
||||||
const { data } = await net.request<APIResponse<ProjectListResponse>>({
|
const { data } = await net.request<Project[] | ProjectListResponse>({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/projects',
|
url: '/api/projects',
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
return data;
|
return Array.isArray(data)
|
||||||
|
? { list: data, page: 1, pageSize: data.length, total: data.length }
|
||||||
|
: data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async show(id: string) {
|
async show(id: string) {
|
||||||
const { data } = await net.request<APIResponse<Project>>({
|
const { data } = await net.request<Project>({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: `/api/projects/${id}`,
|
url: `/api/projects/${id}`,
|
||||||
});
|
});
|
||||||
@@ -24,7 +26,7 @@ class ProjectService {
|
|||||||
description?: string;
|
description?: string;
|
||||||
repository: string;
|
repository: string;
|
||||||
}) {
|
}) {
|
||||||
const { data } = await net.request<APIResponse<Project>>({
|
const { data } = await net.request<Project>({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/api/projects',
|
url: '/api/projects',
|
||||||
data: project,
|
data: project,
|
||||||
@@ -36,7 +38,7 @@ class ProjectService {
|
|||||||
id: string,
|
id: string,
|
||||||
project: Partial<{ name: string; description: string; repository: string }>,
|
project: Partial<{ name: string; description: string; repository: string }>,
|
||||||
) {
|
) {
|
||||||
const { data } = await net.request<APIResponse<Project>>({
|
const { data } = await net.request<Project>({
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
url: `/api/projects/${id}`,
|
url: `/api/projects/${id}`,
|
||||||
data: project,
|
data: project,
|
||||||
@@ -56,17 +58,14 @@ class ProjectService {
|
|||||||
export const projectService = new ProjectService();
|
export const projectService = new ProjectService();
|
||||||
|
|
||||||
interface ProjectListResponse {
|
interface ProjectListResponse {
|
||||||
data: Project[];
|
list: Project[];
|
||||||
pagination: {
|
page: number;
|
||||||
page: number;
|
pageSize: number;
|
||||||
limit: number;
|
total: number;
|
||||||
total: number;
|
|
||||||
totalPages: number;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProjectQueryParams {
|
interface ProjectQueryParams {
|
||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
pageSize?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,13 @@
|
|||||||
import { type APIResponse, net } from '../utils';
|
import { net } from '@utils';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
import type { GlobalStore } from './types';
|
||||||
interface User {
|
import type { User } from '@pages/login/types';
|
||||||
id: string;
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
avatar_url: string;
|
|
||||||
active: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GlobalStore {
|
|
||||||
user: User | null;
|
|
||||||
setUser: (user: User) => void;
|
|
||||||
refreshUser: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useGlobalStore = create<GlobalStore>((set) => ({
|
export const useGlobalStore = create<GlobalStore>((set) => ({
|
||||||
user: null,
|
user: null,
|
||||||
setUser: (user: User) => set({ user }),
|
setUser: (user: User) => set({ user }),
|
||||||
async refreshUser() {
|
async refreshUser() {
|
||||||
const { data } = await net.request<APIResponse<User>>({
|
const { data } = await net.request<User>({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/api/auth/info',
|
url: '/api/auth/info',
|
||||||
});
|
});
|
||||||
|
|||||||
13
apps/web/src/stores/types.ts
Normal file
13
apps/web/src/stores/types.ts
Normal file
@@ -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<void>;
|
||||||
|
}
|
||||||
@@ -42,9 +42,9 @@ class Net {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async request<T>(config: AxiosRequestConfig): Promise<T> {
|
async request<T>(config: AxiosRequestConfig): Promise<APIResponse<T>> {
|
||||||
try {
|
try {
|
||||||
const response = await this.instance.request<T>(config);
|
const response = await this.instance.request<APIResponse<T>>(config);
|
||||||
if (!response || !response.data) {
|
if (!response || !response.data) {
|
||||||
throw new Error('Invalid response');
|
throw new Error('Invalid response');
|
||||||
}
|
}
|
||||||
@@ -56,6 +56,13 @@ class Net {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface APIPagination<T> {
|
||||||
|
list: T[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface APIResponse<T> {
|
export interface APIResponse<T> {
|
||||||
code: number;
|
code: number;
|
||||||
data: T;
|
data: T;
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
"@pages/*": ["./src/pages/*"],
|
"@pages/*": ["./src/pages/*"],
|
||||||
"@styles/*": ["./src/styles/*"],
|
"@styles/*": ["./src/styles/*"],
|
||||||
"@assets/*": ["./src/assets/*"],
|
"@assets/*": ["./src/assets/*"],
|
||||||
"@utils/*": ["./src/utils/*"]
|
"@utils/*": ["./src/utils/*"],
|
||||||
|
"@utils": ["./src/utils"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
|
|||||||
@@ -32,16 +32,48 @@ web 项目代码组织如下:
|
|||||||
|
|
||||||
- 注释符合 jsdoc 规范
|
- 注释符合 jsdoc 规范
|
||||||
- 代码简洁,避免冗余,移除无用的代码引用、变量、函数和css样式
|
- 代码简洁,避免冗余,移除无用的代码引用、变量、函数和css样式
|
||||||
|
- 禁止使用 any 类型
|
||||||
|
|
||||||
## 4. 响应格式
|
## 4. 前端发送net请求示例
|
||||||
|
|
||||||
- 后端统一返回 `APIResponse<T>` 结构:
|
- 分页
|
||||||
|
|
||||||
```json
|
```typescript
|
||||||
{ "code": 0, "data": {}, "message": "success", "timestamp": 12345678 }
|
import {net} from '@utils';
|
||||||
```
|
import type {APIPagination} from '@utils/net';
|
||||||
|
|
||||||
- 由 `RouteScanner` 中的 `wrapControllerMethod` 自动封装。
|
const data = await net.request<APIPagination<Deployment>>({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/deployments',
|
||||||
|
// 注意:查询参数使用 params 传递,不要手动拼接到 url 上
|
||||||
|
params: {
|
||||||
|
projectId: 1,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- 其他
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {net} from '@utils';
|
||||||
|
|
||||||
|
const data = await net.request<void>({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/deployment',
|
||||||
|
data: {
|
||||||
|
name: 'xxx',
|
||||||
|
description: 'xxx',
|
||||||
|
repository: 'https://a.com',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (data.code === 0) {
|
||||||
|
console.log("创建成功")
|
||||||
|
} else {
|
||||||
|
console.log("创建失败")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## 5. 异步处理
|
## 5. 异步处理
|
||||||
|
|
||||||
|
|||||||
33
docs/requirements/0001-Fix-Response-structure.md
Normal file
33
docs/requirements/0001-Fix-Response-structure.md
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -7,15 +7,17 @@
|
|||||||
- 项目管理 (CRUD)
|
- 项目管理 (CRUD)
|
||||||
- 基础流水线执行流程 (Git Clone -> zx Run -> Log Update)
|
- 基础流水线执行流程 (Git Clone -> zx Run -> Log Update)
|
||||||
- 前端项目列表与详情页预览
|
- 前端项目列表与详情页预览
|
||||||
|
- 优化: 移除菜单环境管理及页面(目前无用)
|
||||||
|
- 优化:移除创建 Step 弹窗可用环境变量区域
|
||||||
|
- 优化: 部署记录的分页查询每页 10 条
|
||||||
|
|
||||||
## 进行中 🚧
|
## 进行中 🚧
|
||||||
|
|
||||||
- 优化: 移除菜单环境管理及页面(目前无用)
|
- [标准化接口响应结构](./requirements/0001-Fix-Response-structure.md)
|
||||||
- 优化: 部署记录的分页查询
|
|
||||||
- 修复: 表单必填项,*号和 label 不在一行
|
|
||||||
- 修复:项目详情页,未选中 tab【部署记录】还会拉取日志信息
|
|
||||||
|
|
||||||
## 待办 📅
|
## 待办 📅
|
||||||
|
|
||||||
- [ ] Gitea Webhook 自动触发
|
- [ ] Gitea Webhook 自动触发
|
||||||
- [ ] 用户权限管理 (RBAC)
|
- [ ] 用户权限管理 (RBAC)
|
||||||
|
- [ ] 修复: 表单必填项,*号和 label 不在一行
|
||||||
|
- [ ] 修复:项目详情页,未选中 tab【部署记录】还会拉取日志信息
|
||||||
|
|||||||
Reference in New Issue
Block a user