import { Link, useNavigate, useSearchParams } from "react-router-dom"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMemo, useState } from "react"; import { fetchEmployees } from "../api/employees"; import { mapApiErrorMessage } from "../api/errorMessages"; import { createProject, fetchProjects, type ProjectListItem, type ProjectStatus, } from "../api/projects"; import { isApiError } from "../api/types"; import { useAuth } from "../auth/AuthContext"; import { hasAnyPermission } from "../auth/permissions"; import { PageHeader } from "../components/common/PageHeader"; import { StatusBadge } from "../components/common/StatusBadge"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { Label } from "../components/ui/label"; import { Textarea } from "../components/ui/textarea"; import { toast } from "sonner"; const STATUS_OPTIONS: Array<{ value: "all" | ProjectStatus; label: string }> = [ { value: "all", label: "全部状态" }, { value: "draft", label: "草稿" }, { value: "active", label: "进行中" }, { value: "paused", label: "暂停" }, { value: "completed", label: "完成" }, { value: "archived", label: "归档" }, ]; function resolveError(error: unknown, fallback = "操作失败") { if (isApiError(error)) { return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`; } return error instanceof Error ? error.message : fallback; } function summaryLine(item: ProjectListItem) { const parts = [ `成员 ${item.counts.members}`, `里程碑 ${item.counts.milestones}`, `任务 ${item.counts.tasks}`, `风险 ${item.counts.risks}`, ]; if (item.counts.overdueMilestones > 0) { parts.push(`逾期 ${item.counts.overdueMilestones}`); } if (item.counts.highPriorityTasks > 0) { parts.push(`高优任务 ${item.counts.highPriorityTasks}`); } return parts.join(" · "); } export function ProjectsPage() { const navigate = useNavigate(); const queryClient = useQueryClient(); const { permissions, me } = useAuth(); const [searchParams, setSearchParams] = useSearchParams(); const q = searchParams.get("q") ?? ""; const status = (searchParams.get("status") as "all" | ProjectStatus | null) ?? "all"; const mine = searchParams.get("mine") === "1"; const canManageProjects = hasAnyPermission(permissions, ["project.manage", "system.settings", "report.dashboard.view"]); const [name, setName] = useState(""); const [clientName, setClientName] = useState(""); const [clientAlias, setClientAlias] = useState(""); const [summary, setSummary] = useState(""); const [managerEmployeeId, setManagerEmployeeId] = useState(""); const [startsAt, setStartsAt] = useState(""); const [targetEndAt, setTargetEndAt] = useState(""); const [isConfidential, setIsConfidential] = useState(false); const listQuery = useQuery({ queryKey: ["projects", "list", status, mine, q], queryFn: () => fetchProjects({ q: q || undefined, mine, status: status === "all" ? undefined : status, }), }); const employeesQuery = useQuery({ enabled: canManageProjects, queryKey: ["employees", "projects-managers"], queryFn: () => fetchEmployees({ page: 1, pageSize: 100, status: "active" }), }); const managerCandidates = useMemo( () => (employeesQuery.data?.items ?? []).filter((item) => item.id !== me?.id), [employeesQuery.data?.items, me?.id], ); const createMutation = useMutation({ mutationFn: () => createProject({ name: name.trim(), clientName: clientName.trim(), clientAlias: clientAlias.trim() || null, summary: summary.trim() || null, managerEmployeeId: managerEmployeeId || null, startsAt: startsAt || null, targetEndAt: targetEndAt || null, isConfidential, status: "draft", }), onSuccess: async (payload) => { setName(""); setClientName(""); setClientAlias(""); setSummary(""); setManagerEmployeeId(""); setStartsAt(""); setTargetEndAt(""); setIsConfidential(false); await queryClient.invalidateQueries({ queryKey: ["projects"] }); toast.success("项目已创建"); navigate(`/projects/${payload.item.id}`); }, onError: (error) => toast.error(resolveError(error, "创建项目失败")), }); function updateSearch(next: Partial<{ q: string; status: string; mine: boolean }>) { const params = new URLSearchParams(searchParams); if (next.q !== undefined) { if (next.q) params.set("q", next.q); else params.delete("q"); } if (next.status !== undefined) { if (next.status && next.status !== "all") params.set("status", next.status); else params.delete("status"); } if (next.mine !== undefined) { if (next.mine) params.set("mine", "1"); else params.delete("mine"); } setSearchParams(params, { replace: true }); } return (
} />
updateSearch({ q: event.target.value })} />
{listQuery.isLoading ?
项目加载中...
: null} {listQuery.isError ?
{resolveError(listQuery.error, "加载项目失败")}
: null}
{(listQuery.data?.items ?? []).map((item) => (
{item.projectNo}
{item.isConfidential ? 涉密 : null}
{item.name}
客户:{item.clientAlias || item.clientName}
负责人:{item.owner?.name ?? "-"}
项目经理:{item.manager?.name ?? "-"}
目标完成:{item.targetEndAt ?? "-"}
{item.summary || "暂无项目摘要"}
{summaryLine(item)}
))} {!listQuery.isLoading && !(listQuery.data?.items ?? []).length ? (
当前没有符合筛选条件的项目。
) : null}

新建项目

{!canManageProjects ? (
你当前只有查看权限,项目创建入口仅对老板 / 管理员 / 项目管理角色开放。
) : ( <>
setName(event.target.value)} placeholder="例如:深圳某客户现场实施" />
setClientName(event.target.value)} placeholder="客户全称" />
setClientAlias(event.target.value)} placeholder="可选" />
setStartsAt(event.target.value)} />
setTargetEndAt(event.target.value)} />