文件
hyb-oa/client/src/pages/MobileMePage.tsx
T

151 行
6.4 KiB
TypeScript

import { Link } from "react-router-dom";
import { useMutation, useQuery } from "@tanstack/react-query";
import { toast } from "sonner";
import { updateNotificationPreferences } from "../api/auth";
import { fetchEmployeeProfile } from "../api/employees";
import { mapApiErrorMessage } from "../api/errorMessages";
import { isApiError } from "../api/types";
import { useAuth } from "../auth/AuthContext";
import { hasAnyPermission } from "../auth/permissions";
import { EmployeeAvatar } from "../components/common/EmployeeAvatar";
import { PageHeader } from "../components/common/PageHeader";
import { PageScaffold } from "../components/patterns/PageScaffold";
import { SectionPanel } from "../components/patterns/SectionPanel";
import { Button } from "../components/ui/button";
function resolveErrorMessage(error: unknown) {
if (isApiError(error)) {
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
}
if (error instanceof Error) {
return error.message;
}
return "请求失败,请稍后重试。";
}
function MobileActionLink({
to,
label,
description,
icon,
}: {
to: string;
label: string;
description?: string;
icon: string;
}) {
return (
<Link
to={to}
className="flex min-h-12 items-center gap-3 rounded-lg border border-zinc-200 bg-white px-3 py-2 text-left shadow-sm transition-colors hover:bg-zinc-50"
>
<span className="material-symbols-outlined flex size-8 shrink-0 items-center justify-center rounded-lg bg-zinc-100 text-[18px] text-zinc-700">
{icon}
</span>
<span className="min-w-0">
<span className="block text-sm font-semibold text-zinc-900">{label}</span>
{description ? <span className="mt-0.5 block text-xs leading-5 text-zinc-500">{description}</span> : null}
</span>
</Link>
);
}
export function MobileMePage() {
const { me, permissions, refreshMe, logout } = useAuth();
const canAccessApprovals = hasAnyPermission(permissions, ["expense.approve", "leave.approve", "procurement.approve", "travel.approve"]);
const canAccessAdmin = hasAnyPermission(permissions, ["*", "system.settings", "system.users", "report.dashboard.view"]);
const canAccessAssets = hasAnyPermission(permissions, ["asset.view_own", "asset.view_all", "asset.manage", "asset.transfer", "asset.inventory"]);
const canAccessProjects = hasAnyPermission(permissions, ["project.view_own", "project.view_all", "project.manage"]);
const profileQuery = useQuery({
enabled: Boolean(me?.id),
queryKey: ["mobile", "me", "profile", me?.id],
queryFn: () => fetchEmployeeProfile(me!.id),
});
const notificationPreferenceMutation = useMutation({
mutationFn: (notificationPushEnabled: boolean) => updateNotificationPreferences({ notificationPushEnabled }),
onSuccess: async () => {
await refreshMe();
toast.success("外部推送偏好已更新");
},
onError: (error) => {
toast.error(resolveErrorMessage(error));
},
});
const profile = profileQuery.data?.employee;
const departmentName = profile?.department?.name ?? (me?.departmentId ? "已关联组织" : "待管理员维护");
return (
<PageScaffold className="space-y-3 pb-4">
<PageHeader title="我的" />
<SectionPanel title="个人信息">
<div className="flex items-center gap-3">
<EmployeeAvatar name={me?.name ?? "员工"} src={profile?.avatarUrl} size="md" />
<div className="min-w-0 space-y-1">
<div className="text-lg font-semibold text-zinc-900">{me?.name ?? "-"}</div>
<div className="text-sm text-zinc-500">{departmentName}</div>
{me?.employeeNo ? <div className="text-xs text-zinc-400">员工号:{me.employeeNo}</div> : null}
</div>
</div>
{profileQuery.isLoading ? <p className="mt-4 text-sm text-zinc-500">加载档案信息中...</p> : null}
{profileQuery.isError ? (
<p className="mt-4 rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
{resolveErrorMessage(profileQuery.error)}
</p>
) : null}
</SectionPanel>
<SectionPanel title="账号与绑定">
<div className="grid gap-2">
<MobileActionLink to="/settings/profile" label="个人设置" description="企业微信、账号与通知" icon="manage_accounts" />
{me?.id ? (
<MobileActionLink to={`/people/${me.id}`} label="员工档案" description="办公地点和紧急联系人" icon="badge" />
) : null}
</div>
</SectionPanel>
<SectionPanel title="更多入口">
<div className="grid grid-cols-2 gap-2" data-testid="mobile-more-links">
<MobileActionLink to="/notifications" label="通知中心" icon="notifications" />
<MobileActionLink to="/mobile/leave" label="请假" icon="event_available" />
<MobileActionLink to="/mobile/reimbursements" label="报销" icon="request_quote" />
{canAccessApprovals ? <MobileActionLink to="/mobile/approvals" label="审批" icon="fact_check" /> : null}
{canAccessAssets ? <MobileActionLink to="/assets" label="资产" icon="inventory_2" /> : null}
{canAccessProjects ? <MobileActionLink to="/projects" label="项目" icon="workspaces" /> : null}
</div>
</SectionPanel>
{canAccessAdmin ? (
<SectionPanel title="管理入口">
<div className="grid grid-cols-2 gap-2">
<MobileActionLink to="/people" label="人员管理" icon="group" />
<MobileActionLink to="/organization" label="组织架构" icon="account_tree" />
<MobileActionLink to="/system/settings" label="系统设置" icon="settings" />
</div>
</SectionPanel>
) : null}
<SectionPanel title="外部通知推送">
<label className="flex items-center gap-3 text-sm text-zinc-800">
<input
type="checkbox"
className="h-4 w-4 rounded border-zinc-300"
checked={Boolean(me?.notificationPushEnabled ?? true)}
disabled={!me || notificationPreferenceMutation.isPending}
onChange={(event) => notificationPreferenceMutation.mutate(event.target.checked)}
/>
接收外部渠道推送
</label>
</SectionPanel>
<Button type="button" variant="secondary" className="w-full border-red-200 text-red-600 hover:bg-red-50" onClick={() => void logout()}>
退出登录
</Button>
</PageScaffold>
);
}