统一业务列表 Ant Design 风格(05-31-unify-business-list-ant-design)

这个提交包含在:
2026-05-31 11:39:46 +08:00
父节点 59c942da54
当前提交 40c23c5cf3
共修改 19 个文件,包含 905 行新增和 578 行删除
+74 -18
查看文件
@@ -14,7 +14,12 @@ import { useAuth } from "../auth/AuthContext";
import { hasAnyPermission } from "../auth/permissions";
import { DateTextInput } from "../components/common/DateTextInput";
import { StatusBadge } from "../components/common/StatusBadge";
import { FilterBar, ListWorkspace, WorkspaceHeader, WorkspaceNotice } from "../components/patterns/ListWorkspace";
import { ListWorkspace, WorkspaceHeader, WorkspaceNotice } from "../components/patterns/ListWorkspace";
import {
BusinessListToolbar,
BusinessMobileList,
BusinessTable,
} from "../components/patterns/BusinessListTemplate";
import { SectionPanel } from "../components/patterns/SectionPanel";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
@@ -255,8 +260,15 @@ export function ProjectsPage() {
</SectionPanel>
) : null}
<div className="space-y-6">
<FilterBar className="grid gap-3 md:grid-cols-[minmax(0,1fr),180px,160px]">
<div className="space-y-4">
<BusinessListToolbar
actions={
<Button type="button" size="sm" variant={mine ? "default" : "secondary"} onClick={() => updateSearch({ mine: !mine })}>
{mine ? "我相关" : "全范围"}
</Button>
}
>
<div className="grid gap-3 md:grid-cols-[minmax(0,1fr),180px]">
<Input placeholder="搜索项目编号、项目名、客户名" value={q} onChange={(event) => updateSearch({ q: event.target.value })} />
<select className="select-std" value={status} onChange={(event) => updateSearch({ status: event.target.value })}>
{STATUS_OPTIONS.map((item) => (
@@ -265,10 +277,8 @@ export function ProjectsPage() {
</option>
))}
</select>
<Button type="button" variant={mine ? "default" : "secondary"} onClick={() => updateSearch({ mine: !mine })}>
{mine ? "我相关" : "全范围"}
</Button>
</FilterBar>
</div>
</BusinessListToolbar>
{!showTrialData && hiddenTrialProjectCount > 0 ? (
<WorkspaceNotice title={`已隐藏 ${hiddenTrialProjectCount} 条 TEST/SMOKE 项目`} tone="neutral">
@@ -276,18 +286,16 @@ export function ProjectsPage() {
</WorkspaceNotice>
) : null}
<SectionPanel title="项目列表">
<div className="space-y-4">
{listQuery.isLoading ? <div className="text-sm text-zinc-500">项目加载中...</div> : null}
{listQuery.isError ? <div className="text-sm text-red-600">{resolveError(listQuery.error, "加载项目失败")}</div> : null}
<div className="grid gap-4">
<BusinessMobileList>
{visibleProjectItems.map((item) => (
<Link key={item.id} to={`/projects/${item.id}`} className="card-std block space-y-3 p-5 hover:border-primary/40">
<Link key={item.id} to={`/projects/${item.id}`} className="block rounded-lg border border-zinc-200 bg-white p-3 text-sm">
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div>
<div className="flex items-center gap-3">
<div className="text-lg font-semibold text-zinc-900">{item.projectNo}</div>
<div className="font-semibold text-zinc-900">{item.projectNo}</div>
<StatusBadge status={item.status} />
{item.isConfidential ? <span className="rounded-full bg-amber-100 px-2 py-1 text-xs text-amber-700">涉密</span> : null}
</div>
@@ -305,13 +313,61 @@ export function ProjectsPage() {
</Link>
))}
{!listQuery.isLoading && !visibleProjectItems.length ? (
<div className="rounded-xl border border-dashed border-zinc-200 bg-zinc-50 px-4 py-8 text-sm text-zinc-500">
当前没有符合筛选条件的项目。
</div>
<div className="rounded-lg border border-zinc-200 bg-white p-4 text-sm text-zinc-500">暂无项目</div>
) : null}
</div>
</div>
</SectionPanel>
</BusinessMobileList>
<BusinessTable testId="projects-table">
<thead className="table-header">
<tr>
<th className="table-th">项目</th>
<th className="table-th">客户</th>
<th className="table-th">负责人</th>
<th className="table-th">进度</th>
<th className="table-th">状态</th>
<th className="table-th w-28">操作</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-50">
{visibleProjectItems.length ? (
visibleProjectItems.map((item) => (
<tr key={item.id} className="table-row">
<td className="table-td">
<div className="font-semibold text-zinc-900">{item.projectNo}</div>
<div className="text-xs text-zinc-500">{item.name}</div>
{item.isConfidential ? <div className="text-xs text-amber-700">涉密</div> : null}
</td>
<td className="table-td">
<div className="font-medium text-zinc-900">{item.clientAlias || item.clientName}</div>
<div className="line-clamp-2 text-xs text-zinc-500">{item.summary || "-"}</div>
</td>
<td className="table-td">
<div className="font-medium text-zinc-900">{item.owner?.name ?? "-"}</div>
<div className="text-xs text-zinc-500">项目经理 {item.manager?.name ?? "-"}</div>
</td>
<td className="table-td">
<div className="text-xs text-zinc-500">{summaryLine(item)}</div>
<div className="mt-1 text-xs text-zinc-500">目标 {formatDisplayDate(item.targetEndAt)}</div>
</td>
<td className="table-td">
<StatusBadge status={item.status} />
</td>
<td className="table-td">
<Button asChild type="button" size="sm" variant="secondary">
<Link to={`/projects/${item.id}`}>查看详情</Link>
</Button>
</td>
</tr>
))
) : (
<tr>
<td className="py-8 text-center text-sm text-zinc-500" colSpan={6}>
暂无项目
</td>
</tr>
)}
</tbody>
</BusinessTable>
</div>
</ListWorkspace>
);