R29-1 R29-6: 重构海音达工作台壳层与高频页面

这个提交包含在:
2026-03-23 09:59:35 +08:00
父节点 f9198a781a
当前提交 7e183c0cdc
共修改 33 个文件,包含 2193 行新增和 1564 行删除
+136 -105
查看文件
@@ -1,8 +1,10 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { useSearchParams } from "react-router-dom";
import { fetchWecomConfig, startWecomBind, unbindWecom } from "../api/auth";
import { PageHeader } from "../components/common/PageHeader";
import { useAuth } from "../auth/AuthContext";
import { PageHeader } from "../components/common/PageHeader";
import { SectionPanel } from "../components/patterns/SectionPanel";
import { PageScaffold } from "../components/patterns/PageScaffold";
import { Button } from "../components/ui/button";
import {
resolveWecomBindNotice,
@@ -27,6 +29,12 @@ function maskExternalUserId(value: string) {
return `${value.slice(0, 4)}****${value.slice(-4)}`;
}
function statusTone(status?: string) {
if (status === "active") return "badge-success";
if (status === "inactive") return "badge-warning";
return "badge-neutral";
}
export function ProfilePage() {
const { me, permissions } = useAuth();
const [searchParams] = useSearchParams();
@@ -35,12 +43,14 @@ export function ProfilePage() {
queryFn: () => fetchWecomConfig(),
staleTime: 60_000,
});
const bindMutation = useMutation({
mutationFn: async () => startWecomBind("/settings/profile"),
onSuccess: (payload) => {
window.location.assign(payload.loginUrl);
},
});
const unbindMutation = useMutation({
mutationFn: () => unbindWecom(),
onSuccess: () => {
@@ -53,115 +63,136 @@ export function ProfilePage() {
const isBound = Boolean(me?.wecomBinding?.externalUserId);
return (
<div>
<PageHeader title="个人设置" subtitle="维护个人信息、权限摘要和企业微信绑定状态。" />
<PageScaffold>
<PageHeader
title="个人设置"
eyebrow="账号与入口"
subtitle="维护个人资料、角色摘要与企业微信绑定状态。复杂业务仍在海音达工作台内完成,企业微信只负责进入与提醒。"
meta={<span className={statusTone(me?.status)}>{me?.status === "active" ? "账号正常" : "账号状态待确认"}</span>}
/>
<section className="card-std">
<h3 className="card-title">个人信息</h3>
<dl className="grid gap-3 text-sm text-zinc-700 md:grid-cols-2">
<div>
<dt className="text-secondary">姓名</dt>
<dd className="font-medium text-zinc-900">{me?.name ?? "-"}</dd>
</div>
<div>
<dt className="text-secondary">员工号</dt>
<dd className="font-medium text-zinc-900">{me?.employeeNo ?? "-"}</dd>
</div>
<div>
<dt className="text-secondary">邮箱</dt>
<dd className="font-medium text-zinc-900">{me?.email ?? "-"}</dd>
</div>
<div>
<dt className="text-secondary">手机号</dt>
<dd className="font-medium text-zinc-900">{me?.phone ?? "-"}</dd>
</div>
<div>
<dt className="text-secondary">部门 ID</dt>
<dd className="font-medium text-zinc-900">{me?.departmentId ?? "-"}</dd>
</div>
<div>
<dt className="text-secondary">直属上级 ID</dt>
<dd className="font-medium text-zinc-900">{me?.directManagerId ?? "-"}</dd>
</div>
</dl>
</section>
<section className="card-std mt-4 space-y-4">
<div>
<h3 className="card-title">企业微信绑定</h3>
<p className="text-sm text-zinc-500">绑定后可从企业微信工作台直接进入 OA;企业微信只做入口与提醒,不承接第二套业务录入。</p>
</div>
{bindNotice ? (
<div className={`rounded-xl border px-4 py-3 text-sm ${noticeClasses(bindNotice.tone)}`} data-testid="profile-wecom-notice">
<div className="font-medium">{bindNotice.title}</div>
<div className="mt-1">{bindNotice.description}</div>
</div>
) : null}
<div className="grid gap-4 md:grid-cols-2">
<div className="rounded-2xl border border-zinc-200 p-4">
<div className="text-xs font-semibold uppercase tracking-wide text-zinc-500">接入状态</div>
<div className="mt-2 text-lg font-semibold text-zinc-900">{wecomStatus.label}</div>
<div className="mt-1 text-sm text-zinc-600">{wecomStatus.description}</div>
{wecomStatus.reasonLabels.length ? (
<ul className="mt-3 list-disc space-y-1 pl-5 text-xs text-zinc-500">
{wecomStatus.reasonLabels.map((label) => (
<li key={label}>{label}</li>
))}
</ul>
) : null}
</div>
<div className="rounded-2xl border border-zinc-200 p-4">
<div className="text-xs font-semibold uppercase tracking-wide text-zinc-500">当前绑定状态</div>
<div className="mt-2 text-lg font-semibold text-zinc-900">{isBound ? "已绑定" : "未绑定"}</div>
<div className="mt-1 text-sm text-zinc-600">
{isBound
? "已完成绑定,下次可从企业微信工作台直接进入 OA。"
: "未绑定时,企业微信登录会回退到账号密码登录。"}
</div>
{isBound ? (
<div className="mt-3 rounded-xl border border-zinc-200 bg-zinc-50 px-3 py-2 text-xs text-zinc-500">
绑定标识:{maskExternalUserId(me?.wecomBinding?.externalUserId ?? "")}<br />
绑定时间:{me?.wecomBinding?.createdAt ? new Date(me.wecomBinding.createdAt).toLocaleString() : "-"}
<div className="page-shell-grid">
<div className="space-y-6">
<SectionPanel title="基本信息" description="员工可直接理解的个人资料与联系方式。">
<dl className="grid gap-4 text-sm text-zinc-700 md:grid-cols-2">
<div>
<dt className="text-zinc-500">姓名</dt>
<dd className="mt-1 font-medium text-zinc-900">{me?.name ?? "-"}</dd>
</div>
) : null}
</div>
<div>
<dt className="text-zinc-500">员工号</dt>
<dd className="mt-1 font-medium text-zinc-900">{me?.employeeNo ?? "-"}</dd>
</div>
<div>
<dt className="text-zinc-500">邮箱</dt>
<dd className="mt-1 font-medium text-zinc-900">{me?.email ?? "-"}</dd>
</div>
<div>
<dt className="text-zinc-500">手机号</dt>
<dd className="mt-1 font-medium text-zinc-900">{me?.phone ?? "-"}</dd>
</div>
</dl>
</SectionPanel>
<SectionPanel title="企业微信绑定" description="绑定后可从企业微信工作台直接进入海音达工作台;消息只做提醒与回流,不承接第二套业务录入。">
<div className="space-y-4" data-testid="profile-wecom-card">
{bindNotice ? (
<div className={`rounded-2xl border px-4 py-3 text-sm ${noticeClasses(bindNotice.tone)}`} data-testid="profile-wecom-notice">
<div className="font-medium">{bindNotice.title}</div>
<div className="mt-1">{bindNotice.description}</div>
</div>
) : null}
<div className="grid gap-4 md:grid-cols-2">
<div className="surface-muted px-4 py-4">
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-zinc-400">接入状态</div>
<div className="mt-2 text-lg font-semibold text-zinc-900">{wecomStatus.label}</div>
<div className="mt-2 text-sm leading-6 text-zinc-600">{wecomStatus.description}</div>
{wecomStatus.reasonLabels.length ? (
<ul className="mt-3 list-disc space-y-1 pl-5 text-xs text-zinc-500">
{wecomStatus.reasonLabels.map((label) => (
<li key={label}>{label}</li>
))}
</ul>
) : null}
</div>
<div className="surface-muted px-4 py-4">
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-zinc-400">当前绑定状态</div>
<div className="mt-2 text-lg font-semibold text-zinc-900">{isBound ? "已绑定" : "未绑定"}</div>
<div className="mt-2 text-sm leading-6 text-zinc-600">
{isBound ? "下次可直接从企业微信工作台进入海音达工作台。" : "未绑定时,企业微信登录会自动回退到账号密码登录。"}
</div>
{isBound ? (
<div className="mt-3 rounded-xl border border-zinc-200 bg-white px-3 py-3 text-xs text-zinc-500">
<div>绑定标识:{maskExternalUserId(me?.wecomBinding?.externalUserId ?? "")}</div>
<div className="mt-1">绑定时间:{me?.wecomBinding?.createdAt ? new Date(me.wecomBinding.createdAt).toLocaleString() : "-"}</div>
</div>
) : null}
</div>
</div>
<div className="flex flex-wrap gap-2">
<Button
type="button"
disabled={!wecomConfigQuery.data?.bindEnabled || bindMutation.isPending}
onClick={() => bindMutation.mutate()}
>
{isBound ? "重新绑定企业微信" : "立即绑定企业微信"}
</Button>
<Button
type="button"
variant="secondary"
disabled={!isBound || unbindMutation.isPending}
onClick={() => unbindMutation.mutate()}
>
解绑
</Button>
</div>
</div>
</SectionPanel>
</div>
<div className="flex flex-wrap gap-2">
<Button
type="button"
disabled={!wecomConfigQuery.data?.bindEnabled || bindMutation.isPending}
onClick={() => bindMutation.mutate()}
>
{isBound ? "重新绑定企业微信" : "立即绑定企业微信"}
</Button>
<Button
type="button"
variant="secondary"
disabled={!isBound || unbindMutation.isPending}
onClick={() => unbindMutation.mutate()}
>
解绑
</Button>
</div>
</section>
<div className="space-y-6">
<SectionPanel title="组织信息" description="对员工展示可理解的说明,不直接暴露内部标识字段。">
<div className="space-y-4 text-sm text-zinc-600">
<div className="surface-muted px-4 py-4">
<div className="font-medium text-zinc-900">组织归属</div>
<div className="mt-2 leading-6">部门与直属汇报关系由管理员统一维护,当前个人页只展示员工可理解的设置入口,避免把内部 ID 直接暴露给员工。</div>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="rounded-2xl border border-zinc-200 px-4 py-4">
<div className="text-zinc-500">部门维护状态</div>
<div className="mt-2 font-medium text-zinc-900">{me?.departmentId ? "已关联组织" : "待管理员维护"}</div>
</div>
<div className="rounded-2xl border border-zinc-200 px-4 py-4">
<div className="text-zinc-500">直属汇报关系</div>
<div className="mt-2 font-medium text-zinc-900">{me?.directManagerId ? "已维护" : "待管理员维护"}</div>
</div>
</div>
</div>
</SectionPanel>
<section className="card-std mt-4">
<h3 className="card-title">权限列表</h3>
<div className="flex flex-wrap gap-2">
{permissions.map((permission) => (
<span
className="rounded-full bg-primary-50 px-2.5 py-1 text-xs font-medium text-primary"
key={permission}
>
{permission}
</span>
))}
<SectionPanel title="账号状态与角色" description="角色与权限保持透明,但不让设置页变成技术字段堆叠。">
<div className="space-y-4 text-sm text-zinc-600">
<div className="flex flex-wrap gap-2">
<span className={statusTone(me?.status)}>{me?.status === "active" ? "active" : me?.status ?? "unknown"}</span>
{(me?.roleCodes ?? []).map((role) => (
<span className="badge-neutral" key={role}>{role}</span>
))}
</div>
<div>
<div className="text-sm font-medium text-zinc-900">权限摘要</div>
<div className="mt-2 flex flex-wrap gap-2">
{permissions.map((permission) => (
<span className="badge-neutral" key={permission}>{permission}</span>
))}
</div>
</div>
</div>
</SectionPanel>
</div>
</section>
</div>
</div>
</PageScaffold>
);
}