oa-auth-layout-wecom-login-ux 优化认证页登录布局

这个提交包含在:
2026-05-26 21:23:05 +08:00
父节点 15e4bcb0ed
当前提交 afa03c0b71
共修改 13 个文件,包含 392 行新增和 91 行删除
@@ -105,3 +105,80 @@ onNeedRefresh() {
applyUpdate();
}
```
## Scenario: Auth Page Layout And Login Entry Hierarchy
### 1. Scope / Trigger
- Trigger: login, password recovery, forced password change or future
unauthenticated auth forms need layout changes.
- Applies to `client/src/layouts/AuthLayout.tsx`, shared auth components under
`client/src/components/auth/`, and auth routes such as `/login`,
`/forgot-password` and `/change-password`.
### 2. Component Contract
- Auth routes should use one shared auth panel for brand, page title, subtitle,
content and optional footer links.
- `AuthLayout` owns only the auth page shell: background, width constraint,
centering, PWA update prompt and route outlet.
- Page implementations own only their form fields, submit actions, validation
messages and route-specific login-entry choices.
### 3. Visual Contracts
- Do not nest a form card inside another login card.
- Put the brand block inside the auth panel so logo, system name, title and
form share one visual axis.
- Keep the panel width stable around the current auth-shell width; auth pages
should feel like an OA workstation entry, not a marketing split hero.
- Place `忘记密码?` beside the password label or directly below the password
input on the right; do not bury it in help text or page footer.
- Use password visibility icon buttons with accessible labels and preserve the
current input value.
### 4. Login Entry Hierarchy
- Normal browser: account/password login is primary; Enterprise WeChat and OIDC
entries belong below the primary submit button as secondary methods.
- Enterprise WeChat WebView: Enterprise WeChat one-click entry is primary;
account/password login remains available as the fallback path.
- Enterprise WeChat entry changes must remain frontend entry hierarchy changes
unless the task explicitly approves backend OAuth, binding or WeCom API work.
- Email verification-code login is a separate login mode. Do not infer it from
password-recovery verification-code flows without a backend login-code
contract and abuse-control plan.
### 5. Tests Required
- Frontend typecheck and build.
- Focused auth E2E covering:
- password visibility and forgot-password navigation on `/login`;
- password recovery request-code and reset flow on `/forgot-password`;
- normal-browser Enterprise WeChat entry as secondary CTA;
- WeCom WebView Enterprise WeChat entry as primary CTA with password fallback.
- UI design rules check and Playwright screenshot evidence when the auth layout
changes visibly.
### 6. Wrong vs Correct
#### Wrong
```tsx
<AuthLayout>
<Card>
<Card>
<LoginForm />
</Card>
</Card>
</AuthLayout>
```
#### Correct
```tsx
<AuthPanel title="登录工作台" subtitle="账号密码登录">
<LoginForm />
<SecondaryLoginMethods />
</AuthPanel>
```
@@ -0,0 +1,6 @@
{"file": ".trellis/spec/hyb-oa/project-governance.md", "reason": "Validate Trellis task structure, checks and commit readiness."}
{"file": ".trellis/spec/hyb-oa/frontend-react-vite-design.md", "reason": "Verify auth page layout, PWA prompt behavior, typecheck/build and visual checks."}
{"file": ".trellis/spec/hyb-oa/wecom-boundary.md", "reason": "Verify Enterprise WeChat login entry remains an entry point only."}
{"file": ".trellis/spec/hyb-oa/security-privacy.md", "reason": "Verify no sensitive auth values are persisted or written to evidence."}
{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend quality review entry point."}
{"file": "docs/产品设计规则.md", "reason": "Verify page style follows Hyinda OA design baseline."}
@@ -0,0 +1,8 @@
{"file": ".trellis/spec/hyb-oa/index.md", "reason": "hyb-oa Trellis spec entry point and read order."}
{"file": ".trellis/spec/hyb-oa/project-governance.md", "reason": "Task contract, implementation approval, context curation and commit rules."}
{"file": ".trellis/spec/hyb-oa/frontend-react-vite-design.md", "reason": "Frontend auth page, PWA update and UI verification contracts."}
{"file": ".trellis/spec/hyb-oa/wecom-boundary.md", "reason": "Enterprise WeChat ownership and API/write boundaries for login entry changes."}
{"file": ".trellis/spec/hyb-oa/security-privacy.md", "reason": "Sensitive-data handling for auth, password and Trellis evidence."}
{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend React/Vite guideline entry point."}
{"file": "docs/产品设计规则.md", "reason": "Hyinda brand, PC/mobile OA visual and layout rules."}
{"file": ".trellis/tasks/05-26-oa-auth-layout-wecom-login-ux/research/login-page-patterns.md", "reason": "Login page pattern research and chosen layout hierarchy."}
@@ -0,0 +1,40 @@
# 认证页布局与企业微信登录体验优化实施记录
## Implementation Scope
- 新增共享认证页组件 `AuthPanel`,统一品牌、标题、副标题、内容区和页脚样式。
- 调整 `AuthLayout`,仅保留认证页面背景、居中容器、PWA 更新提示和路由出口。
- 重构登录页,移除卡片套卡片;普通浏览器以账号密码登录为主,企业微信/SSO 放入次级登录区。
- 重构企业微信 WebView 登录页层级,企业微信一键进入为主 CTA,账号密码保留为兜底入口。
- 重构找回密码页和修改密码页,复用同一认证面板,不改后端认证接口。
- 收窄企业微信入口 E2E,避免前端 UI 用例依赖真实登录和个人设置页跳转。
## Safety Boundaries
- 不新增邮箱验证码登录。
- 不改后端登录、找回密码、企业微信 OAuth 或绑定接口。
- 不写入企业微信后台配置,不写真实业务数据。
- 不记录账号密码、验证码、token、Secret 或真实 payload。
- 部署和公网 smoke 需在本地验证通过后另行确认。
## Validation Log
- `cd client && npm run typecheck`:已通过。
- `cd client && npm run build`:已通过,production build 和 PWA precache 正常生成。
- `node scripts/qa/check-ui-design-rules.mjs --strict`:已通过。
- `python3 ./.trellis/scripts/task.py validate 05-26-oa-auth-layout-wecom-login-ux`:已通过。
- `node scripts/qa/check-trellis-governance.mjs`:已通过;仅提示 `docs/产品设计规则.md` 是非 spec/task research 上下文。
- `git diff --check`:已通过。
- 登录/找回密码 focused E2E:已通过,2 passed。
- 企业微信入口 E2E:已通过,2 passed。
- 完整 `login-portal-ux.spec.ts`:认证页、找回密码、企业微信 WebView 相关 5 passed;Portal 历史文案断言 3 failed,见下方已知漂移。
- 视觉截图已生成:
- `output/playwright/oa-auth-layout-wecom-login-ux/desktop-login.png`
- `output/playwright/oa-auth-layout-wecom-login-ux/desktop-forgot-password.png`
- `output/playwright/oa-auth-layout-wecom-login-ux/mobile-wecom-login.png`
- 移动企业微信主按钮计算背景色:`rgb(0, 57, 138)`。
## Known Unrelated Test Drift
- `client/tests/e2e/login-portal-ux.spec.ts` 的完整套件仍有 Portal 历史文案断言漂移,涉及旧文案 `快捷操作栏`、`角色重点`、`常用入口`。
- 该漂移不属于本认证页任务,本任务仅保留登录页、找回密码和企业微信入口 focused 用例作为验收依据。
@@ -0,0 +1,118 @@
# 认证页布局与企业微信登录体验优化
## Goal
把登录、找回密码、改密三类认证页统一成成熟、克制、可复用的 OA 认证页模板,解决当前登录页布局随意、卡片嵌套、企业微信入口层级不清的问题。第一期只优化布局和企业微信登录入口,不新增邮箱验证码登录。
## Scope
- 前端认证页布局:`AuthLayout`、登录页、找回密码页、修改密码页。
- 共享认证页组件:品牌、标题、副标题、内容区和页脚的统一面板。
- 企业微信登录入口层级:普通浏览器为次级入口,企业微信 WebView 为主入口。
- 认证页自动化验证:登录页、找回密码、企业微信入口和 PWA 更新不打断输入。
- Trellis task 治理:PRD、research、implement/check 上下文和验证记录。
## What I Already Know
- 用户已确认按推荐方案执行:先重做登录页布局,强化企业微信入口;邮箱验证码登录作为后续增强。
- 当前 `LoginPage` 已有账号密码登录、忘记密码入口、企业微信入口和企业微信 WebView 优先逻辑。
- 当前 `AuthLayout` 在卡片外展示品牌区,页面内又有主卡片和登录表单内层卡片,视觉层级显得临时拼接。
- 当前 `ForgotPasswordPage`、`ChangePasswordPage` 与登录页样式不完全一致。
- 项目设计规则要求 OA 页面参考 Hyinda 官网品牌气质,保持浅色工作台、克制字号、清晰边界,不做营销页。
- 企业微信边界要求企业微信只做入口、绑定、免登录和消息回流,不替代 OA 主业务页面。
## Research References
- `research/login-page-patterns.md`:中后台和开源系统登录页布局共性,结论是主认证面板、忘记密码放密码字段附近、企业微信/SSO 入口作为明确但分层的登录方式。
## Requirements
- 新增或重构认证页共享模板,使登录、找回密码、改密页使用一致的面板宽度、内边距、标题层级、表单间距和提示样式。
- 登录页只保留一层主面板,不再出现表单卡片套登录卡片。
- 品牌区进入主面板顶部,Logo、系统名、标题、表单左侧对齐。
- 普通浏览器登录页:
- 账号密码表单为主。
- “忘记密码?”放在密码 label 同一行右侧。
- 登录按钮为主按钮。
- 企业微信登录在主按钮下方作为次级入口。
- OIDC/SSO 如启用,仍放在次级入口区。
- 企业微信 WebView 登录页:
- 企业微信一键进入作为主 CTA。
- 账号密码登录作为兜底入口,可展开或保留在下方。
- 保留未绑定/异常提示。
- 找回密码页:
- 使用同一认证面板。
- 保留邮箱验证码找回流程,不改后端接口。
- 返回登录入口放在页脚弱链接。
- 改密页:
- 使用同一认证面板。
- 保留旧密码、新密码、确认新密码和密码显隐。
- 不新增邮箱验证码登录 tab,不新增后端验证码登录接口。
- 不持久化密码、验证码或 token。
- 认证页 PWA 更新提示仍不能自动刷新表单。
## Acceptance Criteria
- [ ] 登录页无卡片套卡片,主认证面板视觉统一。
- [ ] 普通浏览器下账号密码为主,企业微信登录为次级入口。
- [ ] 企业微信 WebView 下企业微信一键进入为主,账号密码为兜底入口。
- [ ] 忘记密码入口位于密码 label 右侧,密码显隐控件仍可用。
- [ ] 找回密码页和改密页复用同一认证页视觉模板。
- [ ] 原有登录、找回密码、企业微信入口 E2E 仍通过。
- [ ] 前端 typecheck、build、设计规则检查通过。
- [ ] 不新增或输出真实账号、密码、验证码、token、Secret。
## Definition of Done
- 本地验证通过:`cd client && npm run typecheck && npm run build`。
- Focused E2E 覆盖登录页布局、忘记密码、企业微信入口。
- UI 设计规则检查通过。
- Trellis validate、governance、`git diff --check` 通过。
- 提交后再请求部署批准;只有部署和公网检查通过后,才能告知可公网测试。
## Out of Scope
- 不做邮箱验证码登录。
- 不改后端认证接口。
- 不新增短信登录。
- 不改企业微信 OAuth/绑定后端逻辑。
- 不修改真实企业微信后台配置。
- 不执行真实账号重置或真实业务写入。
## Proposed Implementation
1. 新增共享认证页组件,例如 `AuthPanel`,统一品牌、标题、副标题、内容区和页脚链接。
2. 调整 `AuthLayout`,只负责页面背景、居中、PWA 更新提示和 Outlet,不再单独摆放品牌区。
3. 重构 `LoginPage`:
- 去掉内层登录卡片。
- 普通浏览器按账号密码主表单 + 次级企业微信入口布局。
- 企业微信 WebView 按企业微信主 CTA + 账号密码兜底布局。
4. 重构 `ForgotPasswordPage`、`ChangePasswordPage` 使用共享认证面板。
5. 更新 Playwright 用例:
- 登录页密码显隐、忘记密码入口、PWA 更新输入不丢失。
- 企业微信入口仍显示并跳转。
6. 运行本任务测试计划。
## Test Plan
```bash
cd client && npm run typecheck
cd client && npm run build
E2E_BASE_URL=http://127.0.0.1:5173 npx playwright test tests/e2e/login-portal-ux.spec.ts tests/e2e/wecom-entry.spec.ts --workers=1 --reporter=list
node scripts/qa/check-ui-design-rules.mjs --strict
python3 ./.trellis/scripts/task.py validate 05-26-oa-auth-layout-wecom-login-ux
node scripts/qa/check-trellis-governance.mjs
git diff --check
```
## Technical Notes
- Design rules: `docs/产品设计规则.md`
- Frontend spec: `.trellis/spec/hyb-oa/frontend-react-vite-design.md`
- WeCom boundary: `.trellis/spec/hyb-oa/wecom-boundary.md`
- Security/privacy: `.trellis/spec/hyb-oa/security-privacy.md`
- Login page: `client/src/pages/LoginPage.tsx`
- Forgot password page: `client/src/pages/ForgotPasswordPage.tsx`
- Change password page: `client/src/pages/ChangePasswordPage.tsx`
- Auth layout: `client/src/layouts/AuthLayout.tsx`
- Existing auth E2E: `client/tests/e2e/login-portal-ux.spec.ts`、`client/tests/e2e/wecom-entry.spec.ts`
@@ -0,0 +1,23 @@
# 登录页布局参考研究
## 参考对象
- Ant Design ProComponents `LoginForm`:中后台登录常见模式,支持账号密码、验证码、忘记密码、其他登录方式等组合。参考链接:https://procomponents.ant.design/en-US/components/login-form/
- `vue-element-admin`:开源后台模板,登录页以账号密码为主,强调简单、稳定、权限系统入口清楚。参考链接:https://github.com/PanJiaChen/vue-element-admin
- JeecgBoot:国内低代码/中后台系统,支持账号登录和第三方登录能力,适合参考“企业内部系统需要多登录入口但入口层级要清晰”。参考链接:https://github.com/jeecgboot/JeecgBoot
## 共性结论
- 登录页常见结构是一个主认证面板,不使用多层嵌套卡片。
- “忘记密码”通常放在密码 label 同一行右侧,或者密码输入框下方右对齐。
- 账号密码是默认兜底入口;验证码登录通常作为 tab 或切换项,不和找回密码验证码复用。
- 企业微信/微信/SSO 这类第三方入口通常放在主登录按钮下方,或在对应容器内作为主 CTA。
- 内部 OA 不需要营销型左右分屏、大图背景或长功能说明;更适合克制、稳定、工作台式页面。
## 映射到 hyb-oa
- 当前任务只做第一期:认证页布局统一 + 企业微信入口层级优化。
- 邮箱验证码登录需要新增后端登录验证码接口、投递能力和风控,不纳入本期。
- 普通浏览器:账号密码为主,企业微信登录为次级入口。
- 企业微信 WebView:企业微信一键进入为主,账号密码为兜底入口。
- 登录页、找回密码页、改密页应复用同一套认证页面板尺寸、标题层级、表单间距和提示样式。
@@ -0,0 +1,33 @@
{
"id": "oa-auth-layout-wecom-login-ux",
"name": "oa-auth-layout-wecom-login-ux",
"title": "认证页布局与企业微信登录体验优化",
"description": "统一登录、找回密码、改密三类认证页布局,强化普通浏览器与企业微信 WebView 的登录入口层级;邮箱验证码登录作为后续增强不纳入本期。",
"status": "in_progress",
"dev_type": null,
"scope": "client/auth-ux",
"package": null,
"priority": "P2",
"creator": "shunli",
"assignee": "shunli",
"createdAt": "2026-05-26",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [
"client/src/layouts/AuthLayout.tsx",
"client/src/pages/LoginPage.tsx",
"client/src/pages/ForgotPasswordPage.tsx",
"client/src/pages/ChangePasswordPage.tsx",
"client/tests/e2e/login-portal-ux.spec.ts",
"client/tests/e2e/wecom-entry.spec.ts"
],
"notes": "",
"meta": {}
}
+31
查看文件
@@ -0,0 +1,31 @@
import type { ReactNode } from "react";
import { AppBrand } from "../shell/AppBrand";
import { cn } from "../../lib/utils";
type AuthPanelProps = {
title: string;
subtitle?: string;
children: ReactNode;
footer?: ReactNode;
className?: string;
"data-testid"?: string;
};
export function AuthPanel({ title, subtitle, children, footer, className, "data-testid": testId }: AuthPanelProps) {
return (
<section
className={cn("w-full rounded-xl border border-slate-200 bg-white p-6 shadow-sm sm:p-8", className)}
data-testid={testId}
>
<div className="space-y-7">
<AppBrand compact />
<div>
<h1 className="text-xl font-semibold leading-7 text-slate-950">{title}</h1>
{subtitle ? <p className="mt-1 text-sm leading-6 text-slate-500">{subtitle}</p> : null}
</div>
<div>{children}</div>
{footer ? <div className="border-t border-slate-100 pt-5 text-center text-sm">{footer}</div> : null}
</div>
</section>
);
}
+2 -6
查看文件
@@ -1,5 +1,4 @@
import { Outlet } from "react-router-dom";
import { AppBrand } from "../components/shell/AppBrand";
import { Button } from "../components/ui/button";
import { usePwaUpdatePrompt } from "../hooks/usePwaUpdatePrompt";
@@ -7,11 +6,8 @@ export function AuthLayout() {
const { updateReady, applyUpdate, dismissUpdate } = usePwaUpdatePrompt();
return (
<div className="min-h-screen bg-slate-50 px-4 py-8 sm:px-6 lg:px-10">
<div className="mx-auto flex min-h-[calc(100vh-4rem)] max-w-md flex-col justify-center">
<div className="mb-5">
<AppBrand />
</div>
<div className="min-h-screen bg-[var(--color-bg-canvas)] px-4 py-8 sm:px-6 lg:px-10">
<div className="mx-auto flex min-h-[calc(100vh-4rem)] max-w-[448px] flex-col justify-center">
{updateReady ? (
<div className="mb-4 rounded-xl border border-primary/20 bg-primary/5 px-4 py-3 text-sm text-slate-700" data-testid="auth-pwa-update-notice">
<div className="font-medium text-slate-900">系统有新版本</div>
+6 -22
查看文件
@@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
import { isApiError } from "../api/types";
import { mapApiErrorMessage } from "../api/errorMessages";
import { useAuth } from "../auth/AuthContext";
import { AuthPanel } from "../components/auth/AuthPanel";
import { Button } from "../components/ui/button";
import { Label } from "../components/ui/label";
import { PasswordInput } from "../components/ui/password-input";
@@ -50,17 +51,9 @@ export function ChangePasswordPage() {
}
return (
<div className="card-std space-y-6 border-primary-100 p-6">
<div className="mb-6 space-y-2">
<p className="text-xs font-semibold uppercase tracking-wide text-primary">hyb-oa</p>
<h1 className="text-2xl font-bold text-zinc-900">修改密码</h1>
<p className="text-sm text-secondary">
为了账号安全,请先修改初始密码后再进入系统。
</p>
</div>
<AuthPanel title="修改密码" subtitle="为了账号安全,请先修改初始密码">
<form className="space-y-4" onSubmit={handleSubmit}>
<div className="space-y-1">
<div className="space-y-2">
<Label htmlFor="oldPassword">旧密码</Label>
<PasswordInput
autoComplete="current-password"
@@ -72,7 +65,7 @@ export function ChangePasswordPage() {
/>
</div>
<div className="space-y-1">
<div className="space-y-2">
<Label htmlFor="newPassword">新密码</Label>
<PasswordInput
autoComplete="new-password"
@@ -85,16 +78,7 @@ export function ChangePasswordPage() {
<p className="text-xs text-zinc-500">密码要求:{PASSWORD_REQUIREMENTS.join(";")}。</p>
</div>
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-xs text-zinc-600">
<p className="font-medium text-zinc-700">建议的新密码规则</p>
<ul className="mt-1 list-disc space-y-1 pl-4">
{PASSWORD_REQUIREMENTS.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
<div className="space-y-1">
<div className="space-y-2">
<Label htmlFor="confirmPassword">确认新密码</Label>
<PasswordInput
autoComplete="new-password"
@@ -123,6 +107,6 @@ export function ChangePasswordPage() {
{submitting ? "提交中..." : "确认修改"}
</Button>
</form>
</div>
</AuthPanel>
);
}
+13 -14
查看文件
@@ -3,6 +3,7 @@ import { Link, useNavigate } from "react-router-dom";
import { forgotPassword, resetPassword } from "../api/auth";
import { mapApiErrorMessage } from "../api/errorMessages";
import { isApiError } from "../api/types";
import { AuthPanel } from "../components/auth/AuthPanel";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { Label } from "../components/ui/label";
@@ -93,13 +94,17 @@ export function ForgotPasswordPage() {
}
return (
<div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6" data-testid="forgot-password-page">
<div>
<h1 className="text-xl font-semibold text-slate-900">找回密码</h1>
<p className="mt-1 text-sm leading-6 text-slate-500">使用账号邮箱接收验证码,设置新密码后重新登录。</p>
</div>
<form className="mt-6 space-y-4" onSubmit={handleRequestCode}>
<AuthPanel
title="找回密码"
subtitle="使用账号邮箱接收验证码"
data-testid="forgot-password-page"
footer={(
<Link className="font-medium text-primary hover:underline" to="/login">
返回登录
</Link>
)}
>
<form className="space-y-4" onSubmit={handleRequestCode}>
<div className="space-y-2">
<Label htmlFor="forgot-password-email">邮箱</Label>
<Input
@@ -181,12 +186,6 @@ export function ForgotPasswordPage() {
{error}
</div>
) : null}
<div className="mt-5 text-center text-sm">
<Link className="font-medium text-primary hover:underline" to="/login">
返回登录
</Link>
</div>
</div>
</AuthPanel>
);
}
+35 -31
查看文件
@@ -5,6 +5,7 @@ import { fetchSsoProviders, fetchWecomConfig } from "../api/auth";
import { mapApiErrorMessage } from "../api/errorMessages";
import { isApiError } from "../api/types";
import { useAuth } from "../auth/AuthContext";
import { AuthPanel } from "../components/auth/AuthPanel";
import { getDeviceFingerprint } from "../auth/deviceFingerprint";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
@@ -108,7 +109,7 @@ export function LoginPage() {
function renderPasswordPanel(testId: string) {
return (
<div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm" data-testid={testId}>
<div data-testid={testId}>
<form className="space-y-4" onSubmit={handleSubmit}>
<div className="space-y-2">
<Label htmlFor="login-identifier">账号</Label>
@@ -150,29 +151,22 @@ export function LoginPage() {
}
return (
<div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6">
<div>
<h1 className="text-xl font-semibold text-slate-900">登录工作台</h1>
<p className="mt-1 text-sm text-slate-500">
{showWecomPrimary ? "企业微信进入优先。" : "账号密码登录。"}
</p>
</div>
<div className="mt-6 space-y-4">
<AuthPanel title="登录工作台" subtitle={showWecomPrimary ? "企业微信进入优先" : "账号密码登录"}>
<div className="space-y-5">
{wecomNotice ? (
<div className={`rounded-2xl border px-4 py-3 text-sm ${noticeClasses(wecomNotice.tone)}`} data-testid="login-wecom-notice">
<div className={`rounded-xl 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 leading-6">{wecomNotice.description}</div>
</div>
) : null}
{showWecomPrimary ? (
<div className="rounded-xl border border-primary/20 bg-primary/5 p-4" data-testid="login-wecom-primary-panel">
<div className="space-y-3" data-testid="login-wecom-primary-panel">
<Button className="w-full" data-testid="wecom-entry-primary" disabled={!wecomEntry} type="button" onClick={handleWecomLogin}>
{wecomConfigQuery.isLoading ? "准备中..." : "进入工作台"}
{wecomConfigQuery.isLoading ? "准备中..." : "企业微信一键进入"}
</Button>
{wecomConfig && !wecomEntry ? (
<div className="mt-3 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-sm leading-6 text-amber-700">
<div className="rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-sm leading-6 text-amber-700">
企业微信入口不可用,请用账号密码。
</div>
) : null}
@@ -192,25 +186,35 @@ export function LoginPage() {
? renderPasswordPanel(showWecomPrimary ? "login-password-secondary-panel" : "login-password-primary-panel")
: null}
{!showWecomPrimary && wecomEntry ? (
<Button className="w-full" data-testid="wecom-entry-secondary" type="button" variant="secondary" onClick={handleWecomLogin}>
企业微信登录
</Button>
) : null}
{!showWecomPrimary && (wecomEntry || oidcProvider) ? (
<div className="space-y-3">
<div className="flex items-center gap-3 text-xs text-slate-400">
<span className="h-px flex-1 bg-slate-100" />
<span>其他登录方式</span>
<span className="h-px flex-1 bg-slate-100" />
</div>
{!showWecomPrimary && oidcProvider ? (
<Button
className="w-full"
type="button"
variant="secondary"
onClick={() => {
window.location.assign(`${oidcProvider.loginUrl}?returnTo=${encodeURIComponent(redirectTo)}`);
}}
>
使用{oidcProvider.label}
</Button>
{wecomEntry ? (
<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>
) : null}
</div>
</div>
</AuthPanel>
);
}
-18
查看文件
@@ -1,17 +1,5 @@
import { expect, test } from "@playwright/test";
import { EMPLOYEE, login } from "./helpers/auth";
import { openProfileSettings } from "./helpers/navigation";
function runtimeEmployee() {
const identifier = process.env.LIVE_SMOKE_EMPLOYEE_IDENTIFIER?.trim();
const password = process.env.LIVE_SMOKE_EMPLOYEE_PASSWORD?.trim();
if (identifier && password) {
return { identifier, password, name: "Live Smoke Employee" };
}
return EMPLOYEE;
}
function mockWecomConfigPayload() {
return {
enabled: true,
@@ -52,12 +40,6 @@ test("E2E 企微入口:普通浏览器下企微按钮为次级入口", async (
await page.getByTestId("wecom-entry-secondary").click();
await expect(page).toHaveURL(/\/api\/auth\/wecom\/login\?returnTo=/);
const employee = runtimeEmployee();
await login(page, employee);
await openProfileSettings(page, employee.name);
await expect(page.getByRole("heading", { name: "企业微信绑定" })).toBeVisible();
await expect(page.getByText("接入状态", { exact: true })).toBeVisible();
});
test("E2E 企微入口:企业微信场景优先显示企微 CTA,并展示未绑定提示", async ({ page }) => {