feat: project list
This commit is contained in:
@@ -20,7 +20,7 @@ export class AuthController {
|
||||
if (!ctx.session.isNew) {
|
||||
return ctx.session.user;
|
||||
}
|
||||
const { code } = (ctx.request as any).body;
|
||||
const { code } = ctx.request.body as LoginRequestBody;
|
||||
const { access_token } = await gitea.getToken(code);
|
||||
const giteaUser = await gitea.getUserInfo(access_token);
|
||||
log.debug(this.TAG, 'gitea user: %o', giteaUser);
|
||||
@@ -64,3 +64,7 @@ export class AuthController {
|
||||
return ctx.session.user;
|
||||
}
|
||||
}
|
||||
|
||||
interface LoginRequestBody {
|
||||
code: string;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import { log } from '../libs/logger.ts';
|
||||
import { BusinessError } from '../middlewares/exception.ts';
|
||||
import { Controller, Get } from '../decorators/route.ts';
|
||||
|
||||
@Controller('/application')
|
||||
export class ApplicationController {
|
||||
@Controller('/project')
|
||||
export class ProjectController {
|
||||
@Get('/list')
|
||||
async list(ctx: Context) {
|
||||
log.debug('app', 'session %o', ctx.session);
|
||||
@@ -1,3 +1,4 @@
|
||||
import bodyParser from 'koa-bodyparser';
|
||||
import type Koa from 'koa';
|
||||
import type { Middleware } from './types.ts';
|
||||
|
||||
@@ -6,41 +7,6 @@ import type { Middleware } from './types.ts';
|
||||
*/
|
||||
export class BodyParser implements Middleware {
|
||||
apply(app: Koa): void {
|
||||
// 使用动态导入来避免类型问题
|
||||
app.use(async (ctx, next) => {
|
||||
if (ctx.request.method === 'POST' ||
|
||||
ctx.request.method === 'PUT' ||
|
||||
ctx.request.method === 'PATCH') {
|
||||
|
||||
// 简单的JSON解析
|
||||
if (ctx.request.type === 'application/json') {
|
||||
try {
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
ctx.req.on('data', (chunk) => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
|
||||
await new Promise((resolve) => {
|
||||
ctx.req.on('end', () => {
|
||||
const body = Buffer.concat(chunks).toString();
|
||||
try {
|
||||
(ctx.request as any).body = JSON.parse(body);
|
||||
} catch {
|
||||
(ctx.request as any).body = {};
|
||||
}
|
||||
resolve(void 0);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
(ctx.request as any).body = {};
|
||||
}
|
||||
} else {
|
||||
(ctx.request as any).body = {};
|
||||
}
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
app.use(bodyParser());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import KoaRouter from '@koa/router';
|
||||
import type Koa from 'koa';
|
||||
import type { Middleware } from './types.ts';
|
||||
import { RouteScanner } from '../libs/route-scanner.ts';
|
||||
import { ApplicationController } from '../controllers/application.ts';
|
||||
import { ProjectController } from '../controllers/project.ts';
|
||||
import { UserController } from '../controllers/user.ts';
|
||||
import { AuthController } from '../controllers/auth.ts';
|
||||
import { log } from '../libs/logger.ts';
|
||||
@@ -33,7 +33,7 @@ export class Router implements Middleware {
|
||||
private registerDecoratorRoutes(): void {
|
||||
// 注册所有使用装饰器的控制器
|
||||
this.routeScanner.registerControllers([
|
||||
ApplicationController,
|
||||
ProjectController,
|
||||
UserController,
|
||||
AuthController,
|
||||
]);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@koa/router": "^14.0.0",
|
||||
"@prisma/client": "^6.15.0",
|
||||
"koa": "^3.0.1",
|
||||
"koa-bodyparser": "^4.4.1",
|
||||
"koa-session": "^7.0.2",
|
||||
"pino": "^9.9.1",
|
||||
"pino-pretty": "^13.1.1"
|
||||
@@ -22,6 +23,7 @@
|
||||
"@tsconfig/node-ts": "^23.6.1",
|
||||
"@tsconfig/node22": "^22.0.2",
|
||||
"@types/koa": "^3.0.0",
|
||||
"@types/koa-bodyparser": "^4.3.12",
|
||||
"@types/koa__cors": "^5.0.0",
|
||||
"@types/koa__router": "^12.0.4",
|
||||
"@types/node": "^24.3.0",
|
||||
|
||||
10
apps/web/src/hooks/useAsyncEffect.ts
Normal file
10
apps/web/src/hooks/useAsyncEffect.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
export function useAsyncEffect(
|
||||
effect: () => Promise<void>,
|
||||
deps: React.DependencyList,
|
||||
) {
|
||||
useEffect(() => {
|
||||
effect();
|
||||
}, [...deps]);
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
import { Avatar, Layout, Menu } from '@arco-design/web-react';
|
||||
import {
|
||||
IconApps,
|
||||
IconBulb,
|
||||
IconFire,
|
||||
IconMenuFold,
|
||||
IconMenuUnfold,
|
||||
IconRobot,
|
||||
IconSafe,
|
||||
IconUser,
|
||||
} from '@arco-design/web-react/icon';
|
||||
import { useState } from 'react';
|
||||
@@ -21,9 +18,15 @@ function Home() {
|
||||
<Layout.Sider
|
||||
collapsible
|
||||
onCollapse={setCollapsed}
|
||||
trigger={collapsed ? <IconMenuUnfold /> : <IconMenuFold />}
|
||||
trigger={
|
||||
collapsed ? (
|
||||
<IconMenuUnfold fontSize={16} />
|
||||
) : (
|
||||
<IconMenuFold fontSize={16} />
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-center px-2 py-3">
|
||||
<div className="flex flex-row items-center justify-center h-[56px]">
|
||||
<Logo />
|
||||
{!collapsed && <h2 className="ml-4 text-xl font-medium">Foka CI</h2>}
|
||||
</div>
|
||||
@@ -31,16 +34,17 @@ function Home() {
|
||||
className="flex-1"
|
||||
defaultOpenKeys={['0']}
|
||||
defaultSelectedKeys={['0_1']}
|
||||
collapse={collapsed}
|
||||
>
|
||||
<Menu.Item key="0">
|
||||
<Link to="/project" className="flex flex-row items-center">
|
||||
<IconApps fontSize={18} />
|
||||
项目管理
|
||||
<Link to="/project">
|
||||
<IconApps fontSize={16} />
|
||||
<span>项目管理</span>
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="1">
|
||||
<Link to="/env" className="flex flex-row items-center">
|
||||
<IconRobot fontSize={18} />
|
||||
<Link to="/env">
|
||||
<IconRobot fontSize={16} />
|
||||
环境管理
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
|
||||
@@ -1,8 +1,114 @@
|
||||
import { Card, Grid, Link, Tag, Avatar, Space, Typography, Button } from '@arco-design/web-react';
|
||||
import { IconBranch, IconCalendar, IconEye } from '@arco-design/web-react/icon';
|
||||
import { useState } from 'react';
|
||||
import type { Project } from './types';
|
||||
import { useAsyncEffect } from '../../hooks/useAsyncEffect';
|
||||
import { projectService } from './service';
|
||||
import IconGitea from '@assets/images/gitea.svg?react'
|
||||
|
||||
function Project() {
|
||||
const [projects, setProjects] = useState([]);
|
||||
return <div>project page</div>;
|
||||
const { Text, Paragraph } = Typography;
|
||||
|
||||
function ProjectPage() {
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
const list = await projectService.list();
|
||||
setProjects(list);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-gray-100 min-h-screen">
|
||||
<div className="mb-6">
|
||||
<Typography.Title heading={2} className="!m-0 !text-gray-900">
|
||||
我的项目
|
||||
</Typography.Title>
|
||||
<Text type="secondary">管理和查看您的所有项目</Text>
|
||||
</div>
|
||||
|
||||
<Grid.Row gutter={[16, 16]}>
|
||||
{projects.map((project) => (
|
||||
<Grid.Col key={project.id} span={8}>
|
||||
<Card
|
||||
className="foka-card !rounded-xl border border-gray-200 h-[280px] hover:border-blue-200"
|
||||
hoverable
|
||||
bodyStyle={{ padding: '20px' }}
|
||||
>
|
||||
{/* 项目头部 */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Avatar
|
||||
size={40}
|
||||
className="bg-blue-600 text-white text-base font-semibold"
|
||||
>
|
||||
{project.name.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<div className="ml-3">
|
||||
<Typography.Title
|
||||
heading={5}
|
||||
className="!m-0 !text-base !font-semibold"
|
||||
>
|
||||
{project.name}
|
||||
</Typography.Title>
|
||||
<Text type="secondary" className="text-xs">
|
||||
更新于 2天前
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<Tag color="blue" size="small">
|
||||
活跃
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
{/* 项目描述 */}
|
||||
<Paragraph
|
||||
className="!m-0 !mb-4 !text-gray-600 !text-sm !leading-6 h-[42px] overflow-hidden line-clamp-2"
|
||||
>
|
||||
{project.description || '暂无描述'}
|
||||
</Paragraph>
|
||||
|
||||
{/* 项目信息 */}
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 flex items-center">
|
||||
<IconGitea className="mr-1.5 w-4" />
|
||||
<Text
|
||||
type="secondary"
|
||||
className="text-xs"
|
||||
>
|
||||
{project.repository}
|
||||
</Text>
|
||||
</div>
|
||||
<Space size={16}>
|
||||
<div className="flex items-center">
|
||||
<IconBranch className="mr-1 text-gray-500 text-xs" />
|
||||
<Text className="text-xs text-gray-500">main</Text>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<IconCalendar className="mr-1 text-gray-500 text-xs" />
|
||||
<Text className="text-xs text-gray-500">3个提交</Text>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex items-center justify-between pt-3 border-t border-gray-100">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<IconEye />}
|
||||
className="text-gray-500"
|
||||
>
|
||||
查看详情
|
||||
</Button>
|
||||
<Link className="text-xs font-medium">
|
||||
管理项目 →
|
||||
</Link>
|
||||
</div>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
))}
|
||||
</Grid.Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Project;
|
||||
export default ProjectPage;
|
||||
|
||||
16
apps/web/src/pages/project/service.ts
Normal file
16
apps/web/src/pages/project/service.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { net, type APIResponse } from "@shared";
|
||||
import type { Project } from "./types";
|
||||
|
||||
|
||||
class ProjectService {
|
||||
|
||||
async list() {
|
||||
const { data } = await net.request<APIResponse<Project[]>>({
|
||||
method: 'GET',
|
||||
url: '/api/project/list',
|
||||
})
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export const projectService = new ProjectService();
|
||||
@@ -8,7 +8,7 @@ export interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
git: string;
|
||||
repository: string;
|
||||
env: Record<string, string>;
|
||||
createdAt: string;
|
||||
status: BuildStatus;
|
||||
|
||||
Reference in New Issue
Block a user