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

这个提交包含在:
2026-03-23 09:59:35 +08:00
父节点 f9198a781a
当前提交 7e183c0cdc
共修改 33 个文件,包含 2193 行新增和 1564 行删除
+117 -107
查看文件
@@ -88,7 +88,7 @@ export function LoginPage() {
} else if (err instanceof TypeError) {
setError("无法连接后端服务,请确认 server 已启动(http://127.0.0.1:3000)。");
} else {
setError("登录失败,请稍后重试");
setError("登录失败,请稍后重试。");
}
} finally {
setSubmitting(false);
@@ -102,125 +102,135 @@ export function LoginPage() {
window.location.assign(buildWecomLoginUrl(wecomEntry.loginUrl, redirectTo));
}
function renderPasswordPanel(testId: string, primary: boolean) {
return (
<div className="space-y-4 rounded-[1.5rem] border border-zinc-200 bg-white p-6 shadow-sm" data-testid={testId}>
<div>
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-zinc-400">{primary ? "推荐入口" : "回退入口"}</div>
<h2 className="mt-2 text-xl font-semibold tracking-tight text-zinc-900">使用账号密码登录</h2>
<p className="mt-2 text-sm leading-6 text-zinc-600">
{primary
? "适合桌面浏览器直接登录;若企业微信入口暂未开放,也可正常使用。"
: "企业微信免登失败、未绑定或超时时,可改用账号密码继续进入。"}
</p>
</div>
<form className="space-y-4" onSubmit={handleSubmit}>
<div className="space-y-2">
<Label htmlFor="login-identifier">账号</Label>
<Input
autoComplete="username"
data-testid="login-identifier"
id="login-identifier"
onChange={(event) => setIdentifier(event.target.value)}
placeholder="邮箱或手机号"
value={identifier}
/>
</div>
<div className="space-y-2">
<Label htmlFor="login-password">密码</Label>
<Input
autoComplete="current-password"
data-testid="login-password"
id="login-password"
onChange={(event) => setPassword(event.target.value)}
placeholder="请输入密码"
type="password"
value={password}
/>
</div>
{error ? <div className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">{error}</div> : null}
<Button data-testid="login-submit" className="w-full" disabled={submitting} type="submit" variant={primary ? "default" : "secondary"}>
{submitting ? "登录中..." : "登录并进入工作台"}
</Button>
</form>
</div>
);
}
return (
<div className="card-std overflow-hidden border-primary-100 p-0">
<div className="border-b border-zinc-200 bg-gradient-to-br from-primary-50 via-white to-zinc-50 p-6">
<p className="text-xs font-semibold uppercase tracking-wide text-primary">hyb-oa</p>
<h1 className="mt-2 text-2xl font-bold text-zinc-900">登录工作台</h1>
<p className="mt-2 text-sm text-secondary">审批、项目、周报和团队状态统一在 OA 完成。</p>
<div className="mt-4 rounded-xl border border-zinc-200 bg-white/90 px-4 py-3 text-sm text-zinc-600">
<div className="card-std overflow-hidden border-zinc-200 p-0 shadow-md">
<div className="border-b border-zinc-200 bg-gradient-to-br from-zinc-50 via-white to-zinc-100 px-6 py-6 sm:px-8 sm:py-7">
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-zinc-400">统一入口</div>
<h1 className="mt-2 text-3xl font-semibold tracking-tight text-zinc-900">登录海音达工作台</h1>
<p className="mt-3 max-w-2xl text-sm leading-6 text-zinc-600">
企业微信负责进入与提醒,真正的审批、项目、周报和团队留痕都在海音达工作台内完成。
</p>
<div className="mt-4 rounded-2xl border border-zinc-200 bg-white/90 px-4 py-3 text-sm text-zinc-600">
{isWecomContext
? "当前在企业微信中打开,推荐直接使用企业微信进入工作台。"
: "桌面浏览器可直接使用账号密码登录;企业微信仅作为入口与提醒。"}
? "当前在企业微信中打开,推荐直接使用企业微信进入工作台;失败时可随时回退到账号密码登录。"
: "当前为浏览器场景,推荐直接使用账号密码登录;企业微信入口会作为次级入口提供。"}
</div>
</div>
<div className="space-y-5 p-6">
<div className="space-y-5 px-6 py-6 sm:px-8 sm:py-7">
{wecomNotice ? (
<div className={`rounded-xl border px-4 py-3 text-sm ${noticeClasses(wecomNotice.tone)}`} data-testid="login-wecom-notice">
<div className={`rounded-2xl border px-4 py-3 text-sm ${noticeClasses(wecomNotice.tone)}`} data-testid="login-wecom-notice">
<div className="font-medium">{wecomNotice.title}</div>
<div className="mt-1">{wecomNotice.description}</div>
<div className="mt-1 leading-6">{wecomNotice.description}</div>
</div>
) : null}
{primaryMode === "wecom" && wecomEntry?.loginUrl ? (
<div className="space-y-3 rounded-2xl border border-primary-100 bg-primary-50/70 p-5" data-testid="login-wecom-primary-panel">
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-primary">推荐入口</p>
<h2 className="mt-1 text-lg font-semibold text-zinc-900">使用企业微信进入工作台</h2>
<p className="mt-1 text-sm text-zinc-600">在企业微信工作台打开时,优先使用免登入口;失败时仍可回退到账号密码登录。</p>
</div>
<Button className="w-full" data-testid="wecom-entry-primary" type="button" onClick={handleWecomLogin}>
使用企业微信进入工作台
</Button>
<p className="text-xs text-zinc-500">若当前企业微信账号尚未绑定,请先使用账号密码登录,再到个人设置完成绑定。</p>
</div>
) : null}
<div className="space-y-4 rounded-2xl border border-zinc-200 p-5" data-testid={primaryMode === "password" ? "login-password-primary-panel" : "login-password-secondary-panel"}>
<div>
<h2 className="text-lg font-semibold text-zinc-900">账号密码登录</h2>
<p className="mt-1 text-sm text-zinc-500">
{primaryMode === "password"
? "使用公司邮箱或手机号登录,复杂填写与审批仍建议在 Web OA 内完成。"
: "企业微信不可用时,可改用账号密码登录。"}
</p>
<p className="mt-1 text-xs text-zinc-500">忘记密码:请联系管理员重置。</p>
</div>
<form className="space-y-4" onSubmit={handleSubmit}>
<div className="space-y-1">
<Label htmlFor="identifier">账号</Label>
<Input
autoComplete="username"
data-testid="login-identifier"
id="identifier"
onChange={(event) => setIdentifier(event.target.value)}
placeholder="邮箱或手机号"
value={identifier}
/>
</div>
<div className="space-y-1">
<Label htmlFor="password">密码</Label>
<Input
autoComplete="current-password"
data-testid="login-password"
id="password"
onChange={(event) => setPassword(event.target.value)}
placeholder="请输入密码"
type="password"
value={password}
/>
</div>
{error ? (
<p
className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600"
data-testid="login-error"
>
{error}
</p>
) : null}
<Button data-testid="login-submit" className="w-full" disabled={submitting} type="submit" variant={primaryMode === "password" ? "default" : "secondary"}>
{submitting ? "登录中..." : "登录"}
</Button>
</form>
</div>
{((primaryMode !== "wecom" && wecomEntry?.loginUrl) || oidcProvider) ? (
<div className="space-y-3 rounded-2xl border border-zinc-200 bg-zinc-50 p-4">
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-zinc-500">其他入口</p>
<p className="mt-1 text-sm text-zinc-600">企业微信负责打开 OA 和接收提醒,实际业务仍在 OA 内完成。</p>
</div>
{primaryMode !== "wecom" && wecomEntry?.loginUrl ? (
<Button className="w-full" data-testid="wecom-entry-secondary" type="button" variant="secondary" onClick={handleWecomLogin}>
<div className="grid gap-4">
{primaryMode === "wecom" && wecomEntry?.loginUrl ? (
<div className="rounded-[1.5rem] border border-primary-100 bg-primary-50/70 p-6" data-testid="login-wecom-primary-panel">
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-primary">推荐入口</div>
<h2 className="mt-2 text-xl font-semibold tracking-tight text-zinc-900">使用企业微信进入工作台</h2>
<p className="mt-2 text-sm leading-6 text-zinc-600">当前是企业微信场景,优先使用免登入口;若未绑定或回调失效,仍可切换为账号密码登录。</p>
<Button className="mt-5 w-full" data-testid="wecom-entry-primary" type="button" onClick={handleWecomLogin}>
使用企业微信进入工作台
</Button>
) : null}
{oidcProvider ? (
<Button
className="w-full"
type="button"
variant="secondary"
onClick={() => {
window.location.assign(`${oidcProvider.loginUrl}?returnTo=${encodeURIComponent(redirectTo)}`);
}}
>
使用{oidcProvider.label}
</Button>
) : null}
</div>
) : null}
</div>
) : null}
{isWecomContext && !wecomEntry?.loginUrl && !wecomConfigQuery.isLoading ? (
<div className={`rounded-xl border px-4 py-3 text-sm ${noticeClasses(wecomStatus.tone)}`}>
<div className="font-medium">企业微信入口当前不可用</div>
<div className="mt-1">{wecomStatus.description}</div>
</div>
) : null}
{renderPasswordPanel(
primaryMode === "password" ? "login-password-primary-panel" : "login-password-secondary-panel",
primaryMode === "password",
)}
{((primaryMode !== "wecom" && wecomEntry?.loginUrl) || oidcProvider) ? (
<div className="rounded-[1.5rem] border border-zinc-200 bg-zinc-50/70 p-6">
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-zinc-400">补充入口</div>
<h2 className="mt-2 text-lg font-semibold text-zinc-900">其他进入方式</h2>
<p className="mt-2 text-sm leading-6 text-zinc-600">入口失败时请优先选择可理解、可回退的方式继续进入。</p>
<div className="mt-4 grid gap-3">
{primaryMode !== "wecom" && wecomEntry?.loginUrl ? (
<Button className="w-full" data-testid="wecom-entry-secondary" type="button" variant="secondary" onClick={handleWecomLogin}>
使用企业微信进入工作台
</Button>
) : null}
{oidcProvider ? (
<Button
className="w-full"
type="button"
variant="secondary"
onClick={() => {
window.location.assign(`${oidcProvider.loginUrl}?returnTo=${encodeURIComponent(redirectTo)}`);
}}
>
使用{oidcProvider.label}
</Button>
) : null}
</div>
</div>
) : null}
</div>
<div className="rounded-2xl border border-zinc-200 bg-zinc-50 px-4 py-3 text-sm text-zinc-600">
<div className="font-medium text-zinc-900">企业微信入口状态:{wecomStatus.label}</div>
<div className="mt-1 leading-6">{wecomStatus.description}</div>
{wecomStatus.reasonLabels.length ? (
<ul className="mt-2 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>
</div>
);