feat: 标准化响应体
This commit is contained in:
@@ -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 = () => {
|
||||
<Route index element={<Navigate to="project" replace />} />
|
||||
<Route path="project" element={<ProjectList />} />
|
||||
<Route path="project/:id" element={<ProjectDetail />} />
|
||||
<Route path="env" element={<Env />} />
|
||||
|
||||
</Route>
|
||||
<Route path="/login" element={<Login />} />
|
||||
</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,
|
||||
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() {
|
||||
<span>项目管理</span>
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="1">
|
||||
<Link to="/env">
|
||||
<IconRobot fontSize={16} />
|
||||
环境管理
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
|
||||
</Menu>
|
||||
</Layout.Sider>
|
||||
<Layout>
|
||||
|
||||
@@ -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<AuthURLResponse>({
|
||||
const { code, data } = await net.request<AuthURL>({
|
||||
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<AuthLoginResponse>({
|
||||
const { data, code } = await net.request<User>({
|
||||
method: 'POST',
|
||||
url: '/api/auth/login',
|
||||
data: {
|
||||
@@ -37,7 +37,7 @@ class LoginService {
|
||||
}
|
||||
|
||||
async logout() {
|
||||
const { code } = await net.request<AuthURLResponse>({
|
||||
const { code } = await net.request<null>({
|
||||
method: 'GET',
|
||||
url: '/api/auth/logout',
|
||||
});
|
||||
|
||||
@@ -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<User>;
|
||||
};
|
||||
|
||||
@@ -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<Deployment[]>([]);
|
||||
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() {
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto min-h-0">
|
||||
{deployRecords.length > 0 ? (
|
||||
<List
|
||||
className="bg-white rounded-lg border"
|
||||
dataSource={deployRecords}
|
||||
render={renderDeployRecordItem}
|
||||
split={true}
|
||||
<div className="flex-1 overflow-y-auto min-h-0 flex flex-col">
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{deployRecords.length > 0 ? (
|
||||
<List
|
||||
className="bg-white rounded-lg border"
|
||||
dataSource={deployRecords}
|
||||
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 className="text-center py-12">
|
||||
<Empty description="暂无部署记录" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1383,14 +1421,7 @@ function ProjectDetailPage() {
|
||||
style={{ fontFamily: 'Monaco, Consolas, monospace' }}
|
||||
/>
|
||||
</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>
|
||||
</Modal>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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<APIResponse<Project>>({
|
||||
const { data } = await net.request<Project>({
|
||||
url: `/api/projects/${id}`,
|
||||
});
|
||||
return data;
|
||||
@@ -19,28 +19,32 @@ class DetailService {
|
||||
|
||||
// 获取项目的所有流水线
|
||||
async getPipelines(projectId: number) {
|
||||
const { data } = await net.request<APIResponse<Pipeline[]>>({
|
||||
const { data } = await net.request<Pipeline[] | { list: Pipeline[] }>({
|
||||
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<any>({
|
||||
url: `/api/deployments?projectId=${projectId}`,
|
||||
async getDeployments(
|
||||
projectId: number,
|
||||
page: number = 1,
|
||||
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'
|
||||
>,
|
||||
) {
|
||||
const { data } = await net.request<APIResponse<Pipeline>>({
|
||||
const { data } = await net.request<Pipeline>({
|
||||
url: '/api/pipelines',
|
||||
method: 'POST',
|
||||
data: pipeline,
|
||||
@@ -71,7 +75,7 @@ class DetailService {
|
||||
name: string,
|
||||
description?: string,
|
||||
) {
|
||||
const { data } = await net.request<APIResponse<Pipeline>>({
|
||||
const { data } = await net.request<Pipeline>({
|
||||
url: '/api/pipelines/from-template',
|
||||
method: 'POST',
|
||||
data: {
|
||||
@@ -100,7 +104,7 @@ class DetailService {
|
||||
>
|
||||
>,
|
||||
) {
|
||||
const { data } = await net.request<APIResponse<Pipeline>>({
|
||||
const { data } = await net.request<Pipeline>({
|
||||
url: `/api/pipelines/${id}`,
|
||||
method: 'PUT',
|
||||
data: pipeline,
|
||||
@@ -110,7 +114,7 @@ class DetailService {
|
||||
|
||||
// 删除流水线
|
||||
async deletePipeline(id: number) {
|
||||
const { data } = await net.request<APIResponse<null>>({
|
||||
const { data } = await net.request<null>({
|
||||
url: `/api/pipelines/${id}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
@@ -119,10 +123,10 @@ class DetailService {
|
||||
|
||||
// 获取流水线的所有步骤
|
||||
async getSteps(pipelineId: number) {
|
||||
const { data } = await net.request<APIResponse<Step[]>>({
|
||||
const { data } = await net.request<Step[] | { list: Step[] }>({
|
||||
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<APIResponse<Step>>({
|
||||
const { data } = await net.request<Step>({
|
||||
url: '/api/steps',
|
||||
method: 'POST',
|
||||
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}`,
|
||||
method: 'PUT',
|
||||
data: step,
|
||||
@@ -161,7 +165,7 @@ class DetailService {
|
||||
// 删除步骤
|
||||
async deleteStep(id: number) {
|
||||
// DELETE请求返回204状态码,通过拦截器处理为成功响应
|
||||
const { data } = await net.request<APIResponse<null>>({
|
||||
const { data } = await net.request<null>({
|
||||
url: `/api/steps/${id}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
@@ -170,23 +174,23 @@ class DetailService {
|
||||
|
||||
// 获取项目的提交记录
|
||||
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}` : ''}`,
|
||||
});
|
||||
return data;
|
||||
return Array.isArray(data) ? data : data.list;
|
||||
}
|
||||
|
||||
// 获取项目的分支列表
|
||||
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}`,
|
||||
});
|
||||
return data;
|
||||
return Array.isArray(data) ? data : data.list;
|
||||
}
|
||||
|
||||
// 创建部署
|
||||
async createDeployment(deployment: CreateDeploymentRequest) {
|
||||
const { data } = await net.request<APIResponse<Deployment>>({
|
||||
const { data } = await net.request<Deployment>({
|
||||
url: '/api/deployments',
|
||||
method: 'POST',
|
||||
data: deployment,
|
||||
@@ -196,7 +200,7 @@ class DetailService {
|
||||
|
||||
// 重新执行部署
|
||||
async retryDeployment(deploymentId: number) {
|
||||
const { data } = await net.request<APIResponse<Deployment>>({
|
||||
const { data } = await net.request<Deployment>({
|
||||
url: `/api/deployments/${deploymentId}/retry`,
|
||||
method: 'POST',
|
||||
});
|
||||
@@ -205,7 +209,7 @@ class DetailService {
|
||||
|
||||
// 获取项目详情(包含工作目录状态)
|
||||
async getProjectDetail(id: number) {
|
||||
const { data } = await net.request<APIResponse<Project>>({
|
||||
const { data } = await net.request<Project>({
|
||||
url: `/api/projects/${id}`,
|
||||
});
|
||||
return data;
|
||||
@@ -213,7 +217,7 @@ class DetailService {
|
||||
|
||||
// 更新项目
|
||||
async updateProject(id: number, project: Partial<Project>) {
|
||||
const { data } = await net.request<APIResponse<Project>>({
|
||||
const { data } = await net.request<Project>({
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ function ProjectPage() {
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
const response = await projectService.list();
|
||||
setProjects(response.data);
|
||||
setProjects(response.list);
|
||||
}, []);
|
||||
|
||||
const handleCreateProject = () => {
|
||||
|
||||
@@ -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<APIResponse<ProjectListResponse>>({
|
||||
const { data } = await net.request<Project[] | ProjectListResponse>({
|
||||
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<APIResponse<Project>>({
|
||||
const { data } = await net.request<Project>({
|
||||
method: 'GET',
|
||||
url: `/api/projects/${id}`,
|
||||
});
|
||||
@@ -24,7 +26,7 @@ class ProjectService {
|
||||
description?: string;
|
||||
repository: string;
|
||||
}) {
|
||||
const { data } = await net.request<APIResponse<Project>>({
|
||||
const { data } = await net.request<Project>({
|
||||
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<APIResponse<Project>>({
|
||||
const { data } = await net.request<Project>({
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user