feat: 完成 R13/R14/R15/R16 小团队管理核心能力

这个提交包含在:
2026-03-21 20:31:34 +08:00
父节点 0826693a8f
当前提交 1e52f1c225
共修改 47 个文件,包含 4298 行新增和 306 行删除
+58 -19
查看文件
@@ -5,21 +5,21 @@ import { isApiError } from "../api/types";
import { mapApiErrorMessage } from "../api/errorMessages";
import { PageHeader } from "../components/common/PageHeader";
import { readRecentVisits } from "../lib/recentVisits";
import { useAuth } from "../auth/AuthContext";
import { hasAnyPermission } from "../auth/permissions";
import { Button } from "../components/ui/button";
function domainLabel(domain: string) {
if (domain === "reimbursement") return "报销";
if (domain === "leave") return "请假";
if (domain === "purchase_request") return "采购";
if (domain === "travel_request") return "差旅";
return domain;
}
export function PortalHomePage() {
const portalQuery = useQuery({
queryKey: ["portal-home"],
queryFn: () => fetchPortalHome(),
});
const { permissions } = useAuth();
const portalQuery = useQuery({ queryKey: ["portal-home"], queryFn: () => fetchPortalHome() });
const recentVisits = readRecentVisits();
const canSetupWorkspace = hasAnyPermission(permissions, ["system.settings"]);
if (portalQuery.isLoading) {
return <div className="card-std p-6 text-sm text-zinc-500">加载 Portal 中...</div>;
@@ -31,9 +31,8 @@ export function PortalHomePage() {
: "Portal 加载失败";
return (
<div className="space-y-6">
<PageHeader title="Portal" subtitle="统一入口与待办概览。" />
<PageHeader title="Portal" subtitle="小团队统一入口与高频待办概览。" />
<div className="card-std p-6 text-sm text-red-600">{message}</div>
<div className="card-std p-6 text-sm text-zinc-600">如 `portal_home_v1` 尚未开启,可先使用 `仪表盘`。</div>
</div>
);
}
@@ -44,7 +43,19 @@ export function PortalHomePage() {
<div className="space-y-6">
<PageHeader title="Portal" subtitle={`你好,${payload.greeting.name}。`} />
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{!payload.workspace.persisted && canSetupWorkspace ? (
<div className="card-std border-amber-200 bg-amber-50 p-4 flex items-center justify-between gap-3">
<div>
<div className="font-medium text-zinc-900">工作区尚未初始化</div>
<div className="text-sm text-zinc-600">建议先完成小团队模式的导航与模块设置,再邀请全员使用。</div>
</div>
<Button asChild type="button">
<Link to="/system/workspace">去初始化</Link>
</Button>
</div>
) : null}
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-6">
{payload.cards.map((card) => (
<Link
key={card.key}
@@ -85,6 +96,12 @@ export function PortalHomePage() {
</Link>
))}
</div>
{payload.todayPresence ? (
<div className="rounded-lg border border-zinc-200 px-3 py-3 text-sm">
今日状态:<span className="font-medium text-zinc-900">{payload.todayPresence.label}</span>
{payload.todayPresence.note ? <div className="mt-1 text-zinc-500">{payload.todayPresence.note}</div> : null}
</div>
) : null}
</div>
<div className="card-std space-y-3 p-6">
@@ -115,19 +132,41 @@ export function PortalHomePage() {
</div>
<div className="card-std space-y-3 p-6">
<h3 className="text-base font-semibold text-zinc-900">待办与风险</h3>
<Link to={payload.pendingTasks.href} className="block rounded-lg border border-zinc-200 px-3 py-3 text-sm hover:bg-zinc-50">
我的待办:<span className="font-medium text-zinc-900">{payload.pendingTasks.count}</span>
</Link>
{payload.riskSummary ? (
<Link to={payload.riskSummary.href} className="block rounded-lg border border-amber-200 bg-amber-50 px-3 py-3 text-sm hover:bg-amber-100">
待复核风险:<span className="font-medium text-zinc-900">{payload.riskSummary.openCount}</span>
<h3 className="text-base font-semibold text-zinc-900">工作台摘要</h3>
<div className="space-y-3 text-sm">
{payload.teamPresence ? (
<Link to={payload.teamPresence.href} className="block rounded-lg border border-zinc-200 px-3 py-3 hover:bg-zinc-50">
今日团队状态:在岗 {payload.teamPresence.office} / 总数 {payload.teamPresence.total},请假 {payload.teamPresence.leave},未更新 {payload.teamPresence.unspecified}
</Link>
) : null}
{payload.weeklySummary ? (
<Link to={payload.weeklySummary.href} className="block rounded-lg border border-zinc-200 px-3 py-3 hover:bg-zinc-50">
本周周报:未提交 {payload.weeklySummary.missingCount},需协助 {payload.weeklySummary.needsHelpCount}
</Link>
) : null}
{payload.todoSummary ? (
<Link to={payload.todoSummary.href} className="block rounded-lg border border-zinc-200 px-3 py-3 hover:bg-zinc-50">
我的待办:未完成 {payload.todoSummary.openCount},高优先级 {payload.todoSummary.highPriorityCount}
</Link>
) : null}
{payload.peopleLifecycle ? (
<Link to={payload.peopleLifecycle.href} className="block rounded-lg border border-zinc-200 px-3 py-3 hover:bg-zinc-50">
人员闭环:入职 {payload.peopleLifecycle.onboardingCount},离职 {payload.peopleLifecycle.offboardingCount},资产待归还 {payload.peopleLifecycle.assetReturnPendingCount}
</Link>
) : null}
<Link to={payload.pendingTasks.href} className="block rounded-lg border border-zinc-200 px-3 py-3 text-sm hover:bg-zinc-50">
审批待办:<span className="font-medium text-zinc-900">{payload.pendingTasks.count}</span>
</Link>
) : (
<div className="rounded-lg border border-zinc-200 px-3 py-3 text-sm text-zinc-500">当前没有开放风险项</div>
)}
{payload.riskSummary ? (
<Link to={payload.riskSummary.href} className="block rounded-lg border border-amber-200 bg-amber-50 px-3 py-3 text-sm hover:bg-amber-100">
待复核风险:<span className="font-medium text-zinc-900">{payload.riskSummary.openCount}</span>
</Link>
) : null}
</div>
</div>
</div>
</div>
);
}
export default PortalHomePage;