feat: R27-2 R27-5 优化企微入口与 Portal 体验

这个提交包含在:
2026-03-22 19:52:54 +08:00
父节点 eaef213d22
当前提交 312200ec1b
共修改 16 个文件,包含 1213 行新增和 315 行删除
+99 -17
查看文件
@@ -4,23 +4,57 @@ import { fetchWecomConfig, startWecomBind, unbindWecom } from "../api/auth";
import { PageHeader } from "../components/common/PageHeader";
import { useAuth } from "../auth/AuthContext";
import { Button } from "../components/ui/button";
import {
resolveWecomBindNotice,
resolveWecomEntryStatus,
type WecomNotice,
} from "../lib/wecomEntry";
function noticeClasses(tone: WecomNotice["tone"]) {
if (tone === "success") {
return "border-emerald-200 bg-emerald-50 text-emerald-700";
}
if (tone === "warning") {
return "border-amber-200 bg-amber-50 text-amber-700";
}
return "border-zinc-200 bg-zinc-50 text-zinc-700";
}
function maskExternalUserId(value: string) {
if (value.length <= 8) {
return value;
}
return `${value.slice(0, 4)}****${value.slice(-4)}`;
}
export function ProfilePage() {
const { me, permissions } = useAuth();
const [searchParams] = useSearchParams();
const wecomConfigQuery = useQuery({ queryKey: ["auth", "wecom-config"], queryFn: () => fetchWecomConfig(), staleTime: 60_000 });
const wecomConfigQuery = useQuery({
queryKey: ["auth", "wecom-config"],
queryFn: () => fetchWecomConfig(),
staleTime: 60_000,
});
const bindMutation = useMutation({
mutationFn: async () => startWecomBind("/settings/profile"),
onSuccess: (payload) => { window.location.assign(payload.loginUrl); },
onSuccess: (payload) => {
window.location.assign(payload.loginUrl);
},
});
const unbindMutation = useMutation({
mutationFn: () => unbindWecom(),
onSuccess: () => { window.location.reload(); },
onSuccess: () => {
window.location.reload();
},
});
const wecomStatus = resolveWecomEntryStatus(wecomConfigQuery.data);
const bindNotice = resolveWecomBindNotice(searchParams.get("wecomBind"));
const isBound = Boolean(me?.wecomBinding?.externalUserId);
return (
<div>
<PageHeader title="个人设置" subtitle="展示当前登录人的基础信息与权限摘要。" />
<PageHeader title="个人设置" subtitle="维护个人信息、权限摘要和企业微信绑定状态。" />
<section className="card-std">
<h3 className="card-title">个人信息</h3>
@@ -42,28 +76,76 @@ export function ProfilePage() {
<dd className="font-medium text-zinc-900">{me?.phone ?? "-"}</dd>
</div>
<div>
<dt className="text-secondary">部门ID</dt>
<dt className="text-secondary">部门 ID</dt>
<dd className="font-medium text-zinc-900">{me?.departmentId ?? "-"}</dd>
</div>
<div>
<dt className="text-secondary">直属上级ID</dt>
<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">
<h3 className="card-title">企业微信绑定</h3>
<div className="space-y-3 text-sm text-zinc-700">
<div>接入状态:<span className="font-medium text-zinc-900">{wecomConfigQuery.data?.enabled ? "已启用" : "未启用"}</span></div>
<div>绑定状态:<span className="font-medium text-zinc-900">{me?.wecomBinding?.externalUserId ?? "未绑定"}</span></div>
{searchParams.get("wecomBind") ? (
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2">绑定结果:{searchParams.get("wecomBind")}</div>
) : null}
<div className="flex flex-wrap gap-2">
<Button type="button" variant="secondary" disabled={!wecomConfigQuery.data?.enabled || bindMutation.isPending} onClick={() => bindMutation.mutate()}>绑定企业微信</Button>
<Button type="button" variant="secondary" disabled={!me?.wecomBinding || unbindMutation.isPending} onClick={() => unbindMutation.mutate()}>解绑企业微信</Button>
<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>
) : 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>
</section>