138 lines
5.9 KiB
TypeScript
138 lines
5.9 KiB
TypeScript
import { Link, useParams } from "react-router-dom";
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import { ArrowLeft, ListTodo } from "lucide-react";
|
||
import { getUserSkillTasks } from "@/api/skill";
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||
import { Button } from "@/components/ui/button";
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow,
|
||
} from "@/components/ui/table";
|
||
import { toZhPriority, toZhStatus } from "@/lib/display";
|
||
import { formatBeijingDateTime } from "@/lib/datetime";
|
||
|
||
export default function SkillTasksPage() {
|
||
const { id: userId, skillId } = useParams();
|
||
const { data, isLoading, error, refetch } = useQuery({
|
||
queryKey: ["skill-tasks", userId, skillId],
|
||
queryFn: () => getUserSkillTasks(userId!, skillId!),
|
||
enabled: !!userId && !!skillId,
|
||
});
|
||
|
||
const items = data?.items ?? [];
|
||
const total = data?.total ?? items.length;
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<div className="flex flex-wrap items-center gap-4">
|
||
<Button variant="ghost" size="icon" asChild>
|
||
<Link to={userId ? `/users/${userId}/profile` : "/users"}>
|
||
<ArrowLeft className="h-4 w-4" />
|
||
</Link>
|
||
</Button>
|
||
<div>
|
||
<h1 className="text-2xl font-bold tracking-tight">技能历史任务</h1>
|
||
<p className="text-muted-foreground">
|
||
用户 {userId} / 技能 {skillId}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>任务列表 ({total})</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{isLoading ? (
|
||
<p className="text-muted-foreground">加载中…</p>
|
||
) : error ? (
|
||
<div className="space-y-3">
|
||
<p className="text-destructive">
|
||
加载失败:{(error as Error).message || "请求异常"}
|
||
</p>
|
||
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
||
重试
|
||
</Button>
|
||
</div>
|
||
) : items.length === 0 ? (
|
||
<div className="flex flex-col items-center py-10 text-center text-muted-foreground">
|
||
<ListTodo className="mb-3 h-10 w-10 opacity-60" />
|
||
<p>暂无历史任务</p>
|
||
<p className="mt-1 text-xs">
|
||
当前后端可能尚未提供技能历史任务接口,页面已兼容空数据。
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<div className="hidden overflow-x-auto [-webkit-overflow-scrolling:touch] md:block">
|
||
<Table className="min-w-[720px]">
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead className="min-w-[240px] whitespace-nowrap">任务</TableHead>
|
||
<TableHead className="min-w-[90px] whitespace-nowrap">状态</TableHead>
|
||
<TableHead className="min-w-[90px] whitespace-nowrap">优先级</TableHead>
|
||
<TableHead className="min-w-[90px] whitespace-nowrap text-right">工时 (h)</TableHead>
|
||
<TableHead className="min-w-[170px] whitespace-nowrap">更新时间</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{items.map((task) => (
|
||
<TableRow key={task.id}>
|
||
<TableCell className="min-w-[240px]">
|
||
<div>
|
||
<p className="font-medium">{task.title}</p>
|
||
{task.description && (
|
||
<p className="line-clamp-1 text-xs text-muted-foreground">
|
||
{task.description}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</TableCell>
|
||
<TableCell className="min-w-[90px] whitespace-nowrap">{toZhStatus(task.status)}</TableCell>
|
||
<TableCell className="min-w-[90px] whitespace-nowrap">{toZhPriority(task.priority)}</TableCell>
|
||
<TableCell className="min-w-[90px] whitespace-nowrap text-right">
|
||
{typeof task.estimated_hours === "number"
|
||
? task.estimated_hours.toFixed(1)
|
||
: "-"}
|
||
</TableCell>
|
||
<TableCell className="min-w-[170px] whitespace-nowrap">
|
||
{task.updated_at
|
||
? formatBeijingDateTime(task.updated_at)
|
||
: "-"}
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
<div className="space-y-3 md:hidden">
|
||
{items.map((task) => (
|
||
<article key={task.id} className="rounded-lg border bg-white p-4">
|
||
<p className="text-base font-semibold">{task.title}</p>
|
||
{task.description && (
|
||
<p className="mt-1 line-clamp-2 text-sm text-muted-foreground">
|
||
{task.description}
|
||
</p>
|
||
)}
|
||
<div className="mt-3 flex items-center justify-between text-sm">
|
||
<span>状态:{toZhStatus(task.status)}</span>
|
||
<span>优先级:{toZhPriority(task.priority)}</span>
|
||
</div>
|
||
<div className="mt-2 flex items-center justify-between text-xs text-muted-foreground">
|
||
<span>工时:{typeof task.estimated_hours === "number" ? task.estimated_hours.toFixed(1) : "-"}h</span>
|
||
<span>{task.updated_at ? formatBeijingDateTime(task.updated_at) : "-"}</span>
|
||
</div>
|
||
</article>
|
||
))}
|
||
</div>
|
||
</>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|