diff --git a/.trellis/spec/hyb-oa/frontend-react-vite-design.md b/.trellis/spec/hyb-oa/frontend-react-vite-design.md index 42a8aa1a..e283d453 100644 --- a/.trellis/spec/hyb-oa/frontend-react-vite-design.md +++ b/.trellis/spec/hyb-oa/frontend-react-vite-design.md @@ -236,6 +236,33 @@ and admin tools available without dominating the default path. asserting the task surface, primary action and secondary/advanced entry behavior. +## Convention: Workbench Onboarding And Profile Scope + +**What**: PC workbench and account settings should stay task-first, not become +training or permission-maintenance surfaces: + +- Do not add a persistent `快速上手` / onboarding checklist to the workbench by + default. If first-login guidance is needed, use a dismissible welcome dialog + with at most three real business entries. +- Welcome or onboarding entries must point to executable user tasks, such as + `处理待办`, `提交报销` or `请假`; do not use explanatory entries like + `了解流程`, and do not route users to settings or notifications as a + substitute for task guidance. +- `个人设置` default content is account identity, notification preference and + Enterprise WeChat binding. Organization, report line and role summary may be + folded under `账号详情`; permission code lists and full employee dossier fields + stay out of the default view. + +**Why**: Small teams need fewer default decisions. Persistent onboarding and +permission-heavy profile pages make ordinary users inspect system internals +before doing work. + +**Tests Required**: + +- Focused Playwright coverage for changed workbench or profile routes. +- Screenshot evidence for first-login welcome, workbench first screen and + profile default view when changing these surfaces. + ## Convention: Detail And Mobile Command Surfaces **What**: PC detail pages and notification-like flows should reuse the Wave3 diff --git a/.trellis/tasks/05-29-portal-onboarding-profile-simplification/check.jsonl b/.trellis/tasks/05-29-portal-onboarding-profile-simplification/check.jsonl new file mode 100644 index 00000000..a9a29cdb --- /dev/null +++ b/.trellis/tasks/05-29-portal-onboarding-profile-simplification/check.jsonl @@ -0,0 +1,4 @@ +{"file": ".trellis/spec/hyb-oa/frontend-react-vite-design.md", "reason": "核对 A-class 工作台、个人设置页和欢迎弹窗是否符合小团队任务优先和降噪规则。"} +{"file": ".trellis/spec/hyb-oa/security-privacy.md", "reason": "核对页面和证据不泄露敏感信息、权限原始字段和企业微信原始标识。"} +{"file": ".trellis/spec/hyb-oa/docs-and-evidence.md", "reason": "核对截图与验证报告只记录相对路径和脱敏摘要。"} +{"file": ".trellis/tasks/05-29-portal-onboarding-profile-simplification/research/grill-decisions.md", "reason": "核对实现是否符合已确认的产品决策。"} diff --git a/.trellis/tasks/05-29-portal-onboarding-profile-simplification/implement.jsonl b/.trellis/tasks/05-29-portal-onboarding-profile-simplification/implement.jsonl new file mode 100644 index 00000000..a2a5c90c --- /dev/null +++ b/.trellis/tasks/05-29-portal-onboarding-profile-simplification/implement.jsonl @@ -0,0 +1,4 @@ +{"file": ".trellis/spec/hyb-oa/frontend-react-vite-design.md", "reason": "前端工作台、欢迎弹窗和个人设置页降噪的设计与验证约束。"} +{"file": ".trellis/spec/hyb-oa/security-privacy.md", "reason": "个人设置页不得暴露敏感账号、权限和企业微信原始标识。"} +{"file": ".trellis/spec/hyb-oa/docs-and-evidence.md", "reason": "截图和验证证据需记录相对路径与脱敏摘要。"} +{"file": ".trellis/tasks/05-29-portal-onboarding-profile-simplification/research/grill-decisions.md", "reason": "用户通过 grill-me 确认的产品决策。"} diff --git a/.trellis/tasks/05-29-portal-onboarding-profile-simplification/implement.md b/.trellis/tasks/05-29-portal-onboarding-profile-simplification/implement.md new file mode 100644 index 00000000..f34e5c41 --- /dev/null +++ b/.trellis/tasks/05-29-portal-onboarding-profile-simplification/implement.md @@ -0,0 +1,45 @@ +# 实施说明 + +## Plan + +1. 移除 `PortalHomePage` 的常驻 `GettingStartedCard` 渲染和清单数据,保留首次欢迎弹窗触发逻辑。 +2. 精简 `WelcomeDialog`: + - 标题改为常用入口语义。 + - 只保留处理待办、提交报销、请假 3 个入口。 + - 移除个人设置、通知中心、考勤汇总。 +3. 精简 `ProfilePage`: + - 第一屏主内容只保留账号信息、通知设置、企业微信绑定。 + - 完整员工档案作为次要入口。 + - 组织/角色状态移入折叠 `账号详情`。 + - 移除权限 code 默认展示。 +4. 更新 `login-portal-ux.spec.ts` 中 onboarding 断言,覆盖: + - 欢迎弹窗入口数量和文案。 + - 快速上手清单不再显示。 + - 个人设置默认不出现权限 code。 +5. 跑本地验证并用 Playwright 生成截图。 + +## Risk + +- 工作台新员工引导测试原本依赖 `快速上手` 卡片,需要同步改断言。 +- 个人设置页如果完全移除角色信息,管理员排查账号状态会少一个入口;因此保留折叠的自然语言 `账号详情`。 +- 移除快速上手可能影响首次用户发现功能;通过欢迎弹窗 3 个常用入口和左侧导航承接。 + +## Rollback + +- 若用户确认仍需要常驻上手清单,可恢复 `GettingStartedCard`,但只放真实任务型入口,不再放个人设置/通知中心/流程说明。 + +## Local Evidence + +- 截图: + - `output/playwright/portal-onboarding-profile-simplification/welcome-dialog.png` + - `output/playwright/portal-onboarding-profile-simplification/portal-workbench.png` + - `output/playwright/portal-onboarding-profile-simplification/profile-settings.png` +- 验证: + - `cd client && npm run build`:通过。 + - `cd client && E2E_BASE_URL=http://127.0.0.1:5173 npx playwright test tests/e2e/login-portal-ux.spec.ts --workers=1 --reporter=list`:12/12 通过。 + - `node scripts/qa/check-ui-design-rules.mjs`:通过。 + - `python3 ./.trellis/scripts/task.py validate 05-29-portal-onboarding-profile-simplification`:通过。 + - `node scripts/qa/check-trellis-governance.mjs`:通过。 + - `node scripts/qa/check-process-contract.mjs --baseline --json`:通过。 + - `node scripts/qa/audit-docs.mjs --check-registry`:通过,保留既有 `unregistered docs files: 24` 警告。 + - `git diff --check`:通过。 diff --git a/.trellis/tasks/05-29-portal-onboarding-profile-simplification/prd.md b/.trellis/tasks/05-29-portal-onboarding-profile-simplification/prd.md new file mode 100644 index 00000000..d0f80347 --- /dev/null +++ b/.trellis/tasks/05-29-portal-onboarding-profile-simplification/prd.md @@ -0,0 +1,112 @@ +# 工作台上手入口与个人设置降噪 + +## Goal + +把工作台从“新手教学清单”拉回“今天该处理什么”的小团队工作入口,同时把个人设置页降噪成用户熟悉的账号与通知设置页。解决当前快速上手入口承诺与落地页面不一致的问题,降低国内小于 10 人科技初创公司员工首次使用 OA 的理解成本。 + +## What I Already Know + +- 用户已通过 `grill-me` 逐项确认: + - 工作台首页不承担常驻新手教学职责。 + - 删除常驻“快速上手”清单。 + - 首次欢迎弹窗保留,但改成轻量“常用入口”,最多 3 个入口。 + - `个人设置` 只放账号信息、通知设置、企业微信绑定。 + - 不在个人设置页默认展示权限 code。 + - 删除 `完善个人资料` 这个上手入口。 + - `了解请假流程` 改成业务入口 `请假` / `提交请假`。 + - `查看通知中心` 不放在欢迎/上手入口里,只在有未读通知时作为任务出现。 + - 本轮不新建“我的档案”页面。 + - 本地截图确认后,再部署公网。 +- 当前 `PortalHomePage.tsx` 里存在 `GettingStartedCard` 和 4 个上手项:完善个人资料、设置通知偏好、了解请假流程、查看通知中心。 +- 当前 `ProfilePage.tsx` 同时展示基本信息、完整员工档案、通知提醒、企业微信绑定、组织信息、账号状态、角色与权限 code,默认页面偏复杂。 +- 当前 `WelcomeDialog.tsx` 包含个人设置和通知中心入口,描述仍像新手功能列表。 + +## Scope + +- 前端页面与交互文案改动: + - 工作台首页 onboarding 区域。 + - 首次欢迎弹窗。 + - PC 个人设置页。 +- 前端聚焦测试与本地截图证据。 +- 本地验证通过后提交代码;公网部署不纳入本任务直接执行。 + +## Requirements + +- 工作台页面: + - 删除常驻 `快速上手` 卡片及其完成度清单。 + - 不再显示 `完善个人资料`、`设置通知偏好`、`了解请假流程`、`查看通知中心` 作为上手步骤。 + - 保留“今日任务”“下一步”和已有工作台业务信息架构。 + - 不新增新的 onboarding 常驻区块。 +- 欢迎弹窗: + - 保留首次欢迎弹窗。 + - 改为轻量“常用入口”语义,不表达必须完成的步骤。 + - 入口最多 3 个:处理待办、提交报销、请假。 + - 不包含个人设置、通知中心、考勤汇总。 +- 个人设置页: + - 默认只展示用户能理解、能操作、经常需要看的内容: + - 账号信息:姓名、员工号、邮箱、手机号。 + - 通知设置:企业微信消息提醒开关,以及关闭后仍保留站内通知的说明。 + - 企业微信绑定:绑定状态、绑定/重新绑定/解绑操作。 + - 组织归属、直属汇报、账号状态、角色摘要最多进入折叠的 `账号详情`。 + - 默认不展示权限 code。 + - 不新建 `我的档案` 页面。 + - 完整员工档案只保留为次要入口,不能让用户误以为个人设置就是档案维护中心。 +- 文案: + - 使用自然中文,避免 `Portal`、`Workflow` 等英文产品词。 + - `了解请假流程` 这类说明型入口不再出现;业务入口用 `请假` 或 `提交请假`。 + - 通知中心不作为上手入口出现。 + +## Acceptance Criteria + +- [ ] 工作台不再显示常驻 `快速上手` 清单。 +- [ ] 欢迎弹窗只显示最多 3 个常用业务入口,且不包含个人设置和通知中心。 +- [ ] 个人设置页第一屏只呈现账号信息、通知设置、企业微信绑定这三类核心内容。 +- [ ] 个人设置页默认不展示权限 code。 +- [ ] `了解请假流程`、`查看通知中心`、`完善个人资料` 不再作为上手清单入口出现。 +- [ ] 本地截图能证明工作台、欢迎弹窗、个人设置页降噪效果。 +- [ ] 通过前端 typecheck、build、聚焦 Playwright、UI 设计规则和 Trellis governance。 + +## Test Plan + +```bash +cd client && npm run typecheck +cd client && npm run build +cd client && npx playwright test tests/e2e/login-portal-ux.spec.ts --workers=1 --reporter=list +node scripts/qa/check-ui-design-rules.mjs +python3 ./.trellis/scripts/task.py validate 05-29-portal-onboarding-profile-simplification +node scripts/qa/check-trellis-governance.mjs +git diff --check +``` + +本地截图: + +- 工作台:确认没有常驻快速上手,第一屏聚焦今日任务。 +- 欢迎弹窗:确认是轻量常用入口。 +- 个人设置:确认默认页面简洁、无权限 code 堆叠。 + +## Definition Of Done + +- 代码改动完成并通过本地验证。 +- 截图证据保存在当前 task research 或 `output/playwright/` 对应目录。 +- 提交中文 commit,commit message 引用本 task。 +- 本轮不直接告知用户公网已更新;公网部署作为截图确认后的后续步骤执行。 + +## Out Of Scope + +- 不新建“我的档案”页面。 +- 不改后端用户/权限/员工档案模型。 +- 不改真实业务流程、不创建业务数据、不执行审批/付款/企业微信写入。 +- 不扩大为全站导航重构。 +- 不在本任务内直接部署公网;部署需本地截图确认后另行执行。 + +## Technical Notes + +- 主要代码: + - `client/src/pages/PortalHomePage.tsx` + - `client/src/components/onboarding/WelcomeDialog.tsx` + - `client/src/pages/ProfilePage.tsx` + - `client/tests/e2e/login-portal-ux.spec.ts` +- 设计约束: + - `.trellis/spec/hyb-oa/frontend-react-vite-design.md` + - `docs/产品设计规则.md` +- 当前用户背景:国内小于 10 人科技初创公司,一个人开发维护,优先减少默认复杂度和误导入口。 diff --git a/.trellis/tasks/05-29-portal-onboarding-profile-simplification/research/grill-decisions.md b/.trellis/tasks/05-29-portal-onboarding-profile-simplification/research/grill-decisions.md new file mode 100644 index 00000000..99c7310c --- /dev/null +++ b/.trellis/tasks/05-29-portal-onboarding-profile-simplification/research/grill-decisions.md @@ -0,0 +1,21 @@ +# Grill-me 决策记录 + +## 已确认 + +- 工作台首页不承担常驻新手教学职责。 +- 删除常驻“快速上手”清单。 +- 首次欢迎弹窗保留,但改成轻量“常用入口”,最多 3 个入口。 +- `个人设置` 只放账号信息、通知设置、企业微信绑定。 +- 不在个人设置页默认展示权限 code。 +- 删除 `完善个人资料` 这个上手入口。 +- `了解请假流程` 改成业务入口 `请假` / `提交请假`。 +- `查看通知中心` 不放在欢迎/上手入口里,只在有未读通知时作为任务出现。 +- 本轮不新建“我的档案”页面。 +- 本地截图确认后,再部署公网。 +- 单独创建 Trellis task,不混入 `oa-seven-day-trial-live-ops`。 + +## 产品判断 + +- 面向国内小于 10 人科技初创公司,工作台优先回答“今天该处理什么”,不要让用户先学习系统。 +- 上手入口必须和落地页面一致:说“流程”就应该是说明,说“申请”才进入申请页。 +- 个人设置页应符合常见账号设置心智,不展示开发者视角的权限 code。 diff --git a/.trellis/tasks/05-29-portal-onboarding-profile-simplification/task.json b/.trellis/tasks/05-29-portal-onboarding-profile-simplification/task.json new file mode 100644 index 00000000..3176fdef --- /dev/null +++ b/.trellis/tasks/05-29-portal-onboarding-profile-simplification/task.json @@ -0,0 +1,31 @@ +{ + "id": "portal-onboarding-profile-simplification", + "name": "portal-onboarding-profile-simplification", + "title": "工作台上手入口与个人设置降噪", + "description": "删除工作台常驻快速上手清单,轻量化首次欢迎弹窗,并将个人设置页降噪为账号、通知和企业微信绑定。", + "status": "in_progress", + "dev_type": null, + "scope": "frontend", + "package": null, + "priority": "P2", + "creator": "shunli", + "assignee": "shunli", + "createdAt": "2026-05-29", + "completedAt": null, + "branch": null, + "base_branch": "main", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [ + "client/src/pages/PortalHomePage.tsx", + "client/src/components/onboarding/WelcomeDialog.tsx", + "client/src/pages/ProfilePage.tsx", + "client/tests/e2e/login-portal-ux.spec.ts" + ], + "notes": "", + "meta": {} +} \ No newline at end of file diff --git a/client/src/components/onboarding/WelcomeDialog.tsx b/client/src/components/onboarding/WelcomeDialog.tsx index 98fbbd65..87f0f094 100644 --- a/client/src/components/onboarding/WelcomeDialog.tsx +++ b/client/src/components/onboarding/WelcomeDialog.tsx @@ -18,34 +18,22 @@ type WelcomeLinkItem = { const WELCOME_LINKS: WelcomeLinkItem[] = [ { - icon: "event_available", - label: "请假申请", - description: "提交和查看请假流程", - to: "/leave-requests", + icon: "fact_check", + label: "处理待办", + description: "先看今天需要审批或跟进的事项", + to: "/workflow/tasks", }, { icon: "request_quote", - label: "报销提交", - description: "进入报销工作台创建单据", + label: "提交报销", + description: "录入票据和报销说明,查看处理进度", to: "/reimbursements", }, { - icon: "schedule", - label: "考勤汇总", - description: "查看企微打卡摘要并提交 OA 修正", - to: "/attendance", - }, - { - icon: "manage_accounts", - label: "个人设置", - description: "补充资料并维护通知偏好", - to: "/settings/profile", - }, - { - icon: "notifications", - label: "通知中心", - description: "查看审批和业务消息", - to: "/notifications", + icon: "event_available", + label: "请假", + description: "提交请假申请或查看自己的请假记录", + to: "/leave-requests?action=create", }, ]; @@ -60,22 +48,22 @@ export function WelcomeDialog({ !nextOpen && onClose()}> - 欢迎加入海音达工作台! + 常用入口 - 你的账号已就绪。以下是常用功能入口,点击即可开始使用: + 你的账号已就绪。先从工作台处理待办,也可以直接进入常用业务。 -
+
{WELCOME_LINKS.map((item) => ( -
-
+
+
{item.icon}
@@ -87,8 +75,8 @@ export function WelcomeDialog({ ))}
-
- 你可以随时在左侧菜单找到所有功能模块。 +
+ 关闭后不再显示这个提示;所有功能都可以从左侧菜单进入。
diff --git a/client/src/pages/PortalHomePage.tsx b/client/src/pages/PortalHomePage.tsx index caf0cd11..93280d96 100644 --- a/client/src/pages/PortalHomePage.tsx +++ b/client/src/pages/PortalHomePage.tsx @@ -25,8 +25,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/ui/tabs" import { employeePortalSchema, managerPortalSchema } from "../features/portal/schema"; import { clearWelcomeDialogFlag, - dismissGettingStartedCard, - shouldShowGettingStartedCard, shouldShowWelcomeDialog, } from "../lib/onboardingGuide"; import { readRecentVisits } from "../lib/recentVisits"; @@ -77,14 +75,6 @@ type PortalFocusLink = { to: string; }; -type GettingStartedChecklistItem = { - key: string; - title: string; - description: string; - href: string; - completed: boolean; -}; - const LEAVE_PORTAL_PERMISSIONS = ["leave.view_own", "leave.view_all", "leave.create", "leave.submit"]; const EXPENSE_PORTAL_PERMISSIONS = ["expense.view_own", "expense.view_all", "expense.create", "expense.proxy_manage"]; const PROCUREMENT_PORTAL_PERMISSIONS = ["procurement.view_own", "procurement.view_all", "procurement.create", "procurement.submit"]; @@ -581,70 +571,6 @@ function PortalSectionFallback({ ); } -function GettingStartedCard({ - items, - completedCount, - onDismiss, -}: { - items: GettingStartedChecklistItem[]; - completedCount: number; - onDismiss: () => void; -}) { - const progressPercent = items.length ? Math.round((completedCount / items.length) * 100) : 0; - - return ( - - 关闭清单 - - )} - className="shadow-sm" - > -
- {items.map((item) => ( - - - - {item.completed ? "check_circle" : "radio_button_unchecked"} - - -
-
{item.title}
-
{item.description}
-
- arrow_forward - - ))} - -
-
- 完成进度 - {completedCount}/{items.length} -
-
-
-
-
-
- - ); -} - function QuickActionTile({ icon, label, @@ -882,7 +808,6 @@ function ActivityAnnouncementsPanel({ export function PortalHomePage() { const { me, permissions } = useAuth(); const [showWelcome, setShowWelcome] = useState(false); - const [showGettingStarted, setShowGettingStarted] = useState(false); const portalQuery = useQuery({ queryKey: ["portal-home"], queryFn: () => fetchPortalHome() }); const workflowSummaryQuery = useQuery({ enabled: hasAnyPermission(permissions, APPROVAL_PORTAL_PERMISSIONS), @@ -898,41 +823,8 @@ export function PortalHomePage() { useEffect(() => { setShowWelcome(shouldShowWelcomeDialog()); - setShowGettingStarted(shouldShowGettingStartedCard()); }, []); - const gettingStartedItems: GettingStartedChecklistItem[] = [ - { - key: "profile", - title: "完善个人资料", - description: "补充紧急联系人和银行账户", - href: "/settings/profile", - completed: (me?.profileCompleteness?.score ?? 0) >= 50, - }, - { - key: "notification-preferences", - title: "设置通知偏好", - description: "选择是否接收外部渠道推送", - href: "/settings/profile", - completed: true, - }, - { - key: "leave-flow", - title: "了解请假流程", - description: "查看假期类型和余额", - href: "/leave-requests", - completed: false, - }, - { - key: "notifications", - title: "查看通知中心", - description: "了解审批和业务消息", - href: "/notifications", - completed: false, - }, - ]; - const gettingStartedCompletedCount = gettingStartedItems.filter((item) => item.completed).length; - if (portalQuery.isLoading) { return
加载工作台中...
; } @@ -976,11 +868,6 @@ export function PortalHomePage() { setShowWelcome(false); } - function handleDismissGettingStarted() { - dismissGettingStartedCard(); - setShowGettingStarted(false); - } - const employeeSummaryItems = ([ { key: "expense", @@ -1208,23 +1095,6 @@ export function PortalHomePage() { hiddenFocusCount={Math.max(todoAggregateItems.length - 1, 0)} /> - {showGettingStarted ? ( - - )} - > - - - ) : null} - = { + admin: "管理员", + ceo: "负责人", + employee: "员工", + finance: "财务", + finance_mgr: "财务负责人", + hr: "人事", + manager: "审批人", + }; + return labels[roleCode] ?? roleCode; +} + function resolveProfileError(error: unknown) { if (isApiError(error)) { return `${error.code}: ${error.message}`; @@ -48,7 +67,7 @@ function resolveProfileError(error: unknown) { } export function ProfilePage() { - const { me, permissions, refreshMe } = useAuth(); + const { me, refreshMe } = useAuth(); const [searchParams] = useSearchParams(); const wecomConfigQuery = useQuery({ queryKey: ["auth", "wecom-config"], @@ -73,7 +92,7 @@ export function ProfilePage() { mutationFn: (notificationPushEnabled: boolean) => updateNotificationPreferences({ notificationPushEnabled }), onSuccess: async () => { await refreshMe(); - toast.success("企业微信消息提醒偏好已更新"); + toast.success("通知设置已更新"); }, onError: (error) => { toast.error(resolveProfileError(error)); @@ -83,174 +102,139 @@ export function ProfilePage() { const wecomStatus = resolveWecomEntryStatus(wecomConfigQuery.data); const bindNotice = resolveWecomBindNotice(searchParams.get("wecomBind")); const isBound = Boolean(me?.wecomBinding?.externalUserId); + const roleLabels = (me?.roleCodes ?? []).map(roleLabel); return ( {me?.status === "active" ? "账号正常" : "账号状态待确认"}} + eyebrow="账号与通知" + subtitle="查看自己的账号信息,设置消息提醒和企业微信入口。员工档案由对应页面维护。" + meta={{accountStatusLabel(me?.status)}} /> -
-
- - 查看完整档案 - - ) : null - } - > -
-
-
姓名
-
{me?.name ?? "-"}
-
-
-
员工号(系统生成)
-
{me?.employeeNo ?? "-"}
-
-
-
邮箱
-
{me?.email ?? "-"}
-
-
-
手机号
-
{me?.phone ?? "-"}
-
-
-
- - {me?.id ? ( - -
-

进入完整档案页后,可按当前权限查看本人绩效、已发放工资条,并编辑自助开放的档案字段。

- -
-
- ) : null} - - -
- -

适用于审批待办、报销付款、资产流转等业务提醒。关闭后不会停止站内通知,也不会移除你的业务权限。

+
+ + 查看员工档案 + + ) : null + } + > +
+
+
姓名
+
{me?.name ?? "-"}
- - - -
- {bindNotice ? ( -
-
{bindNotice.title}
-
{bindNotice.description}
-
- ) : null} - -
-
-
接入状态
-
{wecomStatus.label}
-
{wecomStatus.description}
- {wecomStatus.reasonLabels.length ? ( -
    - {wecomStatus.reasonLabels.map((label) => ( -
  • {label}
  • - ))} -
- ) : null} -
- -
-
当前绑定状态
-
{isBound ? "已绑定" : "未绑定"}
-
- {isBound ? "下次可直接从企业微信工作台进入海音达工作台。" : "未绑定时,企业微信登录会自动回退到账号密码登录。"} -
- {isBound ? ( -
-
绑定标识:{maskExternalUserId(me?.wecomBinding?.externalUserId ?? "")}
-
绑定时间:{me?.wecomBinding?.createdAt ? new Date(me.wecomBinding.createdAt).toLocaleString() : "-"}
-
- ) : null} -
-
- -
- - -
+
+
员工号
+
{me?.employeeNo ?? "-"}
- -
+
+
邮箱
+
{me?.email ?? "-"}
+
+
+
手机号
+
{me?.phone ?? "-"}
+
+
+
-
- -
+ +
+ +

关闭后仍会保留 OA 站内通知,不影响你的业务权限和工作台登录。

+
+
+ + +
+ {bindNotice ? ( +
+
{bindNotice.title}
+
{bindNotice.description}
+
+ ) : null} + +
-
组织归属
-
部门与直属汇报关系由管理员统一维护,当前个人页只展示员工可理解的设置入口,避免把内部 ID 直接暴露给员工。
+
企业微信入口
+
{wecomStatus.label}
+
{wecomStatus.description}
-
-
-
部门维护状态
-
{me?.departmentId ? "已关联组织" : "待管理员维护"}
-
-
-
直属汇报关系
-
{me?.directManagerId ? "已维护" : "待管理员维护"}
-
-
-
- - -
-
- {me?.status === "active" ? "active" : me?.status ?? "unknown"} - {(me?.roleCodes ?? []).map((role) => ( - {role} - ))} -
-
-
权限摘要
-
- {permissions.map((permission) => ( - {permission} - ))} +
+
当前绑定
+
{isBound ? "已绑定" : "未绑定"}
+
+ {isBound ? "下次可直接从企业微信工作台进入 OA。" : "未绑定时仍可使用账号密码登录。"}
+ {isBound ? ( +
+
绑定标识:{maskExternalUserId(me?.wecomBinding?.externalUserId ?? "")}
+
绑定时间:{me?.wecomBinding?.createdAt ? new Date(me.wecomBinding.createdAt).toLocaleString() : "-"}
+
+ ) : null}
- -
+ +
+ + +
+
+
+ +
+ +
+
账号详情
+
组织归属和角色摘要,通常不需要修改。
+
+ 展开 / 收起 +
+
+
+
部门状态
+
{me?.departmentId ? "已关联组织" : "待管理员维护"}
+
+
+
直属汇报关系
+
{me?.directManagerId ? "已维护" : "待管理员维护"}
+
+
+
角色
+
{roleLabels.length ? roleLabels.join("、") : "员工"}
+
+
+
); diff --git a/client/tests/e2e/login-portal-ux.spec.ts b/client/tests/e2e/login-portal-ux.spec.ts index 9faa9774..c8bf6f23 100644 --- a/client/tests/e2e/login-portal-ux.spec.ts +++ b/client/tests/e2e/login-portal-ux.spec.ts @@ -68,6 +68,9 @@ function mockPasswordLogin(page: import("@playwright/test").Page, emailReady = t await route.fulfill({ status: 200, contentType: "application/json", + headers: { + "set-cookie": "has_session=1; Path=/; SameSite=Strict", + }, body: JSON.stringify(envelope({ accessToken: "test-access-token", expiresIn: 900, @@ -567,7 +570,7 @@ test("Portal 管理视角重排正确,更多入口默认折叠", async ({ page await expect(page.getByRole("link", { name: "差旅申请" })).toBeVisible(); }); -test("Portal 新员工引导先显示欢迎弹窗,再显示快速上手清单", async ({ page }) => { +test("Portal 新员工只显示轻量欢迎入口,不再显示快速上手清单", async ({ page }) => { await page.addInitScript(() => { window.localStorage.setItem("hyb_oa_show_welcome", "1"); window.localStorage.setItem("hyb_oa_onboarding_active", "1"); @@ -583,14 +586,47 @@ test("Portal 新员工引导先显示欢迎弹窗,再显示快速上手清单" await login(page, EMPLOYEE); - await expect(page.getByTestId(TEST_IDS.onboarding.welcomeDialog)).toBeVisible(); + const welcomeDialog = page.getByTestId(TEST_IDS.onboarding.welcomeDialog); + await expect(welcomeDialog).toBeVisible(); + await expect(welcomeDialog.getByRole("heading", { name: "常用入口" })).toBeVisible(); + await expect(welcomeDialog.getByRole("link")).toHaveCount(3); + await expect(welcomeDialog.getByRole("link", { name: /处理待办/ })).toBeVisible(); + await expect(welcomeDialog.getByRole("link", { name: /提交报销/ })).toBeVisible(); + await expect(welcomeDialog.getByRole("link", { name: /请假/ })).toBeVisible(); + await expect(welcomeDialog.getByRole("link", { name: /个人设置/ })).not.toBeVisible(); + await expect(welcomeDialog.getByRole("link", { name: /通知中心/ })).not.toBeVisible(); + await page.getByTestId(TEST_IDS.onboarding.welcomeClose).evaluate((element: HTMLButtonElement) => { element.click(); }); await expect(page.getByTestId(TEST_IDS.onboarding.welcomeDialog)).not.toBeVisible(); - await expect(page.getByTestId(TEST_IDS.onboarding.gettingStartedCard)).toBeVisible(); - await page.getByTestId(TEST_IDS.onboarding.gettingStartedDismiss).click(); await expect(page.getByTestId(TEST_IDS.onboarding.gettingStartedCard)).not.toBeVisible(); + await expect(page.getByText("快速上手")).not.toBeVisible(); + await expect(page.getByText("完善个人资料")).not.toBeVisible(); + await expect(page.getByText("了解请假流程")).not.toBeVisible(); + await expect(page.getByText("查看通知中心")).not.toBeVisible(); + await expect(page.getByRole("heading", { name: "今日任务" })).toBeVisible(); +}); + +test("个人设置默认只展示账号通知和企业微信,不展示权限 code", async ({ page }) => { + await mockPasswordLogin(page); + await mockNavigation(page, "employee"); + await mockPortalHome(page, "employee"); + await mockAttendanceToday(page); + await mockNotificationList(page); + await mockMe(page, ["employee"], "Employee"); + + await login(page, EMPLOYEE); + await page.goto("/settings/profile"); + + await expect(page.getByRole("heading", { name: "个人设置" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "账号信息" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "通知设置" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "企业微信绑定" })).toBeVisible(); + await expect(page.getByText("账号详情")).toBeVisible(); + await expect(page.getByText("权限摘要")).not.toBeVisible(); + await expect(page.getByText("expense.create")).not.toBeVisible(); + await expect(page.getByText("leave.submit")).not.toBeVisible(); }); test("Portal bootstrap refresh 首次瞬时失败后重试成功仍保留登录态", async ({ page }) => { diff --git a/docs/产品设计规则.md b/docs/产品设计规则.md index 6d110359..b05b3d51 100644 --- a/docs/产品设计规则.md +++ b/docs/产品设计规则.md @@ -46,8 +46,10 @@ A 类页面优先保证真实试运行路径顺畅;B 类页面优先保证台 - 同一业务对象在同一区域只展示一次主标识;同一状态在同一区域只展示一次。 - 通知中心是事件收件箱,不是待办列表或进度页副本;通知点击应直达业务详情、审批任务或可解释的失败提示。 - 首页只做工作台摘要,不重复完整待办列表、通知列表和进度列表。 +- 首页不承担常驻新手教学职责;首次欢迎提示只做一次性、可关闭的常用业务入口,最多 3 个,不放个人设置、通知中心或“了解流程”这类说明型入口。 - 审批入口同时服务审批人和发起人:审批人处理待我审批,员工查看我发起的进度。 - 普通员工不应看到不可点或无权限的审批入口;审批、财务、负责人和管理员只能看到服务端允许的动作。 +- 个人设置默认只放账号信息、通知设置和企业微信绑定;组织归属、直属汇报和角色摘要可折叠展示,不默认暴露权限 code 或完整员工档案维护内容。 ## 业务体验事实