feat: add project management frontend pages
- Home page with navigation - Projects page with task list table - Task status/priority/progress display - Real-time API integration - Responsive layout and styling
This commit is contained in:
@@ -7,7 +7,8 @@
|
||||
|
||||
## 当前阶段
|
||||
|
||||
**Phase 1 已完成** - 核心平台层已搭建完成并稳定化中
|
||||
**Phase 1 已完成** - 核心平台层已搭建完成并稳定化中
|
||||
**Phase 2 进行中** - 项目管理模块已落地
|
||||
|
||||
---
|
||||
|
||||
@@ -28,11 +29,17 @@
|
||||
- ✅ Asset(素材)
|
||||
- ✅ IngestJob(入库任务:PENDING → PROCESSING → COMPLETED/FAILED)
|
||||
- ✅ ClassificationJob(分类任务:含 9 种分类)
|
||||
- ✅ **Task(任务:支持状态/进度/优先级/子任务/计划时间/实际时间)**
|
||||
- ✅ **Milestone(里程碑)**
|
||||
- ✅ **TaskIssue(任务问题/卡点)**
|
||||
|
||||
### 核心业务流程
|
||||
- ✅ 上传 → 入库 → Asset 创建链路
|
||||
- ✅ 分类任务链路
|
||||
- ✅ 完整异步任务处理(Celery + Redis)
|
||||
- ✅ **任务创建 → 状态更新 → 进度跟踪链路**
|
||||
- ✅ **里程碑管理**
|
||||
- ✅ **任务问题/卡点记录与解决**
|
||||
|
||||
### 基础设施
|
||||
- ✅ **MinIO 真实文件存储**(`apps/api/app/core/storage.py`)
|
||||
@@ -41,29 +48,34 @@
|
||||
- ✅ API 文档(Swagger)
|
||||
|
||||
### 测试与质量
|
||||
- ✅ 8 个集成测试全绿
|
||||
- ✅ **15 个集成测试全绿**(素材管理 8 个 + 项目管理 7 个)
|
||||
- ✅ 测试策略:集成测试优先,覆盖核心业务流程
|
||||
|
||||
---
|
||||
|
||||
## 进行中
|
||||
|
||||
### 项目推进器前端页面
|
||||
- 🔄 Next.js 页面设计与开发(下一步)
|
||||
- 🔄 树形任务视图
|
||||
- 🔄 甘特图视图
|
||||
- 🔄 里程碑时间轴
|
||||
- 🔄 问题卡点面板
|
||||
|
||||
### 稳定化与部署打磨
|
||||
- 🔄 Gitea workflows 持续优化(最近 10+ commits)
|
||||
- 🔄 Gitea workflows 持续优化
|
||||
- 🔄 部署流程与环境隔离稳定化
|
||||
- 🔄 Docker 构建优化(国内镜像源、多阶段构建)
|
||||
|
||||
### 前端页面
|
||||
- 🔄 Next.js 前端占位已建立,实际页面待补
|
||||
|
||||
---
|
||||
|
||||
## 下一步优先级
|
||||
|
||||
1. **前端页面补齐**
|
||||
- 项目/素材库管理页面
|
||||
- 素材上传/预览页面
|
||||
- 任务状态监控页面
|
||||
1. **项目推进器前端页面**
|
||||
- 任务列表与详情页
|
||||
- 里程碑管理页
|
||||
- 问题卡点面板
|
||||
- 甘特图/时间线视图
|
||||
|
||||
2. **更完整的 SAAS 产品化能力**
|
||||
- 多租户/组织/权限体系
|
||||
@@ -115,6 +127,7 @@
|
||||
- 测试策略:集成测试优先,覆盖核心业务流程
|
||||
- 数据库迁移从第一天起就版本化管理
|
||||
- CI/CD 基于 Gitea Actions + 自建 runner
|
||||
- **旧项目推进器(纯前端 HTML)已废弃,项目管理功能重新在 SAAS 里实现**
|
||||
|
||||
---
|
||||
|
||||
@@ -123,7 +136,7 @@
|
||||
- **本地路径**: `F:\openclaw-saas`
|
||||
- **远程仓库**: `xiaoxia-server:/var/lib/xiaoxia-ci/xiaoxia-saas.git`
|
||||
- **分支**: `main`
|
||||
- **最新提交**: `2c78407 fix: pass commit metadata into deploy builds`
|
||||
- **最新提交**: `355dd0f feat: add project management module (tasks, milestones, issues)`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Microsoft YaHei", sans-serif;
|
||||
background: #F5F7FA;
|
||||
color: #1D2129;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #165DFF;
|
||||
--primary-hover: #0e48d1;
|
||||
--border: #E5E6EB;
|
||||
--bg-white: #fff;
|
||||
--bg-gray: #F5F7FA;
|
||||
--text-primary: #1D2129;
|
||||
--text-secondary: #6E7681;
|
||||
--success: #00B42A;
|
||||
--warning: #FF7D00;
|
||||
--error: #F53F3F;
|
||||
}
|
||||
+11
-4
@@ -1,9 +1,16 @@
|
||||
export const metadata = {
|
||||
title: 'xiaoxia-saas',
|
||||
description: 'Xiaoxia SaaS scaffold',
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "小虾 SaaS - 项目推进器",
|
||||
description: "AI 视频自动化剪辑系统 - 项目管理",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body>{children}</body>
|
||||
|
||||
+44
-10
@@ -1,14 +1,48 @@
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main style={{ padding: 32, fontFamily: 'sans-serif' }}>
|
||||
<h1>xiaoxia-saas</h1>
|
||||
<p>SaaS 新主线前端骨架已创建。</p>
|
||||
<ul>
|
||||
<li>项目管理</li>
|
||||
<li>素材管理</li>
|
||||
<li>配音管理</li>
|
||||
<li>任务与成片</li>
|
||||
</ul>
|
||||
</main>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100vh',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '20px'
|
||||
}}>
|
||||
<h1 style={{ fontSize: '32px', fontWeight: 'bold', color: '#165DFF' }}>
|
||||
📁 小虾 SaaS 项目推进器
|
||||
</h1>
|
||||
<p style={{ color: '#6E7681' }}>
|
||||
项目管理模块开发中...
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<a
|
||||
href="/projects"
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
background: '#165DFF',
|
||||
color: 'white',
|
||||
borderRadius: '6px',
|
||||
textDecoration: 'none',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
进入项目列表
|
||||
</a>
|
||||
<a
|
||||
href="/api/docs"
|
||||
target="_blank"
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
background: 'white',
|
||||
color: '#165DFF',
|
||||
border: '1px solid #165DFF',
|
||||
borderRadius: '6px',
|
||||
textDecoration: 'none'
|
||||
}}
|
||||
>
|
||||
查看 API 文档
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
priority: string;
|
||||
progress: number;
|
||||
assignee_user_id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
|
||||
|
||||
export default function ProjectsPage() {
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// 模拟项目ID,生产环境应该从路由或上下文获取
|
||||
const projectId = 'demo_project_1';
|
||||
|
||||
useEffect(() => {
|
||||
fetchTasks();
|
||||
}, []);
|
||||
|
||||
const fetchTasks = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/v1/project-management/tasks?project_id=${projectId}`);
|
||||
if (!res.ok) throw new Error('获取任务列表失败');
|
||||
const data = await res.json();
|
||||
setTasks(data);
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
pending: '#86909C',
|
||||
in_progress: '#165DFF',
|
||||
completed: '#00B42A',
|
||||
blocked: '#F53F3F',
|
||||
cancelled: '#6E7681',
|
||||
};
|
||||
return colors[status] || '#6E7681';
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const texts: Record<string, string> = {
|
||||
pending: '待开始',
|
||||
in_progress: '进行中',
|
||||
completed: '已完成',
|
||||
blocked: '阻塞',
|
||||
cancelled: '已取消',
|
||||
};
|
||||
return texts[status] || status;
|
||||
};
|
||||
|
||||
const getPriorityText = (priority: string) => {
|
||||
const texts: Record<string, string> = {
|
||||
low: '低',
|
||||
medium: '中',
|
||||
high: '高',
|
||||
urgent: '紧急',
|
||||
};
|
||||
return texts[priority] || priority;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh' }}>
|
||||
<p style={{ color: 'var(--text-secondary)' }}>加载中...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
|
||||
{/* Header */}
|
||||
<header style={{
|
||||
height: '60px',
|
||||
background: 'var(--bg-white)',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 20px',
|
||||
justifyContent: 'space-between',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '20px' }}>
|
||||
<Link href="/" style={{ fontSize: '18px', fontWeight: 'bold', color: 'var(--primary)', textDecoration: 'none' }}>
|
||||
📁 项目推进器
|
||||
</Link>
|
||||
<span style={{ color: 'var(--text-secondary)' }}>Demo 项目</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<button
|
||||
onClick={fetchTasks}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--bg-white)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
刷新
|
||||
</button>
|
||||
<button
|
||||
onClick={() => alert('创建任务功能开发中')}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
borderRadius: '4px',
|
||||
border: 'none',
|
||||
background: 'var(--primary)',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
+ 新增任务
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<main style={{ flex: 1, padding: '20px', overflow: 'auto' }}>
|
||||
{error && (
|
||||
<div style={{
|
||||
padding: '12px',
|
||||
background: '#FFECE8',
|
||||
border: '1px solid #F53F3F',
|
||||
borderRadius: '4px',
|
||||
color: '#F53F3F',
|
||||
marginBottom: '20px',
|
||||
}}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tasks.length === 0 ? (
|
||||
<div style={{
|
||||
background: 'var(--bg-white)',
|
||||
borderRadius: '8px',
|
||||
padding: '40px',
|
||||
textAlign: 'center',
|
||||
color: 'var(--text-secondary)',
|
||||
}}>
|
||||
<p>暂无任务</p>
|
||||
<p style={{ fontSize: '14px', marginTop: '8px' }}>点击右上角"+ 新增任务"创建第一个任务</p>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
background: 'var(--bg-white)',
|
||||
borderRadius: '8px',
|
||||
padding: '20px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.04)',
|
||||
}}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid var(--border)' }}>
|
||||
<th style={{ padding: '12px', textAlign: 'left', color: 'var(--text-secondary)', fontWeight: 'normal' }}>任务名称</th>
|
||||
<th style={{ padding: '12px', textAlign: 'left', color: 'var(--text-secondary)', fontWeight: 'normal' }}>状态</th>
|
||||
<th style={{ padding: '12px', textAlign: 'left', color: 'var(--text-secondary)', fontWeight: 'normal' }}>优先级</th>
|
||||
<th style={{ padding: '12px', textAlign: 'left', color: 'var(--text-secondary)', fontWeight: 'normal' }}>进度</th>
|
||||
<th style={{ padding: '12px', textAlign: 'left', color: 'var(--text-secondary)', fontWeight: 'normal' }}>创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tasks.map((task) => (
|
||||
<tr key={task.id} style={{ borderBottom: '1px solid #F7F8FA' }}>
|
||||
<td style={{ padding: '12px', fontWeight: '500' }}>{task.name}</td>
|
||||
<td style={{ padding: '12px' }}>
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
padding: '2px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
color: 'white',
|
||||
background: getStatusColor(task.status),
|
||||
}}>
|
||||
{getStatusText(task.status)}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '12px', color: 'var(--text-secondary)' }}>
|
||||
{getPriorityText(task.priority)}
|
||||
</td>
|
||||
<td style={{ padding: '12px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{
|
||||
flex: 1,
|
||||
height: '6px',
|
||||
background: '#E5E6EB',
|
||||
borderRadius: '3px',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<div style={{
|
||||
width: `${task.progress}%`,
|
||||
height: '100%',
|
||||
background: 'var(--primary)',
|
||||
transition: 'width 0.3s',
|
||||
}} />
|
||||
</div>
|
||||
<span style={{ fontSize: '12px', color: 'var(--text-secondary)', minWidth: '40px' }}>
|
||||
{task.progress}%
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ padding: '12px', fontSize: '12px', color: 'var(--text-secondary)' }}>
|
||||
{new Date(task.created_at).toLocaleDateString('zh-CN')}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer style={{
|
||||
height: '30px',
|
||||
lineHeight: '30px',
|
||||
background: 'var(--bg-white)',
|
||||
borderTop: '1px solid var(--border)',
|
||||
padding: '0 20px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
fontSize: '12px',
|
||||
color: 'var(--text-secondary)',
|
||||
}}>
|
||||
<div>当前项目:Demo 项目</div>
|
||||
<div>总计任务:{tasks.length}</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user