R29-1 R29-6: 重构海音达工作台壳层与高频页面
这个提交包含在:
+124
-60
@@ -1,8 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { cancelTodo, completeTodo, createTodo, fetchTodos } from "../api/todos";
|
||||
import { fetchProjects } from "../api/projects";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { FilterBar, ListWorkspace, PrimaryTable } from "../components/patterns/ListWorkspace";
|
||||
import { MetricCard } from "../components/patterns/MetricCard";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Input } from "../components/ui/input";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
||||
@@ -32,6 +35,20 @@ export function TodosPage() {
|
||||
queryFn: () => fetchProjects({}),
|
||||
});
|
||||
|
||||
const todoItems = todosQuery.data?.items ?? [];
|
||||
const todoStats = useMemo(() => {
|
||||
return todoItems.reduce(
|
||||
(acc, item) => {
|
||||
acc.total += 1;
|
||||
if (item.status === "todo") acc.todo += 1;
|
||||
if (item.status === "doing") acc.doing += 1;
|
||||
if (item.status === "done") acc.done += 1;
|
||||
return acc;
|
||||
},
|
||||
{ total: 0, todo: 0, doing: 0, done: 0 },
|
||||
);
|
||||
}, [todoItems]);
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: () => createTodo({ title, description: description || undefined, projectId: projectId || null }),
|
||||
onSuccess: async () => {
|
||||
@@ -72,69 +89,116 @@ export function TodosPage() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<ListWorkspace>
|
||||
<PageHeader title="待办" subtitle="员工维护个人待办,老板看高优先级和未完成项。支持直接挂到项目。" />
|
||||
|
||||
<div className="card-std p-6 space-y-4">
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
<Input placeholder="待办标题" value={title} onChange={(event) => setTitle(event.target.value)} />
|
||||
<Input placeholder="补充说明(可选)" value={description} onChange={(event) => setDescription(event.target.value)} />
|
||||
<select className="select-std" value={projectId} onChange={(event) => setProjectId(event.target.value)}>
|
||||
<option value="">不关联项目</option>
|
||||
{(projectsQuery.data?.items ?? []).map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.projectNo} · {item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex gap-2">
|
||||
<select className="select-std" value={statusFilter} onChange={(event) => setStatusFilter(event.target.value as typeof statusFilter)}>
|
||||
<option value="">全部状态</option>
|
||||
<option value="todo">todo</option>
|
||||
<option value="doing">doing</option>
|
||||
<option value="done">done</option>
|
||||
<option value="cancelled">cancelled</option>
|
||||
</select>
|
||||
<Button type="button" onClick={() => createMutation.mutate()} disabled={createMutation.isPending || !title.trim()}>新建待办</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<MetricCard title="全部待办" value={todoStats.total} note="当前筛选结果" />
|
||||
<MetricCard title="待处理" value={todoStats.todo} note="建议优先清空新增事项" tone={todoStats.todo > 0 ? "warning" : "neutral"} />
|
||||
<MetricCard title="进行中" value={todoStats.doing} note="重点跟进阻塞项" />
|
||||
<MetricCard title="已完成" value={todoStats.done} note="已完成项会持续留痕" />
|
||||
</section>
|
||||
|
||||
<div className="card-std p-6">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>标题</TableHead>
|
||||
<TableHead>项目</TableHead>
|
||||
<TableHead>负责人</TableHead>
|
||||
<TableHead>优先级</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{(todosQuery.data?.items ?? []).map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>
|
||||
<div className="font-medium text-zinc-900">{item.title}</div>
|
||||
<div className="text-xs text-zinc-500">{item.description ?? "-"}</div>
|
||||
</TableCell>
|
||||
<TableCell>{item.project ? `${item.project.projectNo} · ${item.project.name}` : "-"}</TableCell>
|
||||
<TableCell>{item.assignee?.name ?? "-"}</TableCell>
|
||||
<TableCell>{item.priority}</TableCell>
|
||||
<TableCell>{item.status}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" type="button" disabled={completeMutation.isPending || item.status === "done"} onClick={() => completeMutation.mutate(item.id)}>完成</Button>
|
||||
<Button size="sm" variant="secondary" type="button" disabled={cancelMutation.isPending || item.status === "cancelled"} onClick={() => cancelMutation.mutate(item.id)}>取消</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="page-shell-grid">
|
||||
<div className="space-y-6">
|
||||
<FilterBar className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="text-sm text-zinc-500">按状态筛选后,列表只保留当前关注的工作项。</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<select className="select-std w-[180px]" value={statusFilter} onChange={(event) => setStatusFilter(event.target.value as typeof statusFilter)}>
|
||||
<option value="">全部状态</option>
|
||||
<option value="todo">todo</option>
|
||||
<option value="doing">doing</option>
|
||||
<option value="done">done</option>
|
||||
<option value="cancelled">cancelled</option>
|
||||
</select>
|
||||
<Button type="button" variant="secondary" onClick={() => queryClient.invalidateQueries({ queryKey: ["todos"] })}>
|
||||
刷新列表
|
||||
</Button>
|
||||
</div>
|
||||
</FilterBar>
|
||||
|
||||
<SectionPanel title="待办列表" description={todoItems.length ? `当前共 ${todoItems.length} 条待办` : "暂无待办,建议从当前周计划或项目动作拆出任务。"}>
|
||||
{todosQuery.isError ? <div className="mb-4 text-sm text-red-600">{errorMessage(todosQuery.error, "加载待办失败")}</div> : null}
|
||||
<PrimaryTable>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>标题</TableHead>
|
||||
<TableHead>项目</TableHead>
|
||||
<TableHead>负责人</TableHead>
|
||||
<TableHead>优先级</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{todosQuery.isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell className="py-8 text-center text-sm text-zinc-500" colSpan={6}>
|
||||
待办加载中...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : todoItems.length ? (
|
||||
todoItems.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>
|
||||
<div className="font-medium text-zinc-900">{item.title}</div>
|
||||
<div className="text-xs text-zinc-500">{item.description ?? "-"}</div>
|
||||
</TableCell>
|
||||
<TableCell>{item.project ? `${item.project.projectNo} · ${item.project.name}` : "-"}</TableCell>
|
||||
<TableCell>{item.assignee?.name ?? "-"}</TableCell>
|
||||
<TableCell>{item.priority}</TableCell>
|
||||
<TableCell>{item.status}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" type="button" disabled={completeMutation.isPending || item.status === "done"} onClick={() => completeMutation.mutate(item.id)}>
|
||||
完成
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" type="button" disabled={cancelMutation.isPending || item.status === "cancelled"} onClick={() => cancelMutation.mutate(item.id)}>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell className="py-8 text-center text-sm text-zinc-500" colSpan={6}>
|
||||
当前没有符合条件的待办。
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</PrimaryTable>
|
||||
</SectionPanel>
|
||||
</div>
|
||||
|
||||
<SectionPanel title="新增待办" description="建议把周报计划、项目跟进或临时事项都拆成清晰动作。">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Input placeholder="待办标题" value={title} onChange={(event) => setTitle(event.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Input placeholder="补充说明(可选)" value={description} onChange={(event) => setDescription(event.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<select className="select-std" value={projectId} onChange={(event) => setProjectId(event.target.value)}>
|
||||
<option value="">不关联项目</option>
|
||||
{(projectsQuery.data?.items ?? []).map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.projectNo} · {item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<Button type="button" onClick={() => createMutation.mutate()} disabled={createMutation.isPending || !title.trim()}>
|
||||
{createMutation.isPending ? "新建中..." : "新建待办"}
|
||||
</Button>
|
||||
</div>
|
||||
</SectionPanel>
|
||||
</div>
|
||||
</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户