151 行
6.4 KiB
TypeScript
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>
|
|
);
|
|
}
|