feat(project): add workspace directory configuration and management
- Add projectDir field to Project model for workspace directory management - Implement workspace directory creation, validation and Git initialization - Add workspace status query endpoint with directory info and Git status - Create GitManager for Git repository operations (clone, branch, commit info) - Add PathValidator for secure path validation and traversal attack prevention - Implement execution queue with concurrency control for build tasks - Refactor project list UI to remove edit/delete actions from cards - Add project settings tab in detail page with edit/delete functionality - Add icons to all tabs (History, Code, Settings) - Implement time formatting with dayjs in YYYY-MM-DD HH:mm:ss format - Display all timestamps using browser's local timezone - Update PipelineRunner to use workspace directory for command execution - Add workspace status card showing directory path, size, Git info - Enhance CreateProjectModal with repository URL validation
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"axios": "^1.11.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router": "^7.8.0",
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Modal,
|
||||
Select,
|
||||
} from '@arco-design/web-react';
|
||||
import { formatDateTime } from '../../../../utils/time';
|
||||
import { IconDelete, IconPlus } from '@arco-design/web-react/icon';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { Branch, Commit, Pipeline } from '../../types';
|
||||
@@ -182,7 +183,7 @@ function DeployModal({
|
||||
{commit.sha.substring(0, 7)}
|
||||
</span>
|
||||
<span className="text-gray-500 text-xs">
|
||||
{new Date(commit.commit.author.date).toLocaleString()}
|
||||
{formatDateTime(commit.commit.author.date)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-gray-600 text-sm truncate">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { List, Space, Tag } from '@arco-design/web-react';
|
||||
import { formatDateTime } from '../../../../utils/time';
|
||||
import type { Deployment } from '../../types';
|
||||
|
||||
interface DeployRecordItemProps {
|
||||
@@ -76,7 +77,7 @@ function DeployRecordItem({
|
||||
<span className="text-sm text-gray-500">
|
||||
执行时间:{' '}
|
||||
<span className="font-medium text-gray-700">
|
||||
{item.createdAt}
|
||||
{formatDateTime(item.createdAt)}
|
||||
</span>
|
||||
</span>
|
||||
</Space>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Descriptions,
|
||||
Dropdown,
|
||||
Empty,
|
||||
Form,
|
||||
@@ -10,19 +11,24 @@ import {
|
||||
Message,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
} from '@arco-design/web-react';
|
||||
import {
|
||||
IconCode,
|
||||
IconCopy,
|
||||
IconDelete,
|
||||
IconEdit,
|
||||
IconFolder,
|
||||
IconHistory,
|
||||
IconMore,
|
||||
IconPlayArrow,
|
||||
IconPlus,
|
||||
IconRefresh,
|
||||
IconSettings,
|
||||
} from '@arco-design/web-react/icon';
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import {
|
||||
@@ -40,9 +46,10 @@ import {
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
||||
import type { Deployment, Pipeline, Project, Step } from '../types';
|
||||
import { formatDateTime } from '../../../utils/time';
|
||||
import type { Deployment, Pipeline, Project, Step, WorkspaceDirStatus, WorkspaceStatus } from '../types';
|
||||
import DeployModal from './components/DeployModal';
|
||||
import DeployRecordItem from './components/DeployRecordItem';
|
||||
import PipelineStepItem from './components/PipelineStepItem';
|
||||
@@ -59,6 +66,7 @@ interface PipelineWithEnabled extends Pipeline {
|
||||
|
||||
function ProjectDetailPage() {
|
||||
const [detail, setDetail] = useState<Project | null>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 拖拽传感器配置
|
||||
const sensors = useSensors(
|
||||
@@ -87,6 +95,10 @@ function ProjectDetailPage() {
|
||||
const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(null);
|
||||
const [templates, setTemplates] = useState<Array<{id: number, name: string, description: string}>>([]);
|
||||
|
||||
// 项目设置相关状态
|
||||
const [projectEditModalVisible, setProjectEditModalVisible] = useState(false);
|
||||
const [projectForm] = Form.useForm();
|
||||
|
||||
const { id } = useParams();
|
||||
|
||||
// 获取可用的流水线模板
|
||||
@@ -580,6 +592,56 @@ function ProjectDetailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 项目设置相关函数
|
||||
const handleEditProject = () => {
|
||||
if (detail) {
|
||||
projectForm.setFieldsValue({
|
||||
name: detail.name,
|
||||
description: detail.description,
|
||||
repository: detail.repository,
|
||||
});
|
||||
setProjectEditModalVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleProjectEditSuccess = async () => {
|
||||
try {
|
||||
const values = await projectForm.validate();
|
||||
await detailService.updateProject(Number(id), values);
|
||||
Message.success('项目更新成功');
|
||||
setProjectEditModalVisible(false);
|
||||
|
||||
// 刷新项目详情
|
||||
if (id) {
|
||||
const projectDetail = await detailService.getProjectDetail(Number(id));
|
||||
setDetail(projectDetail);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新项目失败:', error);
|
||||
Message.error('更新项目失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteProject = () => {
|
||||
Modal.confirm({
|
||||
title: '删除项目',
|
||||
content: `确定要删除项目 "${detail?.name}" 吗?删除后将无法恢复。`,
|
||||
okButtonProps: {
|
||||
status: 'danger',
|
||||
},
|
||||
onOk: async () => {
|
||||
try {
|
||||
await detailService.deleteProject(Number(id));
|
||||
Message.success('项目删除成功');
|
||||
navigate('/project');
|
||||
} catch (error) {
|
||||
console.error('删除项目失败:', error);
|
||||
Message.error('删除项目失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const selectedRecord = deployRecords.find(
|
||||
(record) => record.id === selectedRecordId,
|
||||
);
|
||||
@@ -613,6 +675,74 @@ function ProjectDetailPage() {
|
||||
(pipeline) => pipeline.id === selectedPipelineId,
|
||||
);
|
||||
|
||||
// 格式化文件大小
|
||||
const formatSize = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
// 获取工作目录状态标签
|
||||
const getWorkspaceStatusTag = (status: string): { text: string; color: string } => {
|
||||
const statusMap: Record<string, { text: string; color: string }> = {
|
||||
not_created: { text: '未创建', color: 'gray' },
|
||||
empty: { text: '空目录', color: 'orange' },
|
||||
no_git: { text: '无Git仓库', color: 'orange' },
|
||||
ready: { text: '就绪', color: 'green' },
|
||||
};
|
||||
return statusMap[status] || { text: '未知', color: 'gray' };
|
||||
};
|
||||
|
||||
// 渲染工作目录状态卡片
|
||||
const renderWorkspaceStatus = () => {
|
||||
if (!detail?.workspaceStatus) return null;
|
||||
|
||||
const { workspaceStatus } = detail;
|
||||
const statusInfo = getWorkspaceStatusTag(workspaceStatus.status as string);
|
||||
|
||||
return (
|
||||
<Card className="mb-6" title={<Space><IconFolder />工作目录状态</Space>}>
|
||||
<Descriptions
|
||||
column={2}
|
||||
data={[
|
||||
{
|
||||
label: '目录路径',
|
||||
value: detail.projectDir,
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
value: <Tag color={statusInfo.color}>{statusInfo.text}</Tag>,
|
||||
},
|
||||
{
|
||||
label: '目录大小',
|
||||
value: workspaceStatus.size ? formatSize(workspaceStatus.size) : '-',
|
||||
},
|
||||
{
|
||||
label: '当前分支',
|
||||
value: workspaceStatus.gitInfo?.branch || '-',
|
||||
},
|
||||
{
|
||||
label: '最后提交',
|
||||
value: workspaceStatus.gitInfo?.lastCommit ? (
|
||||
<Space direction="vertical" size="mini">
|
||||
<Typography.Text code>{workspaceStatus.gitInfo.lastCommit}</Typography.Text>
|
||||
<Typography.Text type="secondary">{workspaceStatus.gitInfo.lastCommitMessage}</Typography.Text>
|
||||
</Space>
|
||||
) : '-',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{workspaceStatus.error && (
|
||||
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded">
|
||||
<Typography.Text type="danger">{workspaceStatus.error}</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 flex flex-col h-full">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
@@ -626,13 +756,14 @@ function ProjectDetailPage() {
|
||||
部署
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-md flex-1 flex flex-col overflow-hidden">
|
||||
<Tabs
|
||||
type="line"
|
||||
size="large"
|
||||
className="h-full flex flex-col [&>.arco-tabs-content]:flex-1 [&>.arco-tabs-content]:overflow-hidden [&>.arco-tabs-content_.arco-tabs-content-inner]:h-full [&>.arco-tabs-pane]:h-full"
|
||||
>
|
||||
<Tabs.TabPane title="部署记录" key="deployRecords">
|
||||
<Tabs.TabPane title={<Space><IconHistory />部署记录</Space>} key="deployRecords">
|
||||
<div className="grid grid-cols-5 gap-6 h-full">
|
||||
{/* 左侧部署记录列表 */}
|
||||
<div className="col-span-2 space-y-4 h-full flex flex-col">
|
||||
@@ -671,7 +802,7 @@ function ProjectDetailPage() {
|
||||
{selectedRecord && (
|
||||
<Typography.Text type="secondary" className="text-sm">
|
||||
{selectedRecord.branch} · {selectedRecord.env} ·{' '}
|
||||
{selectedRecord.createdAt}
|
||||
{formatDateTime(selectedRecord.createdAt)}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
@@ -707,7 +838,7 @@ function ProjectDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane title="流水线" key="pipeline">
|
||||
<Tabs.TabPane title={<Space><IconCode />流水线</Space>} key="pipeline">
|
||||
<div className="grid grid-cols-5 gap-6 h-full">
|
||||
{/* 左侧流水线列表 */}
|
||||
<div className="col-span-2 space-y-4">
|
||||
@@ -823,7 +954,7 @@ function ProjectDetailPage() {
|
||||
<span>
|
||||
{pipeline.steps?.length || 0} 个步骤
|
||||
</span>
|
||||
<span>{pipeline.updatedAt}</span>
|
||||
<span>{formatDateTime(pipeline.updatedAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -913,6 +1044,50 @@ function ProjectDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
|
||||
{/* 项目设置标签页 */}
|
||||
<Tabs.TabPane key="settings" title={<Space><IconSettings />项目设置</Space>}>
|
||||
<div className="p-6">
|
||||
<Card title="项目信息" className="mb-4">
|
||||
<Descriptions
|
||||
column={1}
|
||||
data={[
|
||||
{
|
||||
label: '项目名称',
|
||||
value: detail?.name,
|
||||
},
|
||||
{
|
||||
label: '项目描述',
|
||||
value: detail?.description || '-',
|
||||
},
|
||||
{
|
||||
label: 'Git 仓库',
|
||||
value: detail?.repository,
|
||||
},
|
||||
{
|
||||
label: '工作目录',
|
||||
value: detail?.projectDir || '-',
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
value: formatDateTime(detail?.createdAt),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<Button type="primary" onClick={handleEditProject}>
|
||||
编辑项目
|
||||
</Button>
|
||||
<Button status="danger" onClick={handleDeleteProject}>
|
||||
删除项目
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 工作目录状态 */}
|
||||
{renderWorkspaceStatus()}
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@@ -1053,6 +1228,47 @@ function ProjectDetailPage() {
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 编辑项目模态框 */}
|
||||
<Modal
|
||||
title="编辑项目"
|
||||
visible={projectEditModalVisible}
|
||||
onOk={handleProjectEditSuccess}
|
||||
onCancel={() => setProjectEditModalVisible(false)}
|
||||
style={{ width: 500 }}
|
||||
>
|
||||
<Form form={projectForm} layout="vertical">
|
||||
<Form.Item
|
||||
field="name"
|
||||
label="项目名称"
|
||||
rules={[
|
||||
{ required: true, message: '请输入项目名称' },
|
||||
{ minLength: 2, message: '项目名称至少2个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="例如:我的应用" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
field="description"
|
||||
label="项目描述"
|
||||
rules={[{ maxLength: 200, message: '描述不能超过200个字符' }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="请输入项目描述"
|
||||
rows={3}
|
||||
maxLength={200}
|
||||
showWordLimit
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
field="repository"
|
||||
label="Git 仓库地址"
|
||||
rules={[{ required: true, message: '请输入仓库地址' }]}
|
||||
>
|
||||
<Input placeholder="例如:https://github.com/user/repo.git" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<DeployModal
|
||||
visible={deployModalVisible}
|
||||
onCancel={() => setDeployModalVisible(false)}
|
||||
|
||||
@@ -192,6 +192,35 @@ class DetailService {
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// 获取项目详情(包含工作目录状态)
|
||||
async getProjectDetail(id: number) {
|
||||
const { data } = await net.request<APIResponse<Project>>({
|
||||
url: `/api/projects/${id}`,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// 更新项目
|
||||
async updateProject(
|
||||
id: number,
|
||||
project: Partial<{ name: string; description: string; repository: string }>,
|
||||
) {
|
||||
const { data } = await net.request<APIResponse<Project>>({
|
||||
url: `/api/projects/${id}`,
|
||||
method: 'PUT',
|
||||
data: project,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
// 删除项目
|
||||
async deleteProject(id: number) {
|
||||
await net.request({
|
||||
url: `/api/projects/${id}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const detailService = new DetailService();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, Form, Input, Message, Modal } from '@arco-design/web-react';
|
||||
import { useState } from 'react';
|
||||
import { projectService } from '../service';
|
||||
import type { Project } from '../types';
|
||||
import type { Project } from '../../types';
|
||||
|
||||
interface CreateProjectModalProps {
|
||||
visible: boolean;
|
||||
@@ -97,6 +97,33 @@ function CreateProjectModal({
|
||||
>
|
||||
<Input placeholder="请输入仓库地址,如: https://github.com/user/repo" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="工作目录路径"
|
||||
field="projectDir"
|
||||
rules={[
|
||||
{ required: true, message: '请输入工作目录路径' },
|
||||
{
|
||||
validator: (value, cb) => {
|
||||
if (!value) {
|
||||
return cb('工作目录路径不能为空');
|
||||
}
|
||||
if (!value.startsWith('/')) {
|
||||
return cb('工作目录路径必须是绝对路径(以 / 开头)');
|
||||
}
|
||||
if (value.includes('..') || value.includes('~')) {
|
||||
return cb('不能包含路径遍历字符(.. 或 ~)');
|
||||
}
|
||||
if (/[<>:"|?*\x00-\x1f]/.test(value)) {
|
||||
return cb('路径包含非法字符');
|
||||
}
|
||||
cb();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入绝对路径,如: /data/projects/my-app" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
Dropdown,
|
||||
Menu,
|
||||
Modal,
|
||||
Space,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@arco-design/web-react';
|
||||
import {
|
||||
IconBranch,
|
||||
IconCalendar,
|
||||
IconCloud,
|
||||
IconDelete,
|
||||
IconEdit,
|
||||
IconMore,
|
||||
} from '@arco-design/web-react/icon';
|
||||
import IconGitea from '@assets/images/gitea.svg?react';
|
||||
import { useCallback } from 'react';
|
||||
@@ -27,27 +19,10 @@ const { Text, Paragraph } = Typography;
|
||||
|
||||
interface ProjectCardProps {
|
||||
project: Project;
|
||||
onEdit?: (project: Project) => void;
|
||||
onDelete?: (project: Project) => void;
|
||||
}
|
||||
|
||||
function ProjectCard({ project, onEdit, onDelete }: ProjectCardProps) {
|
||||
function ProjectCard({ project }: ProjectCardProps) {
|
||||
const navigate = useNavigate();
|
||||
// 处理删除操作
|
||||
const handleDelete = () => {
|
||||
Modal.confirm({
|
||||
title: '确认删除项目',
|
||||
content: `确定要删除项目 "${project.name}" 吗?此操作不可恢复。`,
|
||||
okText: '删除',
|
||||
cancelText: '取消',
|
||||
okButtonProps: {
|
||||
status: 'danger',
|
||||
},
|
||||
onOk: () => {
|
||||
onDelete?.(project);
|
||||
},
|
||||
});
|
||||
};
|
||||
// 获取环境信息
|
||||
const environments = [
|
||||
{ name: 'staging', color: 'orange', icon: '🚧' },
|
||||
@@ -109,37 +84,9 @@ function ProjectCard({ project, onEdit, onDelete }: ProjectCardProps) {
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Tag color="blue" size="small" className="font-medium">
|
||||
活跃
|
||||
</Tag>
|
||||
<Dropdown
|
||||
droplist={
|
||||
<Menu>
|
||||
<Menu.Item key="edit" onClick={() => onEdit?.(project)}>
|
||||
<IconEdit className="mr-2" />
|
||||
编辑
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="delete"
|
||||
onClick={() => handleDelete()}
|
||||
className="text-red-500"
|
||||
>
|
||||
<IconDelete className="mr-2" />
|
||||
删除
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
position="br"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<IconMore />}
|
||||
className="text-gray-400 hover:text-blue-500 hover:bg-blue-50 transition-all duration-200 p-1 rounded-md"
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<Tag color="blue" size="small" className="font-medium">
|
||||
活跃
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
{/* 项目描述 */}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useState } from 'react';
|
||||
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
||||
import type { Project } from '../types';
|
||||
import CreateProjectModal from './components/CreateProjectModal';
|
||||
import EditProjectModal from './components/EditProjectModal';
|
||||
import ProjectCard from './components/ProjectCard';
|
||||
import { projectService } from './service';
|
||||
|
||||
@@ -12,8 +11,6 @@ const { Text } = Typography;
|
||||
|
||||
function ProjectPage() {
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [editingProject, setEditingProject] = useState<Project | null>(null);
|
||||
const [createModalVisible, setCreateModalVisible] = useState(false);
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
@@ -21,22 +18,6 @@ function ProjectPage() {
|
||||
setProjects(response.data);
|
||||
}, []);
|
||||
|
||||
const handleEditProject = (project: Project) => {
|
||||
setEditingProject(project);
|
||||
setEditModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEditSuccess = (updatedProject: Project) => {
|
||||
setProjects((prev) =>
|
||||
prev.map((p) => (p.id === updatedProject.id ? updatedProject : p)),
|
||||
);
|
||||
};
|
||||
|
||||
const handleEditCancel = () => {
|
||||
setEditModalVisible(false);
|
||||
setEditingProject(null);
|
||||
};
|
||||
|
||||
const handleCreateProject = () => {
|
||||
setCreateModalVisible(true);
|
||||
};
|
||||
@@ -49,17 +30,6 @@ function ProjectPage() {
|
||||
setCreateModalVisible(false);
|
||||
};
|
||||
|
||||
const handleDeleteProject = async (project: Project) => {
|
||||
try {
|
||||
await projectService.delete(project.id);
|
||||
setProjects((prev) => prev.filter((p) => p.id !== project.id));
|
||||
Message.success('项目删除成功');
|
||||
} catch (error) {
|
||||
console.error('删除项目失败:', error);
|
||||
Message.error('删除项目失败,请稍后重试');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
@@ -82,22 +52,11 @@ function ProjectPage() {
|
||||
<Grid.Row gutter={[16, 16]}>
|
||||
{projects.map((project) => (
|
||||
<Grid.Col key={project.id} span={8}>
|
||||
<ProjectCard
|
||||
project={project}
|
||||
onEdit={handleEditProject}
|
||||
onDelete={handleDeleteProject}
|
||||
/>
|
||||
<ProjectCard project={project} />
|
||||
</Grid.Col>
|
||||
))}
|
||||
</Grid.Row>
|
||||
|
||||
<EditProjectModal
|
||||
visible={editModalVisible}
|
||||
project={editingProject}
|
||||
onCancel={handleEditCancel}
|
||||
onSuccess={handleEditSuccess}
|
||||
/>
|
||||
|
||||
<CreateProjectModal
|
||||
visible={createModalVisible}
|
||||
onCancel={handleCreateCancel}
|
||||
|
||||
@@ -4,17 +4,45 @@ enum BuildStatus {
|
||||
Stopped = 'Stopped',
|
||||
}
|
||||
|
||||
// 工作目录状态枚举
|
||||
export enum WorkspaceDirStatus {
|
||||
NOT_CREATED = 'not_created',
|
||||
EMPTY = 'empty',
|
||||
NO_GIT = 'no_git',
|
||||
READY = 'ready',
|
||||
}
|
||||
|
||||
// Git 仓库信息
|
||||
export interface GitInfo {
|
||||
branch?: string;
|
||||
lastCommit?: string;
|
||||
lastCommitMessage?: string;
|
||||
}
|
||||
|
||||
// 工作目录状态信息
|
||||
export interface WorkspaceStatus {
|
||||
status: WorkspaceDirStatus;
|
||||
exists: boolean;
|
||||
isEmpty?: boolean;
|
||||
hasGit?: boolean;
|
||||
size?: number;
|
||||
gitInfo?: GitInfo;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
repository: string;
|
||||
projectDir: string; // 项目工作目录路径(必填)
|
||||
valid: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdBy: string;
|
||||
updatedBy: string;
|
||||
status: BuildStatus;
|
||||
workspaceStatus?: WorkspaceStatus; // 工作目录状态信息
|
||||
}
|
||||
|
||||
// 流水线步骤类型定义
|
||||
|
||||
31
apps/web/src/utils/time.ts
Normal file
31
apps/web/src/utils/time.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
/**
|
||||
* 格式化时间为 YYYY-MM-DD HH:mm:ss
|
||||
* @param date 时间字符串或 Date 对象
|
||||
* @returns 格式化后的时间字符串
|
||||
*/
|
||||
export function formatDateTime(date: string | Date | undefined | null): string {
|
||||
if (!date) return '-';
|
||||
return dayjs(date).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间为 YYYY-MM-DD
|
||||
* @param date 时间字符串或 Date 对象
|
||||
* @returns 格式化后的日期字符串
|
||||
*/
|
||||
export function formatDate(date: string | Date | undefined | null): string {
|
||||
if (!date) return '-';
|
||||
return dayjs(date).format('YYYY-MM-DD');
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间为 HH:mm:ss
|
||||
* @param date 时间字符串或 Date 对象
|
||||
* @returns 格式化后的时间字符串
|
||||
*/
|
||||
export function formatTime(date: string | Date | undefined | null): string {
|
||||
if (!date) return '-';
|
||||
return dayjs(date).format('HH:mm:ss');
|
||||
}
|
||||
Reference in New Issue
Block a user