From 8a0330e38773f30fdb133e2c8011054bbfcfa071 Mon Sep 17 00:00:00 2001 From: monster Date: Fri, 13 Mar 2026 12:37:29 +0800 Subject: [PATCH 01/25] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E6=A6=82=E8=A7=88=E9=A1=B5=EF=BC=8C=E6=95=B4=E5=90=88?= =?UTF-8?q?=E4=BF=A1=E6=81=AF/=E9=9C=80=E6=B1=82/=E4=BB=BB=E5=8A=A1=20Tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 /console/project/:projectId 路由,点击项目名称进入概览页 - 顶部展示 ProjectInfo,下方为信息/需求/任务三个 Tab - 信息 Tab:文件管理 + README 文档 - 需求 Tab:需求列表、创建、查看详情 - 任务 Tab:任务列表、滚动加载更多 - 三个 Tab 拆分为独立组件:info-tab、issues-tab、tasks-tab Made-with: Cursor --- frontend/src/App.tsx | 2 + .../components/console/nav/nav-project.tsx | 6 +- .../console/user/project/overview/index.tsx | 84 +++++++ .../user/project/overview/info-tab.tsx | 123 +++++++++++ .../user/project/overview/issues-tab.tsx | 86 ++++++++ .../user/project/overview/tasks-tab.tsx | 206 ++++++++++++++++++ 6 files changed, 504 insertions(+), 3 deletions(-) create mode 100644 frontend/src/pages/console/user/project/overview/index.tsx create mode 100644 frontend/src/pages/console/user/project/overview/info-tab.tsx create mode 100644 frontend/src/pages/console/user/project/overview/issues-tab.tsx create mode 100644 frontend/src/pages/console/user/project/overview/tasks-tab.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3038e643..e5292d01 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -28,6 +28,7 @@ import PlaygroundDetailPage from "./pages/playground-detail" import PublicTaskPage from "./pages/public-task" import PostCreatePage from "./pages/post-create" import ProjectIssuesPage from "./pages/console/user/project/issues" +import ProjectOverviewPage from "./pages/console/user/project/overview" import ProjectTasksPage from "./pages/console/user/project/tasks" import TaskDevelopPage from "./pages/console/user/task/task-dev" import TaskViewPage from "./pages/console/user/task/task-view" @@ -52,6 +53,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/console/nav/nav-project.tsx b/frontend/src/components/console/nav/nav-project.tsx index 4ec42e3d..bc2ba65e 100644 --- a/frontend/src/components/console/nav/nav-project.tsx +++ b/frontend/src/components/console/nav/nav-project.tsx @@ -58,12 +58,12 @@ export default function NavProject() { {projects.length > 0 ? projects.map((project) => ( - - {location.pathname.startsWith(`/console/project/${project.id}/`) ? : } + + {(location.pathname === `/console/project/${project.id}` || location.pathname.startsWith(`/console/project/${project.id}/`)) ? : } {project.name} - {location.pathname.startsWith(`/console/project/${project.id}/`) && + {(location.pathname === `/console/project/${project.id}` || location.pathname.startsWith(`/console/project/${project.id}/`)) && diff --git a/frontend/src/pages/console/user/project/overview/index.tsx b/frontend/src/pages/console/user/project/overview/index.tsx new file mode 100644 index 00000000..21865300 --- /dev/null +++ b/frontend/src/pages/console/user/project/overview/index.tsx @@ -0,0 +1,84 @@ +import { useEffect, useRef, useState } from "react" +import { useParams } from "react-router-dom" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import ProjectInfo from "@/components/console/project/project-info" +import { type DomainProject } from "@/api/Api" +import { apiRequest } from "@/utils/requestUtils" +import { toast } from "sonner" +import { isProjectRepoUnbound } from "@/utils/project" +import { + Empty, + EmptyDescription, + EmptyHeader, + EmptyMedia, + EmptyTitle, +} from "@/components/ui/empty" +import { IconAlertCircle } from "@tabler/icons-react" +import ProjectOverviewInfoTab from "./info-tab" +import ProjectOverviewIssuesTab from "./issues-tab" +import ProjectOverviewTasksTab from "./tasks-tab" + +export default function ProjectOverviewPage() { + const { projectId = "" } = useParams<{ projectId: string }>() + const projectIdRef = useRef(projectId) + projectIdRef.current = projectId + + const [project, setProject] = useState(undefined) + + const fetchProject = async () => { + const requestedId = projectId + await apiRequest("v1UsersProjectsDetail", {}, [requestedId], (resp) => { + if (projectIdRef.current !== requestedId) return + if (resp.code === 0) { + setProject(resp.data) + } else { + toast.error(resp.message || "获取项目失败") + } + }) + } + + useEffect(() => { + if (projectId) { + setProject(undefined) + fetchProject() + } + }, [projectId]) + + const isRepoUnbound = isProjectRepoUnbound(project) + + if (projectId && project && isRepoUnbound) { + return ( + + + + + + 项目异常 + 这个项目没有绑定仓库 + + + ) + } + + return ( +
+ + + + 信息 + 需求 + 任务 + + + + + + + + + + + +
+ ) +} diff --git a/frontend/src/pages/console/user/project/overview/info-tab.tsx b/frontend/src/pages/console/user/project/overview/info-tab.tsx new file mode 100644 index 00000000..7a096539 --- /dev/null +++ b/frontend/src/pages/console/user/project/overview/info-tab.tsx @@ -0,0 +1,123 @@ +import { useEffect, useRef, useState } from "react" +import { ProjectFileManager } from "@/components/console/project/files" +import { Markdown } from "@/components/common/markdown" +import { type DomainProject } from "@/api/Api" +import { apiRequest } from "@/utils/requestUtils" +import { b64decode } from "@/utils/common" +import { + Empty, + EmptyDescription, + EmptyHeader, + EmptyMedia, +} from "@/components/ui/empty" +import { Label } from "@/components/ui/label" +import { IconFileText, IconLoader } from "@tabler/icons-react" +import { cn } from "@/lib/utils" + +interface ProjectOverviewInfoTabProps { + projectId: string + project?: DomainProject +} + +export default function ProjectOverviewInfoTab({ projectId, project }: ProjectOverviewInfoTabProps) { + const projectIdRef = useRef(projectId) + projectIdRef.current = projectId + + const [readmeContent, setReadmeContent] = useState("") + const [readmePath, setReadmePath] = useState("") + const [readmeLoaded, setReadmeLoaded] = useState(false) + + useEffect(() => { + if (!project?.id) { + setReadmeContent("") + setReadmePath("") + setReadmeLoaded(true) + return + } + const requestedProjectId = project.id + setReadmeLoaded(false) + apiRequest("v1UsersProjectsTreeDetail", { recursive: false, path: "" }, [project.id], (resp) => { + if (projectIdRef.current !== requestedProjectId) return + if (resp.code !== 0 || !resp.data) { + setReadmeLoaded(true) + return + } + const readme = (resp.data as { name?: string; path?: string }[]).find( + (e) => e.name?.toLowerCase() === "readme.md" + ) + const readmePathVal = readme?.path + if (!readme || !readmePathVal) { + setReadmeContent("") + setReadmePath("") + setReadmeLoaded(true) + return + } + apiRequest("v1UsersProjectsTreeBlobDetail", { path: readmePathVal }, [project.id as string], (r) => { + if (projectIdRef.current !== requestedProjectId) return + if (r.code === 0 && r.data?.content) { + setReadmeContent(b64decode(r.data.content)) + setReadmePath(readmePathVal) + window.scrollTo(0, 0) + } + setReadmeLoaded(true) + }) + }) + }, [project?.id]) + + const ReadmeHeader = ( +
+ +
+ ) + + const renderReadme = () => { + if (!readmeLoaded) { + return ( +
+ {ReadmeHeader} + + + + + + 正在加载... + + +
+ ) + } + if (!readmeContent) { + return ( +
+ {ReadmeHeader} + + + + + + 暂无文档 + + +
+ ) + } + return ( +
+ {ReadmeHeader} +
+ {readmeContent} +
+
+ ) + } + + return ( +
+ + {renderReadme()} +
+ ) +} diff --git a/frontend/src/pages/console/user/project/overview/issues-tab.tsx b/frontend/src/pages/console/user/project/overview/issues-tab.tsx new file mode 100644 index 00000000..ea12bdf8 --- /dev/null +++ b/frontend/src/pages/console/user/project/overview/issues-tab.tsx @@ -0,0 +1,86 @@ +import { useCallback, useEffect, useState } from "react" +import { type DomainProject, type DomainProjectIssue } from "@/api/Api" +import { apiRequest } from "@/utils/requestUtils" +import { toast } from "sonner" +import { Button } from "@/components/ui/button" +import { IconPlus } from "@tabler/icons-react" +import ProjectIssueList from "@/components/console/project/issue-list" +import ViewIssueDialog from "@/components/console/project/issue-detail" +import CreateIssueDialog from "@/components/console/project/create-issue" + +interface ProjectOverviewIssuesTabProps { + projectId: string + project?: DomainProject +} + +export default function ProjectOverviewIssuesTab({ projectId, project }: ProjectOverviewIssuesTabProps) { + const [issues, setIssues] = useState([]) + const [isCreateIssueDialogOpen, setIsCreateIssueDialogOpen] = useState(false) + const [viewingIssue, setViewingIssue] = useState(undefined) + const [viewIssueDialogOpen, setViewIssueDialogOpen] = useState(false) + + const fetchProjectIssues = useCallback(async () => { + if (!projectId) return + await apiRequest("v1UsersProjectsIssuesDetail", {}, [projectId], (resp) => { + if (resp.code === 0) { + const rawIssues = resp.data?.issues || [] + const sorted = [...rawIssues].sort((a, b) => { + const getStatusOrder = (s?: string) => (s === "open" ? 1 : s === "completed" ? 2 : s === "closed" ? 3 : 4) + const d = getStatusOrder(a.status) - getStatusOrder(b.status) + if (d !== 0) return d + const pA = a.priority ?? 999 + const pB = b.priority ?? 999 + if (pA !== pB) return pA - pB + return (b.created_at ?? 0) - (a.created_at ?? 0) + }) + setIssues(sorted) + } else { + toast.error(resp.message || "获取项目需求失败") + } + }) + }, [projectId]) + + const handleViewIssue = (issue: DomainProjectIssue) => { + setViewingIssue(issue) + setViewIssueDialogOpen(true) + } + + useEffect(() => { + if (projectId) { + setIssues([]) + fetchProjectIssues() + } + }, [projectId, fetchProjectIssues]) + + return ( +
+
+

项目需求列表

+ +
+ + + +
+ ) +} diff --git a/frontend/src/pages/console/user/project/overview/tasks-tab.tsx b/frontend/src/pages/console/user/project/overview/tasks-tab.tsx new file mode 100644 index 00000000..57ccfaed --- /dev/null +++ b/frontend/src/pages/console/user/project/overview/tasks-tab.tsx @@ -0,0 +1,206 @@ +import { useCallback, useEffect, useRef, useState } from "react" +import { type DomainProjectTask } from "@/api/Api" +import { ConstsTaskType } from "@/api/Api" +import { apiRequest } from "@/utils/requestUtils" +import { toast } from "sonner" +import { Badge } from "@/components/ui/badge" +import { HoverCard, HoverCardTrigger } from "@/components/ui/hover-card" +import { Item, ItemContent, ItemDescription, ItemFooter, ItemTitle } from "@/components/ui/item" +import { Spinner } from "@/components/ui/spinner" +import { + Empty, + EmptyDescription, + EmptyHeader, + EmptyMedia, + EmptyTitle, +} from "@/components/ui/empty" +import { IconListDetails, IconCircleCheck, IconAlertTriangle } from "@tabler/icons-react" +import { cn } from "@/lib/utils" +import { getRepoNameFromUrl, renderHoverCardContent, stripMarkdown } from "@/utils/common" +import dayjs from "dayjs" + +const TASKS_PAGE_SIZE = 24 + +interface ProjectOverviewTasksTabProps { + projectId: string +} + +export default function ProjectOverviewTasksTab({ projectId }: ProjectOverviewTasksTabProps) { + const [tasks, setTasks] = useState([]) + const [tasksPage, setTasksPage] = useState(1) + const [tasksHasMore, setTasksHasMore] = useState(true) + const [tasksLoading, setTasksLoading] = useState(false) + const [tasksInitialLoading, setTasksInitialLoading] = useState(true) + const loadMoreRef = useRef(null) + const tasksLoadingRef = useRef(false) + + const fetchTasks = useCallback((pageNum: number, append: boolean) => { + if (!projectId || tasksLoadingRef.current) return + tasksLoadingRef.current = true + setTasksLoading(true) + const reset = () => { + tasksLoadingRef.current = false + setTasksLoading(false) + } + apiRequest("v1UsersTasksList", { project_id: projectId, page: pageNum, size: TASKS_PAGE_SIZE }, [], (resp) => { + if (resp.code === 0) { + const newTasks = resp.data?.tasks || [] + setTasks((prev) => (append ? [...prev, ...newTasks] : newTasks)) + setTasksHasMore(newTasks.length >= TASKS_PAGE_SIZE) + setTasksPage(pageNum) + } else { + toast.error("获取任务列表失败: " + resp.message) + } + reset() + setTasksInitialLoading(false) + }, () => { + reset() + setTasksInitialLoading(false) + }) + }, [projectId]) + + const loadMoreTasks = useCallback(() => { + if (!tasksLoading && tasksHasMore) fetchTasks(tasksPage + 1, true) + }, [tasksLoading, tasksHasMore, tasksPage, fetchTasks]) + + useEffect(() => { + if (projectId) { + setTasks([]) + setTasksPage(1) + setTasksHasMore(true) + setTasksInitialLoading(true) + fetchTasks(1, false) + } + }, [projectId, fetchTasks]) + + useEffect(() => { + const el = loadMoreRef.current + if (!el || !projectId) return + const observer = new IntersectionObserver( + (entries) => { + if (entries[0]?.isIntersecting && tasksHasMore && !tasksLoading) loadMoreTasks() + }, + { rootMargin: "200px" } + ) + observer.observe(el) + return () => observer.disconnect() + }, [tasksHasMore, tasksLoading, loadMoreTasks, projectId]) + + if (tasksInitialLoading && tasks.length === 0) { + return ( +
+ +
+ ) + } + + if (tasks.length === 0) { + return ( + + + + + + 暂无任务 + 该项目下还没有创建任何任务 + + + ) + } + + return ( +
+
+

项目任务列表

+ 共 {tasks.length} 个任务 +
+
+ {tasks.map((task) => ( + + + + + + window.open(`/console/task/view?taskId=${task.id}&projectId=${projectId}`, "_blank") + } + > + {task.summary || stripMarkdown(task.content)} + + + {renderHoverCardContent([ + { title: "任务名称", content: task.summary || "" }, + { title: "任务内容", content: task.content || "" }, + { title: "任务状态", content: task.status || "" }, + { title: "任务类型", content: `${task.type}/${task.sub_type}` || "" }, + task.repo_url ? { title: "代码仓库", content: task.repo_url } : null, + task.repo_filename ? { title: "代码文件", content: task.repo_filename } : null, + task.repo_url ? { title: "仓库分支", content: task.branch || "" } : null, + { title: "开发工具", content: task.cli_name || "" }, + { title: "大模型", content: task.model?.model || "" }, + { + title: "创建时间", + content: dayjs.unix(task.created_at as number).format("YYYY-MM-DD HH:mm:ss"), + }, + ])} + + + {getRepoNameFromUrl(task?.repo_url || "") || task.repo_filename || "-"} + + + +
+ + {task.status === "finished" && ( + <> + + 任务完成 + + )} + {task.status === "error" && ( + <> + + 执行失败 + + )} + {task.status === "pending" && ( + <> + + 等待执行 + + )} + {task.status === "processing" && ( + <> + + 正在执行 + + )} + + + {task?.type === ConstsTaskType.TaskTypeDevelop && "开发任务"} + {task?.type === ConstsTaskType.TaskTypeDesign && "设计任务"} + {task?.type === ConstsTaskType.TaskTypeReview && "审查任务"} + +
+ {dayjs.unix(task.created_at as number).fromNow()} +
+
+ ))} +
+
+ {tasksLoading && } +
+
+ ) +} From e9c6d6249f7aeaf922e0e5a9f1f7c493e9a1fec9 Mon Sep 17 00:00:00 2001 From: monster Date: Fri, 13 Mar 2026 13:59:53 +0800 Subject: [PATCH 02/25] =?UTF-8?q?refactor:=20=E9=A1=B9=E7=9B=AE=E6=A6=82?= =?UTF-8?q?=E8=A7=88=E9=A1=B5=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 需求 Tab:移除标题行,添加状态/优先级筛选,创建需求按钮改小 - 任务 Tab:移除标题行 - 需求/任务空状态撑满父容器剩余高度 - Tabs 布局调整支持 flex 高度传递 Made-with: Cursor --- .../components/console/project/issue-list.tsx | 24 ++++--- .../console/user/project/overview/index.tsx | 10 +-- .../user/project/overview/issues-tab.tsx | 68 +++++++++++++++++-- .../user/project/overview/tasks-tab.tsx | 28 ++++---- 4 files changed, 93 insertions(+), 37 deletions(-) diff --git a/frontend/src/components/console/project/issue-list.tsx b/frontend/src/components/console/project/issue-list.tsx index 2e3ccd18..3ba5f22f 100644 --- a/frontend/src/components/console/project/issue-list.tsx +++ b/frontend/src/components/console/project/issue-list.tsx @@ -6,17 +6,19 @@ import IssueCard from "./issue-card"; export default function ProjectIssueList({ issues, projectId, project, onViewIssue }: { issues: DomainProjectIssue[], projectId: string, project?: DomainProject, onViewIssue: (issue: DomainProjectIssue) => void }) { if (issues.length === 0) { return ( - - - - - - 暂无内容 - - 可以点击右上角的 “创建需求” - - - +
+ + + + + + 暂无内容 + + 可以点击右上角的 “创建需求” + + + +
) } diff --git a/frontend/src/pages/console/user/project/overview/index.tsx b/frontend/src/pages/console/user/project/overview/index.tsx index 21865300..a4a3b262 100644 --- a/frontend/src/pages/console/user/project/overview/index.tsx +++ b/frontend/src/pages/console/user/project/overview/index.tsx @@ -61,21 +61,21 @@ export default function ProjectOverviewPage() { } return ( -
+
- + 信息 需求 任务 - + - + - + diff --git a/frontend/src/pages/console/user/project/overview/issues-tab.tsx b/frontend/src/pages/console/user/project/overview/issues-tab.tsx index ea12bdf8..90346790 100644 --- a/frontend/src/pages/console/user/project/overview/issues-tab.tsx +++ b/frontend/src/pages/console/user/project/overview/issues-tab.tsx @@ -1,24 +1,57 @@ -import { useCallback, useEffect, useState } from "react" -import { type DomainProject, type DomainProjectIssue } from "@/api/Api" +import { useCallback, useEffect, useMemo, useState } from "react" +import { + ConstsProjectIssueStatus, + type DomainProject, + type DomainProjectIssue, +} from "@/api/Api" import { apiRequest } from "@/utils/requestUtils" import { toast } from "sonner" import { Button } from "@/components/ui/button" import { IconPlus } from "@tabler/icons-react" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import ProjectIssueList from "@/components/console/project/issue-list" import ViewIssueDialog from "@/components/console/project/issue-detail" import CreateIssueDialog from "@/components/console/project/create-issue" +import { getStatusName } from "@/utils/common" interface ProjectOverviewIssuesTabProps { projectId: string project?: DomainProject } +const STATUS_ALL = "__all__" +const PRIORITY_ALL = "__all__" + +const STATUS_OPTIONS = [ + { value: STATUS_ALL, label: "全部状态" }, + { value: ConstsProjectIssueStatus.ProjectIssueStatusOpen, label: getStatusName(ConstsProjectIssueStatus.ProjectIssueStatusOpen) }, + { value: ConstsProjectIssueStatus.ProjectIssueStatusCompleted, label: getStatusName(ConstsProjectIssueStatus.ProjectIssueStatusCompleted) }, + { value: ConstsProjectIssueStatus.ProjectIssueStatusClosed, label: getStatusName(ConstsProjectIssueStatus.ProjectIssueStatusClosed) }, +] + +const PRIORITY_OPTIONS = [ + { value: PRIORITY_ALL, label: "全部优先级" }, + { value: "3", label: "高" }, + { value: "2", label: "中" }, + { value: "1", label: "低" }, +] + export default function ProjectOverviewIssuesTab({ projectId, project }: ProjectOverviewIssuesTabProps) { const [issues, setIssues] = useState([]) + const [statusFilter, setStatusFilter] = useState(STATUS_ALL) + const [priorityFilter, setPriorityFilter] = useState(PRIORITY_ALL) const [isCreateIssueDialogOpen, setIsCreateIssueDialogOpen] = useState(false) const [viewingIssue, setViewingIssue] = useState(undefined) const [viewIssueDialogOpen, setViewIssueDialogOpen] = useState(false) + const filteredIssues = useMemo(() => { + return issues.filter((issue) => { + if (statusFilter !== STATUS_ALL && issue.status !== statusFilter) return false + if (priorityFilter !== PRIORITY_ALL && issue.priority?.toString() !== priorityFilter) return false + return true + }) + }, [issues, statusFilter, priorityFilter]) + const fetchProjectIssues = useCallback(async () => { if (!projectId) return await apiRequest("v1UsersProjectsIssuesDetail", {}, [projectId], (resp) => { @@ -54,15 +87,38 @@ export default function ProjectOverviewIssuesTab({ projectId, project }: Project return (
-
-

项目需求列表

-
+
) @@ -96,24 +96,22 @@ export default function ProjectOverviewTasksTab({ projectId }: ProjectOverviewTa if (tasks.length === 0) { return ( - - - - - - 暂无任务 - 该项目下还没有创建任何任务 - - +
+ + + + + + 暂无任务 + 该项目下还没有创建任何任务 + + +
) } return ( -
-
-

项目任务列表

- 共 {tasks.length} 个任务 -
+
{tasks.map((task) => ( From 6055793a93a0a5727edcb1e815e06a9c24a95e5a Mon Sep 17 00:00:00 2001 From: monster Date: Fri, 13 Mar 2026 14:21:54 +0800 Subject: [PATCH 03/25] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E5=AD=90=E8=8F=9C=E5=8D=95=E5=8F=8A=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=EF=BC=8C=E7=BB=9F=E4=B8=80=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E6=A6=82=E8=A7=88=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除侧边栏项目/需求/任务三个子菜单 - 删除 project/info、project/issues、project/tasks 路由及对应页面 - 创建项目、删除项目、任务页返回等跳转改为概览页 Made-with: Cursor --- frontend/src/App.tsx | 6 - .../components/console/nav/nav-project.tsx | 36 +--- .../console/project/add-project.tsx | 2 +- .../console/project/project-info.tsx | 2 +- .../src/pages/console/user/project/issues.tsx | 111 ----------- .../pages/console/user/project/project.tsx | 173 ------------------ .../src/pages/console/user/project/tasks.tsx | 167 ----------------- .../src/pages/console/user/task/task-view.tsx | 2 +- 8 files changed, 9 insertions(+), 490 deletions(-) delete mode 100644 frontend/src/pages/console/user/project/issues.tsx delete mode 100644 frontend/src/pages/console/user/project/project.tsx delete mode 100644 frontend/src/pages/console/user/project/tasks.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e5292d01..1cc01e08 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -22,14 +22,11 @@ import ResetPasswordPage from "./pages/resetpassword" import FindPasswordPage from "./pages/findpassword" import TeamManagerManager from "./pages/console/manager/manager" import TeamManagerOtherSettings from "./pages/console/manager/other-settings" -import ProjectPage from "./pages/console/user/project/project" import PlaygroundPage from "./pages/playground" import PlaygroundDetailPage from "./pages/playground-detail" import PublicTaskPage from "./pages/public-task" import PostCreatePage from "./pages/post-create" -import ProjectIssuesPage from "./pages/console/user/project/issues" import ProjectOverviewPage from "./pages/console/user/project/overview" -import ProjectTasksPage from "./pages/console/user/project/tasks" import TaskDevelopPage from "./pages/console/user/task/task-dev" import TaskViewPage from "./pages/console/user/task/task-view" @@ -50,9 +47,6 @@ function App() { }> } /> } /> - } /> - } /> - } /> } /> } /> } /> diff --git a/frontend/src/components/console/nav/nav-project.tsx b/frontend/src/components/console/nav/nav-project.tsx index bc2ba65e..c56a4221 100644 --- a/frontend/src/components/console/nav/nav-project.tsx +++ b/frontend/src/components/console/nav/nav-project.tsx @@ -7,17 +7,13 @@ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarMenuSubItem, } from "@/components/ui/sidebar" import { useCommonData } from "../data-provider" -import { IconBook, IconCalendarCode, IconFolderCode, IconPlus, IconReload, IconSubtask } from "@tabler/icons-react" +import { IconFolderCode, IconPlus, IconReload } from "@tabler/icons-react" import { FolderOpenDot } from "lucide-react" import { Button } from "@/components/ui/button" import AddProjectDialog from "../project/add-project" import { Label } from "@/components/ui/label" -import { Badge } from "@/components/ui/badge" export default function NavProject() { const location = useLocation() @@ -57,35 +53,15 @@ export default function NavProject() { {projects.length > 0 ? projects.map((project) => ( - + - {(location.pathname === `/console/project/${project.id}` || location.pathname.startsWith(`/console/project/${project.id}/`)) ? : } + {(location.pathname === `/console/project/${project.id}` || location.pathname.startsWith(`/console/project/${project.id}/`)) ? : } {project.name} - {(location.pathname === `/console/project/${project.id}` || location.pathname.startsWith(`/console/project/${project.id}/`)) && - - - - - 项目 - - - - - - 需求 - {project.open_issue_count || 0} - - - - - - 任务 - - - - } )) : ( diff --git a/frontend/src/components/console/project/add-project.tsx b/frontend/src/components/console/project/add-project.tsx index f13a41ed..4fd8bb0a 100644 --- a/frontend/src/components/console/project/add-project.tsx +++ b/frontend/src/components/console/project/add-project.tsx @@ -183,7 +183,7 @@ export default function AddProjectDialog({ setIdentityRepoOptions([]) onSuccess?.() if (resp.data?.id) { - navigate(`/console/project/${resp.data.id}/info/`) + navigate(`/console/project/${resp.data.id}`) } } else { toast.error(resp.message || "创建项目失败") diff --git a/frontend/src/components/console/project/project-info.tsx b/frontend/src/components/console/project/project-info.tsx index 4b17a87f..5e6e54da 100644 --- a/frontend/src/components/console/project/project-info.tsx +++ b/frontend/src/components/console/project/project-info.tsx @@ -74,7 +74,7 @@ const ProjectInfo = ({ const remainingProjects = projects.filter(p => p.id !== deletingProject.id) reloadProjects() if (remainingProjects.length > 0) { - navigate(`/console/project/${remainingProjects[0].id}/info/`) + navigate(`/console/project/${remainingProjects[0].id}`) } else { navigate('/console/tasks') } diff --git a/frontend/src/pages/console/user/project/issues.tsx b/frontend/src/pages/console/user/project/issues.tsx deleted file mode 100644 index a2fa0a72..00000000 --- a/frontend/src/pages/console/user/project/issues.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { useEffect, useState } from "react"; -import { type DomainProjectIssue, type DomainProject } from "@/api/Api"; -import { apiRequest } from "@/utils/requestUtils"; -import { toast } from "sonner"; -import { useParams } from "react-router-dom"; -import { Button } from "@/components/ui/button"; -import { IconPlus } from "@tabler/icons-react"; -import ProjectIssueList from "@/components/console/project/issue-list"; -import ViewIssueDialog from "@/components/console/project/issue-detail"; -import CreateIssueDialog from "@/components/console/project/create-issue"; - -export default function ProjectIssuesPage() { - const { projectId = '' } = useParams<{ projectId: string }>() - const [project, setProject] = useState(undefined) - const [issues, setIssues] = useState([]) - const [isCreateIssueDialogOpen, setIsCreateIssueDialogOpen] = useState(false) - const [viewingIssue, setViewingIssue] = useState(undefined) - const [viewIssueDialogOpen, setViewIssueDialogOpen] = useState(false) - - const fetchProject = async () => { - await apiRequest("v1UsersProjectsDetail", {}, [projectId], (resp) => { - if (resp.code === 0) { - setProject(resp.data) - } else { - toast.error(resp.message || "获取项目失败") - } - }) - } - - const fetchProjectIssues = async () => { - await apiRequest('v1UsersProjectsIssuesDetail', {}, [projectId], (resp) => { - if (resp.code === 0) { - const issues = resp.data?.issues || [] - // 排序:首先按状态(open -> completed -> closed),其次按优先级(1 -> 2 -> 3),最后按修改时间(最新的在前) - const sortedIssues = [...issues].sort((a, b) => { - // 1. 首先按状态排序 - const getStatusOrder = (status?: string) => { - if (status === 'open') return 1 - if (status === 'completed') return 2 - if (status === 'closed') return 3 - return 4 // 其他状态排在最后 - } - const statusDiff = getStatusOrder(a.status) - getStatusOrder(b.status) - if (statusDiff !== 0) return statusDiff - - // 2. 其次按优先级排序(1 最高优先级,3 最低优先级) - const priorityA = a.priority ?? 999 // 没有优先级的排在最后 - const priorityB = b.priority ?? 999 - const priorityDiff = priorityA - priorityB - if (priorityDiff !== 0) return priorityDiff - - // 3. 最后按修改时间排序(最新的在前,即时间戳大的在前) - const timeA = a.created_at ?? 0 - const timeB = b.created_at ?? 0 - return timeB - timeA - }) - setIssues(sortedIssues) - } else { - toast.error(resp.message || "获取项目需求失败") - } - }) - } - - const handleViewIssue = (issue: DomainProjectIssue) => { - setViewingIssue(issue) - setViewIssueDialogOpen(true) - } - - useEffect(() => { - if (projectId) { - fetchProject() - fetchProjectIssues() - } - }, [projectId]) - - - return ( -
-
-

项目需求列表

- -
- - - - - - -
- ) -} \ No newline at end of file diff --git a/frontend/src/pages/console/user/project/project.tsx b/frontend/src/pages/console/user/project/project.tsx deleted file mode 100644 index b8c93228..00000000 --- a/frontend/src/pages/console/user/project/project.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { type DomainProject } from "@/api/Api" -import { Markdown } from "@/components/common/markdown" -import { ProjectFileManager } from "@/components/console/project/files" -import ProjectInfo from "@/components/console/project/project-info" -import { - Empty, - EmptyDescription, - EmptyHeader, - EmptyMedia, - EmptyTitle, -} from "@/components/ui/empty" -import { Label } from "@/components/ui/label" -import { apiRequest } from "@/utils/requestUtils" -import { b64decode } from "@/utils/common" -import { cn } from "@/lib/utils" -import { isProjectRepoUnbound } from "@/utils/project" -import { useEffect, useRef, useState } from "react" -import { useParams } from "react-router-dom" -import { toast } from "sonner" -import { IconAlertCircle, IconFileText, IconLoader } from "@tabler/icons-react" - -export default function ProjectPage() { - const { projectId = '' } = useParams<{ projectId: string }>() - const projectIdRef = useRef(projectId) - projectIdRef.current = projectId - - const [project, setProject] = useState(undefined) - const [readmeContent, setReadmeContent] = useState('') - const [readmePath, setReadmePath] = useState('') - const [readmeLoaded, setReadmeLoaded] = useState(false) - - const fetchProject = async () => { - const requestedId = projectId - await apiRequest("v1UsersProjectsDetail", {}, [requestedId], (resp) => { - // 忽略过期响应:切换 project 后,旧请求可能晚于新请求返回 - if (projectIdRef.current !== requestedId) return - if (resp.code === 0) { - setProject(resp.data) - } else { - toast.error(resp.message || "获取项目失败") - } - }) - } - - const isRepoUnbound = isProjectRepoUnbound(project) - - useEffect(() => { - if (projectId) { - setProject(undefined) - setReadmeContent('') - setReadmePath('') - setReadmeLoaded(false) - fetchProject() - } - }, [projectId]) - - // 读取根目录文件列表,找到 readme.md(忽略大小写)后拉取内容 - useEffect(() => { - if (!project?.id) { - setReadmeContent('') - setReadmePath('') - setReadmeLoaded(true) - return - } - - const requestedProjectId = project.id - setReadmeLoaded(false) - apiRequest('v1UsersProjectsTreeDetail', { - recursive: false, - path: '' - }, [project.id], (resp) => { - if (projectIdRef.current !== requestedProjectId) return - if (resp.code !== 0 || !resp.data) { - setReadmeLoaded(true) - return - } - const readme = (resp.data as { name?: string; path?: string }[]).find( - (e) => e.name?.toLowerCase() === 'readme.md' - ) - const readmePathVal = readme?.path - if (!readme || !readmePathVal) { - setReadmeContent('') - setReadmePath('') - setReadmeLoaded(true) - return - } - const pathToFetch = readmePathVal as string - apiRequest('v1UsersProjectsTreeBlobDetail', { path: pathToFetch }, [project.id as string], (r) => { - if (projectIdRef.current !== requestedProjectId) return - if (r.code === 0 && r.data?.content) { - setReadmeContent(b64decode(r.data.content)) - setReadmePath(pathToFetch) - window.scrollTo(0, 0) - } - setReadmeLoaded(true) - }) - }) - }, [project?.id]) - - if (project && isRepoUnbound) { - return ( - - - - - - 项目异常 - 这个项目没有绑定仓库 - - - ) - } - - const ReadmeHeader = ( -
- -
- ) - - const renderReadme = () => { - if (!readmeLoaded) { - return ( -
- {ReadmeHeader} - - - - - - 正在加载... - - -
- ) - } - if (!readmeContent) { - return ( -
- {ReadmeHeader} - - - - - - 暂无文档 - - -
- ) - } - return ( -
- {ReadmeHeader} -
- {readmeContent} -
-
- ) - } - - return ( -
- - - - - {renderReadme()} -
- ) -} diff --git a/frontend/src/pages/console/user/project/tasks.tsx b/frontend/src/pages/console/user/project/tasks.tsx deleted file mode 100644 index 38c57490..00000000 --- a/frontend/src/pages/console/user/project/tasks.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { ConstsTaskType, type DomainProjectTask } from "@/api/Api"; -import { Badge } from "@/components/ui/badge"; -import { HoverCard, HoverCardTrigger } from "@/components/ui/hover-card"; -import { Item, ItemContent, ItemDescription, ItemFooter, ItemTitle } from "@/components/ui/item"; -import { Spinner } from "@/components/ui/spinner"; -import { cn } from "@/lib/utils"; -import { getRepoNameFromUrl, renderHoverCardContent, stripMarkdown } from "@/utils/common"; -import { apiRequest } from "@/utils/requestUtils"; -import { IconAlertTriangle, IconCircleCheck, IconListDetails } from "@tabler/icons-react"; -import dayjs from "dayjs"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { useParams } from "react-router-dom"; -import { toast } from "sonner"; -import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"; - -const PAGE_SIZE = 24; - -export default function ProjectTasksPage() { - const { projectId = '' } = useParams<{ projectId: string }>() - const [tasks, setTasks] = useState([]) - const [page, setPage] = useState(1) - const [hasMore, setHasMore] = useState(true) - const [loading, setLoading] = useState(false) - const [initialLoading, setInitialLoading] = useState(true) - const loadMoreRef = useRef(null) - const loadingRef = useRef(false) - - const fetchTasks = useCallback((pageNum: number, append: boolean) => { - if (!projectId || loadingRef.current) return - loadingRef.current = true - setLoading(true) - const resetLoading = () => { - loadingRef.current = false - setLoading(false) - } - apiRequest('v1UsersTasksList', { project_id: projectId, page: pageNum, size: PAGE_SIZE }, [], (resp) => { - if (resp.code === 0) { - const newTasks = resp.data?.tasks || [] - setTasks(prev => append ? [...prev, ...newTasks] : newTasks) - setHasMore(newTasks.length >= PAGE_SIZE) - setPage(pageNum) - } else { - toast.error("获取任务列表失败: " + resp.message) - } - resetLoading() - setInitialLoading(false) - }, () => { - resetLoading() - setInitialLoading(false) - }) - }, [projectId]) - - const loadMore = useCallback(() => { - if (!loading && hasMore) { - fetchTasks(page + 1, true) - } - }, [loading, hasMore, page, fetchTasks]) - - useEffect(() => { - if (projectId) { - setTasks([]) - setInitialLoading(true) - setPage(1) - setHasMore(true) - fetchTasks(1, false) - } - }, [projectId, fetchTasks]) - - useEffect(() => { - const el = loadMoreRef.current - if (!el || !projectId) return - const observer = new IntersectionObserver( - (entries) => { - if (entries[0]?.isIntersecting && hasMore && !loading) { - loadMore() - } - }, - { rootMargin: "200px" } - ) - observer.observe(el) - return () => observer.disconnect() - }, [hasMore, loading, loadMore, projectId]) - - if (initialLoading && tasks.length === 0) { - return ( -
- -
- ) - } - - if (tasks.length === 0) { - return ( - - - - - - 暂无任务 - - 该项目下还没有创建任何任务 - - - - ) - } - - return ( -
-
-

项目任务列表

- 共 {tasks.length} 个任务 -
-
- {tasks?.map((task) => ( - - - - - { - window.open(`/console/task/view?taskId=${task.id}&projectId=${projectId}`, "_blank") - }}> - {task.summary || stripMarkdown(task.content)} - - - {renderHoverCardContent([ - {title: "任务名称", content: task.summary || ""}, - {title: "任务内容", content: task.content || ""}, - {title: "任务状态", content: task.status || ""}, - {title: "任务类型", content: `${task.type}/${task.sub_type}` || ""}, - task.repo_url ? {title: "代码仓库", content: task.repo_url} : null, - task.repo_filename ? {title: "代码文件", content: task.repo_filename} : null, - task.repo_url ? {title: "仓库分支", content: task.branch || ""} : null, - {title: "开发工具", content: task.cli_name || ""}, - {title: "大模型", content: task.model?.model || ""}, - {title: "创建时间", content: dayjs.unix(task.created_at as number).format("YYYY-MM-DD HH:mm:ss")}, - ])} - - - {getRepoNameFromUrl(task?.repo_url || '') || task.repo_filename || '-'} - - - -
- - {task.status === "finished" && <>任务完成} - {task.status === "error" && <>执行失败} - {task.status === "pending" && <>等待执行} - {task.status === "processing" && <>正在执行} - - - {task?.type === ConstsTaskType.TaskTypeDevelop && "开发任务"} - {task?.type === ConstsTaskType.TaskTypeDesign && "设计任务"} - {task?.type === ConstsTaskType.TaskTypeReview && "审查任务"} - -
- {dayjs.unix(task.created_at as number).fromNow()} -
-
- ))} -
-
- {loading && } -
-
- ) -} diff --git a/frontend/src/pages/console/user/task/task-view.tsx b/frontend/src/pages/console/user/task/task-view.tsx index f0b5fbc6..d6dd8cdf 100644 --- a/frontend/src/pages/console/user/task/task-view.tsx +++ b/frontend/src/pages/console/user/task/task-view.tsx @@ -90,7 +90,7 @@ export default function TaskViewPage() { variant="outline" size="sm" className="flex-1" - onClick={() => window.location.href = projectId ? `/console/project/${projectId}/tasks` : '/console/tasks'} + onClick={() => window.location.href = projectId ? `/console/project/${projectId}` : '/console/tasks'} > {sidebarWidth === 'wide' ? '启动新任务' : '新任务'} From 7525d4c7c03eabf20d572c0a9919d78564183f3d Mon Sep 17 00:00:00 2001 From: monster Date: Fri, 13 Mar 2026 15:09:42 +0800 Subject: [PATCH 04/25] =?UTF-8?q?feat:=20=E4=BD=99=E9=A2=9D=E7=A7=BB?= =?UTF-8?q?=E8=87=B3=20header=E3=80=81=E6=99=BA=E8=83=BD=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=94=B9=E5=90=8D=E4=B8=BA=E6=96=B0=E4=BB=BB=E5=8A=A1=E3=80=81?= =?UTF-8?q?overview=20=E4=BB=BB=E5=8A=A1=E5=8D=A1=E7=89=87=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F=E7=BB=9F=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将余额按钮从侧边栏移至 header,放在微信交流群右侧 - 左侧导航「智能任务」改名为「新任务」 - 项目 overview 页任务卡片样式与新任务页保持一致(移除任务类型徽章,添加 tokens 徽章) Made-with: Cursor --- frontend/src/api/Api.ts | 4 +- .../components/console/nav/nav-balance.tsx | 34 +++++++++++---- .../src/components/console/nav/nav-main.tsx | 2 +- .../components/console/nav/nav-project.tsx | 43 ++++++++++++++++++- .../components/console/nav/user-sidebar.tsx | 2 - frontend/src/pages/console/user/page.tsx | 4 +- .../user/project/overview/tasks-tab.tsx | 23 +++++----- 7 files changed, 85 insertions(+), 27 deletions(-) diff --git a/frontend/src/api/Api.ts b/frontend/src/api/Api.ts index 9b795460..ad8ec0a5 100644 --- a/frontend/src/api/Api.ts +++ b/frontend/src/api/Api.ts @@ -866,12 +866,12 @@ export interface DomainProject { issues?: DomainProjectIssue[]; /** 项目名 */ name?: string; - /** 未解决问题数量 */ - open_issue_count?: number; /** 项目平台 */ platform?: ConstsGitPlatform; /** 项目仓库URL */ repo_url?: string; + /** 项目相关的任务 */ + tasks?: DomainProjectTask[]; /** 更新时间 */ updated_at?: number; /** 用户信息 */ diff --git a/frontend/src/components/console/nav/nav-balance.tsx b/frontend/src/components/console/nav/nav-balance.tsx index bb682761..f9b50afd 100644 --- a/frontend/src/components/console/nav/nav-balance.tsx +++ b/frontend/src/components/console/nav/nav-balance.tsx @@ -18,7 +18,11 @@ import { cn } from "@/lib/utils"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useCommonData } from "../data-provider"; -export default function NavBalance() { +interface NavBalanceProps { + variant?: "sidebar" | "header"; +} + +export default function NavBalance({ variant = "sidebar" }: NavBalanceProps) { const [transcations, setTranscations] = useState([]); const [exchangeCode, setExchangeCode] = useState(""); const [isLoading, setIsLoading] = useState(false); @@ -110,17 +114,29 @@ export default function NavBalance() { toast.success("邀请链接已复制到剪贴板"); } + const triggerContent = ( + <> + + 余额: {Math.ceil(balance + bonus).toLocaleString()} 点 + + ); + return ( - - - - - 余额: {Math.ceil(balance + bonus).toLocaleString()} 点 - - - + {variant === "header" ? ( + + ) : ( + + + + {triggerContent} + + + + )} diff --git a/frontend/src/components/console/nav/nav-main.tsx b/frontend/src/components/console/nav/nav-main.tsx index ac43c78d..7f1fa021 100644 --- a/frontend/src/components/console/nav/nav-main.tsx +++ b/frontend/src/components/console/nav/nav-main.tsx @@ -24,7 +24,7 @@ export default function NavMain() { > - 智能任务 + 新任务
diff --git a/frontend/src/components/console/nav/nav-project.tsx b/frontend/src/components/console/nav/nav-project.tsx index c56a4221..b363715b 100644 --- a/frontend/src/components/console/nav/nav-project.tsx +++ b/frontend/src/components/console/nav/nav-project.tsx @@ -7,13 +7,19 @@ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, } from "@/components/ui/sidebar" import { useCommonData } from "../data-provider" -import { IconFolderCode, IconPlus, IconReload } from "@tabler/icons-react" +import { IconCircleMinus, IconFolderCode, IconLoader, IconPlus, IconReload } from "@tabler/icons-react" import { FolderOpenDot } from "lucide-react" import { Button } from "@/components/ui/button" import AddProjectDialog from "../project/add-project" import { Label } from "@/components/ui/label" +import { type DomainProjectTask } from "@/api/Api" +import { stripMarkdown } from "@/utils/common" +import { cn } from "@/lib/utils" export default function NavProject() { const location = useLocation() @@ -62,6 +68,41 @@ export default function NavProject() { {project.name} + + + {(project.tasks || []).map((task: DomainProjectTask) => { + const TaskIcon = + task.status === "finished" || task.status === "error" + ? IconCircleMinus + : IconLoader + return ( + svg]:!text-muted-foreground" + )} + > + + + {task.summary || stripMarkdown(task.content || "")} + + + ) + })} + + )) : ( diff --git a/frontend/src/components/console/nav/user-sidebar.tsx b/frontend/src/components/console/nav/user-sidebar.tsx index afb894fb..523e6ca1 100644 --- a/frontend/src/components/console/nav/user-sidebar.tsx +++ b/frontend/src/components/console/nav/user-sidebar.tsx @@ -10,7 +10,6 @@ import { SidebarMenuButton, SidebarMenuItem, } from "@/components/ui/sidebar" -import NavBalance from "./nav-balance" import NavMain from "./nav-main" import { Link } from "react-router-dom" import { Settings } from "lucide-react" @@ -54,7 +53,6 @@ export default function UserSidebar({
- diff --git a/frontend/src/pages/console/user/page.tsx b/frontend/src/pages/console/user/page.tsx index 81af0485..69cb49fe 100644 --- a/frontend/src/pages/console/user/page.tsx +++ b/frontend/src/pages/console/user/page.tsx @@ -17,6 +17,7 @@ import { } from "@/components/ui/sidebar" import { Button } from "@/components/ui/button" import { HelpCircle, Users } from "lucide-react" +import NavBalance from "@/components/console/nav/nav-balance" import { DataProvider } from "@/components/console/data-provider" import { HoverCard, @@ -35,7 +36,7 @@ export default function UserConsolePage() { { label: "仪表盘", href: "/console/dashboard" }, ], "/console/tasks": [ - { label: "智能任务", href: "/console/tasks" }, + { label: "新任务", href: "/console/tasks" }, ], "/console/projects": [ { label: "项目管理", href: "/console/projects" }, @@ -120,6 +121,7 @@ export default function UserConsolePage() {
+
{dayjs.unix(task.created_at as number).fromNow()} From f80401703ca8337a762a8153dba6f31529e64a01 Mon Sep 17 00:00:00 2001 From: monster Date: Fri, 13 Mar 2026 16:30:56 +0800 Subject: [PATCH 05/25] =?UTF-8?q?feat:=20=E4=BE=A7=E8=BE=B9=E6=A0=8F?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E9=A1=B9=E7=9B=AE=E5=A4=9A=E9=A1=B9=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 项目支持折叠/展开,chevron 图标可点击,状态存 localStorage - 默认全部折叠,有 pending/processing 任务时自动展开 - 项目名称右侧添加「启动任务」加号按钮,打开 StartDevelopTaskDialog - 加号按钮默认灰色,悬停行变正常色,悬停按钮变 primary - 创建项目按钮加号改为 tooltip 显示「创建项目」 - 默认入口始终显示,不再依赖加载状态 - 项目列表每 30 秒自动刷新 - 移除 nav-main 中的新任务入口(与默认入口合并) - 启动 AI 任务对话框标题改为「启动 AI 任务」 Made-with: Cursor --- .../src/components/console/nav/nav-main.tsx | 16 +- .../components/console/nav/nav-project.tsx | 368 +++++++++++++++--- .../project/start-develop-task-dialog.tsx | 2 +- 3 files changed, 314 insertions(+), 72 deletions(-) diff --git a/frontend/src/components/console/nav/nav-main.tsx b/frontend/src/components/console/nav/nav-main.tsx index 7f1fa021..2064f9c3 100644 --- a/frontend/src/components/console/nav/nav-main.tsx +++ b/frontend/src/components/console/nav/nav-main.tsx @@ -1,7 +1,4 @@ -import { - Bot, - Github, -} from "lucide-react" +import { Github } from "lucide-react" import { Link, useLocation } from "react-router-dom" import { @@ -17,17 +14,6 @@ export default function NavMain() { return ( - - - - - 新任务 - - - => { + try { + const cached = localStorage.getItem(STORAGE_KEY) + if (cached) return JSON.parse(cached) + } catch {} + return {} +} + +const saveExpandedToStorage = (state: Record) => { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)) + } catch {} +} export default function NavProject() { const location = useLocation() const [addDialogOpen, setAddDialogOpen] = useState(false) + const [startTaskProject, setStartTaskProject] = useState<{ id: string; name?: string } | null>(null) + const [unlinkedTasks, setUnlinkedTasks] = useState([]) + const [loadingUnlinkedTasks, setLoadingUnlinkedTasks] = useState(false) + const [expandedProjects, setExpandedProjects] = useState>(loadExpandedFromStorage) const { projects, loadingProjects, reloadProjects } = useCommonData() + useEffect(() => { + const stored = loadExpandedFromStorage() + const next: Record = {} + const toSave: Record = { ...stored } + let changed = false + + for (const project of projects) { + const projectId = project.id ?? "" + const hasActiveTasks = (project.tasks || []).some( + (t) => t.status === "pending" || t.status === "processing" + ) + if (hasActiveTasks) { + next[projectId] = true + } else if (projectId in stored) { + next[projectId] = stored[projectId] + } else { + next[projectId] = false + } + if (!(projectId in stored)) { + toSave[projectId] = next[projectId] + changed = true + } + } + + setExpandedProjects((prev) => { + const merged = { ...prev, ...next } + return merged + }) + if (changed) saveExpandedToStorage(toSave) + }, [projects]) + + useEffect(() => { + if (loadingUnlinkedTasks) return + const stored = loadExpandedFromStorage() + const hasActiveUnlinked = unlinkedTasks.some( + (t) => t.status === "pending" || t.status === "processing" + ) + const nextUnlinked = hasActiveUnlinked + ? true + : UNLINKED_KEY in stored + ? stored[UNLINKED_KEY] + : false + const changed = !(UNLINKED_KEY in stored) + setExpandedProjects((prev) => ({ ...prev, [UNLINKED_KEY]: nextUnlinked })) + if (changed && unlinkedTasks.length > 0) { + saveExpandedToStorage({ ...stored, [UNLINKED_KEY]: nextUnlinked }) + } + }, [unlinkedTasks, loadingUnlinkedTasks]) + + const handleProjectOpenChange = (projectId: string, open: boolean) => { + setExpandedProjects((prev) => { + const next = { ...prev, [projectId]: open } + saveExpandedToStorage(next) + return next + }) + } + + const fetchUnlinkedTasks = useCallback(() => { + setLoadingUnlinkedTasks(true) + apiRequest("v1UsersTasksList", { page: 1, size: UNLINKED_TASKS_FETCH_SIZE }, [], (resp) => { + if (resp.code === 0) { + const allTasks = resp.data?.tasks || [] + const unlinked = allTasks + .filter((t: DomainProjectTask) => { + const pid = t.extra?.project_id + return !pid || pid === "00000000-0000-0000-0000-000000000000" + }) + .sort((a: DomainProjectTask, b: DomainProjectTask) => (b.created_at || 0) - (a.created_at || 0)) + .slice(0, UNLINKED_TASKS_LIMIT) + setUnlinkedTasks(unlinked) + } + setLoadingUnlinkedTasks(false) + }, () => setLoadingUnlinkedTasks(false)) + }, []) + + useEffect(() => { + fetchUnlinkedTasks() + }, [fetchUnlinkedTasks]) + + useEffect(() => { + const timer = setInterval(() => { + reloadProjects() + fetchUnlinkedTasks() + }, 30000) + return () => clearInterval(timer) + }, [reloadProjects, fetchUnlinkedTasks]) + + const isUnlinkedActive = location.pathname === "/console/tasks" + return ( @@ -36,19 +152,27 @@ export default function NavProject() { variant="ghost" size="icon" className="size-5" - onClick={reloadProjects} + onClick={() => { + reloadProjects() + fetchUnlinkedTasks() + }} disabled={loadingProjects} > - + + + + + 创建项目 +
+ {startTaskProject && ( + { + if (!open) { + setStartTaskProject(null) + reloadProjects() + } + }} + project={projects.find((p) => p.id === startTaskProject.id)} + /> + )} - {projects.length > 0 ? projects.map((project) => ( - - handleProjectOpenChange(UNLINKED_KEY, open)} + > + +
svg]:size-4 [&>svg]:shrink-0", + isUnlinkedActive && "bg-sidebar-accent font-medium text-sidebar-accent-foreground" + )} + > + + + + + 默认 + +
+ + {unlinkedTasks.length > 0 && ( + + + {unlinkedTasks.map((task: DomainProjectTask) => { + const TaskIcon = + task.status === "finished" || task.status === "error" + ? IconCircleMinus + : IconLoader + return ( + svg]:!text-muted-foreground" + )} + > +
+ + {task.summary || stripMarkdown(task.content || "")} + + + ) + })} + + + )} + + + + {projects.length > 0 ? projects.map((project) => { + const projectId = project.id ?? "" + const isExpanded = expandedProjects[projectId] ?? false + const isProjectActive = location.pathname === `/console/project/${projectId}` || location.pathname.startsWith(`/console/project/${projectId}/`) + return ( + handleProjectOpenChange(projectId, open)} > - - {(location.pathname === `/console/project/${project.id}` || location.pathname.startsWith(`/console/project/${project.id}/`)) ? : } - {project.name} - - - - - {(project.tasks || []).map((task: DomainProjectTask) => { - const TaskIcon = - task.status === "finished" || task.status === "error" - ? IconCircleMinus - : IconLoader - return ( - svg]:!text-muted-foreground" - )} + +
svg]:size-4 [&>svg]:shrink-0", + isProjectActive && "bg-sidebar-accent font-medium text-sidebar-accent-foreground" + )} + > + + + + + {project.name} + + + + + + 启动任务 + +
+ + + + {(project.tasks || []).map((task: DomainProjectTask) => { + const TaskIcon = + task.status === "finished" || task.status === "error" + ? IconCircleMinus + : IconLoader + return ( + svg]:!text-muted-foreground" )} - /> - {task.summary || stripMarkdown(task.content || "")} - -
- ) - })} -
-
- - )) : ( + > + + + {task.summary || stripMarkdown(task.content || "")} + + + ) + })} + + + + +
+ ) + }) : ( 暂无项目 diff --git a/frontend/src/components/console/project/start-develop-task-dialog.tsx b/frontend/src/components/console/project/start-develop-task-dialog.tsx index 66f65c61..4aa73502 100644 --- a/frontend/src/components/console/project/start-develop-task-dialog.tsx +++ b/frontend/src/components/console/project/start-develop-task-dialog.tsx @@ -134,7 +134,7 @@ export default function StartDevelopTaskDialog({ - 发起对话 + 启动 AI 任务
From af847631738e3bd3df3d721d457aa7fba8a60dde Mon Sep 17 00:00:00 2001 From: monster Date: Fri, 13 Mar 2026 17:00:26 +0800 Subject: [PATCH 06/25] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=20task/vie?= =?UTF-8?q?w=20=E9=A1=B5=E9=9D=A2=EF=BC=8C=E5=88=9B=E5=BB=BA=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E5=90=8E=E7=9B=B4=E6=8E=A5=E8=B7=B3=E8=BD=AC=E5=88=B0?= =?UTF-8?q?=20task/develop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- frontend/src/App.tsx | 2 - .../components/console/nav/nav-project.tsx | 8 +- .../console/project/issue-design-dialog.tsx | 2 +- .../console/project/issue-dev-dialog.tsx | 2 +- .../project/start-develop-task-dialog.tsx | 5 +- .../components/console/task/task-input.tsx | 2 +- .../user/project/overview/tasks-tab.tsx | 2 +- .../src/pages/console/user/task/task-view.tsx | 176 ------------------ frontend/src/pages/console/user/tasks.tsx | 4 +- 9 files changed, 11 insertions(+), 192 deletions(-) delete mode 100644 frontend/src/pages/console/user/task/task-view.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1cc01e08..c91cffdc 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -28,7 +28,6 @@ import PublicTaskPage from "./pages/public-task" import PostCreatePage from "./pages/post-create" import ProjectOverviewPage from "./pages/console/user/project/overview" import TaskDevelopPage from "./pages/console/user/task/task-dev" -import TaskViewPage from "./pages/console/user/task/task-view" function App() { return ( @@ -54,7 +53,6 @@ function App() { } /> } /> - } /> } /> } /> }> diff --git a/frontend/src/components/console/nav/nav-project.tsx b/frontend/src/components/console/nav/nav-project.tsx index 7f598646..4e8c3f23 100644 --- a/frontend/src/components/console/nav/nav-project.tsx +++ b/frontend/src/components/console/nav/nav-project.tsx @@ -239,14 +239,14 @@ export default function NavProject() { svg]:!text-muted-foreground" )} > @@ -332,14 +332,14 @@ export default function NavProject() { svg]:!text-muted-foreground" )} > diff --git a/frontend/src/components/console/project/issue-design-dialog.tsx b/frontend/src/components/console/project/issue-design-dialog.tsx index a038e265..1a5e431d 100644 --- a/frontend/src/components/console/project/issue-design-dialog.tsx +++ b/frontend/src/components/console/project/issue-design-dialog.tsx @@ -138,7 +138,7 @@ ${issue?.requirement_document?.replaceAll("`", "\\`")} toast.success('方案设计任务已启动') onConfirm?.() handleOpenChange(false) - window.open(`/console/task/view?taskId=${resp.data?.id}`, "_blank") + window.open(`/console/task/develop/${resp.data?.id}`, "_blank") } else { toast.error(resp.message || '任务启动失败') } diff --git a/frontend/src/components/console/project/issue-dev-dialog.tsx b/frontend/src/components/console/project/issue-dev-dialog.tsx index 8d2fa6b9..85138e07 100644 --- a/frontend/src/components/console/project/issue-dev-dialog.tsx +++ b/frontend/src/components/console/project/issue-dev-dialog.tsx @@ -143,7 +143,7 @@ ${issue?.design_document?.replaceAll("`", "\\`")} toast.success('开发任务已启动') onConfirm?.() handleOpenChange(false) - window.open(`/console/task/view?taskId=${resp.data?.id}`, "_blank") + window.open(`/console/task/develop/${resp.data?.id}`, "_blank") } else { toast.error(resp.message || '任务启动失败') } diff --git a/frontend/src/components/console/project/start-develop-task-dialog.tsx b/frontend/src/components/console/project/start-develop-task-dialog.tsx index 4aa73502..2a9a818c 100644 --- a/frontend/src/components/console/project/start-develop-task-dialog.tsx +++ b/frontend/src/components/console/project/start-develop-task-dialog.tsx @@ -118,10 +118,7 @@ export default function StartDevelopTaskDialog({ if (resp.code === 0) { toast.success('对话任务已启动') onOpenChange(false) - const viewUrl = project?.id - ? `/console/task/view?taskId=${resp.data?.id}&projectId=${project.id}` - : `/console/task/view?taskId=${resp.data?.id}` - window.open(viewUrl, "_blank") + window.open(`/console/task/develop/${resp.data?.id}`, "_blank") } else { toast.error(resp.message || '任务启动失败') } diff --git a/frontend/src/components/console/task/task-input.tsx b/frontend/src/components/console/task/task-input.tsx index c8fed039..87b43898 100644 --- a/frontend/src/components/console/task/task-input.tsx +++ b/frontend/src/components/console/task/task-input.tsx @@ -299,7 +299,7 @@ export function TaskInput({ repos, onTaskCreated }: TaskInputProps) { if (resp.code === 0) { toast.success('任务启动成功'); onTaskCreated(); - window.open(`/console/task/view?taskId=${resp.data?.id}`, "_blank"); + window.open(`/console/task/develop/${resp.data?.id}`, "_blank"); } else { toast.error(resp.message || "任务启动失败"); } diff --git a/frontend/src/pages/console/user/project/overview/tasks-tab.tsx b/frontend/src/pages/console/user/project/overview/tasks-tab.tsx index d7c2a760..4809f66a 100644 --- a/frontend/src/pages/console/user/project/overview/tasks-tab.tsx +++ b/frontend/src/pages/console/user/project/overview/tasks-tab.tsx @@ -127,7 +127,7 @@ export default function ProjectOverviewTasksTab({ projectId }: ProjectOverviewTa - window.open(`/console/task/view?taskId=${task.id}&projectId=${projectId}`, "_blank") + window.open(`/console/task/develop/${task.id}`, "_blank") } > {task.summary || stripMarkdown(task.content)} diff --git a/frontend/src/pages/console/user/task/task-view.tsx b/frontend/src/pages/console/user/task/task-view.tsx deleted file mode 100644 index d6dd8cdf..00000000 --- a/frontend/src/pages/console/user/task/task-view.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import { type DomainProjectTask } from "@/api/Api" -import { Button } from "@/components/ui/button" -import { ScrollArea } from "@/components/ui/scroll-area" -import { Spinner } from "@/components/ui/spinner" -import { cn } from "@/lib/utils" -import { stripMarkdown } from "@/utils/common" -import { apiRequest } from "@/utils/requestUtils" -import { IconPlus, IconCircleCheck, IconAlertTriangle, IconLoader, IconLayoutSidebar } from "@tabler/icons-react" -import dayjs from "dayjs" -import relativeTime from "dayjs/plugin/relativeTime" -import { useCallback, useEffect, useState } from "react" -import { useSearchParams } from "react-router-dom" -import { toast } from "sonner" - -dayjs.extend(relativeTime) - -const PAGE_SIZE = 500 - -export default function TaskViewPage() { - const [searchParams, setSearchParams] = useSearchParams() - const projectId = searchParams.get('projectId') || undefined - const [tasks, setTasks] = useState([]) - const [loading, setLoading] = useState(false) - const [selectedTaskId, setSelectedTaskId] = useState(searchParams.get('taskId') || null) - const [sidebarWidth, setSidebarWidth] = useState<'wide' | 'narrow'>('wide') - const [, update] = useState(0) - - useEffect(() => { - const interval = setInterval(() => { - update(v => v + 1) - }, 30000) - return () => clearInterval(interval) - }, []) - - const fetchTasks = useCallback(async () => { - setLoading(true) - const params: { page: number; size: number; project_id?: string } = { page: 1, size: PAGE_SIZE } - if (projectId) params.project_id = projectId - await apiRequest('v1UsersTasksList', params, [], (resp) => { - if (resp.code === 0) { - const fetchedTasks = resp.data?.tasks || [] - setTasks(fetchedTasks) - if (fetchedTasks.length > 0 && !selectedTaskId) { - const firstTask = fetchedTasks[0] - setSelectedTaskId(firstTask.id) - const nextParams = new URLSearchParams({ taskId: firstTask.id }) - if (projectId) nextParams.set('projectId', projectId) - setSearchParams(nextParams, { replace: true }) - } else if (selectedTaskId) { - const foundTask = fetchedTasks.find((t: DomainProjectTask) => t.id === selectedTaskId) - if (foundTask) { - setTasks(fetchedTasks) - } - } - } else { - toast.error("获取任务列表失败: " + resp.message) - } - }) - setLoading(false) - }, [selectedTaskId, setSearchParams, projectId]) - - useEffect(() => { - fetchTasks() - }, [fetchTasks]) - - useEffect(() => { - const taskIdFromUrl = searchParams.get('taskId') - if (taskIdFromUrl && tasks.length > 0) { - setTimeout(() => { - const element = document.querySelector(`[data-task-id="${taskIdFromUrl}"]`) - element?.scrollIntoView({ behavior: 'smooth', block: 'center' }) - }, 100) - } - }, [tasks, searchParams]) - - useEffect(() => { - if (selectedTaskId) { - setTimeout(() => { - const element = document.querySelector(`[data-task-id="${selectedTaskId}"]`) - element?.scrollIntoView({ behavior: 'smooth', block: 'center' }) - }, 100) - } - }, [selectedTaskId]) - - return ( -
-
-
- - -
- -
- {loading && tasks.length === 0 ? ( -
- -
- ) : tasks.length === 0 ? ( -
- 暂无任务 -
- ) : ( - tasks.map((task) => ( -
{ - setSelectedTaskId(task.id) - const nextParams = new URLSearchParams({ taskId: task.id }) - if (projectId) nextParams.set('projectId', projectId) - setSearchParams(nextParams) - }} - > -
- {sidebarWidth === 'wide' ? ( -
-
- {task.status === "finished" && } - {task.status === "error" && } - {(task.status === "pending" || task.status === "processing") && } -
-
- {task.summary || stripMarkdown(task.content)} -
-
- {dayjs.unix(task.created_at as number).fromNow()} -
-
- ) : ( -
- {task.summary || stripMarkdown(task.content)} -
- )} -
-
- )) - )} -
-
-
-
- {selectedTaskId ? ( -