refactor: 简化工作台上手入口和个人设置(portal-onboarding-profile-simplification)
这个提交包含在:
@@ -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
|
||||
|
||||
@@ -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": "核对实现是否符合已确认的产品决策。"}
|
||||
@@ -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 确认的产品决策。"}
|
||||
@@ -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`:通过。
|
||||
@@ -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 人科技初创公司,一个人开发维护,优先减少默认复杂度和误导入口。
|
||||
@@ -0,0 +1,21 @@
|
||||
# Grill-me 决策记录
|
||||
|
||||
## 已确认
|
||||
|
||||
- 工作台首页不承担常驻新手教学职责。
|
||||
- 删除常驻“快速上手”清单。
|
||||
- 首次欢迎弹窗保留,但改成轻量“常用入口”,最多 3 个入口。
|
||||
- `个人设置` 只放账号信息、通知设置、企业微信绑定。
|
||||
- 不在个人设置页默认展示权限 code。
|
||||
- 删除 `完善个人资料` 这个上手入口。
|
||||
- `了解请假流程` 改成业务入口 `请假` / `提交请假`。
|
||||
- `查看通知中心` 不放在欢迎/上手入口里,只在有未读通知时作为任务出现。
|
||||
- 本轮不新建“我的档案”页面。
|
||||
- 本地截图确认后,再部署公网。
|
||||
- 单独创建 Trellis task,不混入 `oa-seven-day-trial-live-ops`。
|
||||
|
||||
## 产品判断
|
||||
|
||||
- 面向国内小于 10 人科技初创公司,工作台优先回答“今天该处理什么”,不要让用户先学习系统。
|
||||
- 上手入口必须和落地页面一致:说“流程”就应该是说明,说“申请”才进入申请页。
|
||||
- 个人设置页应符合常见账号设置心智,不展示开发者视角的权限 code。
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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({
|
||||
<Dialog open={open} onOpenChange={(nextOpen) => !nextOpen && onClose()}>
|
||||
<DialogContent className="max-w-2xl" data-testid="onboarding-welcome-dialog">
|
||||
<DialogHeader>
|
||||
<DialogTitle>欢迎加入海音达工作台!</DialogTitle>
|
||||
<DialogTitle>常用入口</DialogTitle>
|
||||
<DialogDescription>
|
||||
你的账号已就绪。以下是常用功能入口,点击即可开始使用:
|
||||
你的账号已就绪。先从工作台处理待办,也可以直接进入常用业务。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
{WELCOME_LINKS.map((item) => (
|
||||
<Link
|
||||
key={item.label}
|
||||
to={item.to}
|
||||
onClick={onClose}
|
||||
className="rounded-2xl border border-zinc-200 bg-white px-4 py-4 shadow-sm transition-colors hover:bg-zinc-50"
|
||||
className="rounded-lg border border-zinc-200 bg-white px-4 py-4 shadow-sm transition-colors hover:bg-zinc-50"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex size-10 items-center justify-center rounded-2xl bg-primary/10 text-primary">
|
||||
<div className="flex h-full flex-col gap-3">
|
||||
<div className="flex size-10 items-center justify-center rounded-md bg-primary/10 text-primary">
|
||||
<span className="material-symbols-outlined text-[20px]">{item.icon}</span>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
@@ -87,8 +75,8 @@ export function WelcomeDialog({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-zinc-200 bg-zinc-50 px-4 py-3 text-sm text-zinc-600">
|
||||
你可以随时在左侧菜单找到所有功能模块。
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-4 py-3 text-sm text-zinc-600">
|
||||
关闭后不再显示这个提示;所有功能都可以从左侧菜单进入。
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
||||
@@ -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 (
|
||||
<SectionPanel
|
||||
title="快速上手"
|
||||
description="基础设置"
|
||||
actions={(
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm font-medium text-zinc-500 transition-colors hover:text-zinc-700"
|
||||
data-testid="onboarding-getting-started-dismiss"
|
||||
onClick={onDismiss}
|
||||
>
|
||||
关闭清单
|
||||
</button>
|
||||
)}
|
||||
className="shadow-sm"
|
||||
>
|
||||
<div className="space-y-3" data-testid="onboarding-getting-started-card">
|
||||
{items.map((item) => (
|
||||
<Link
|
||||
key={item.key}
|
||||
to={item.href}
|
||||
className="portal-list-card flex items-center gap-3"
|
||||
>
|
||||
<span className={item.completed ? "text-emerald-600" : "text-zinc-400"}>
|
||||
<span className="material-symbols-outlined text-[20px]">
|
||||
{item.completed ? "check_circle" : "radio_button_unchecked"}
|
||||
</span>
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-medium text-zinc-900">{item.title}</div>
|
||||
<div className="mt-1 text-sm leading-6 text-zinc-500">{item.description}</div>
|
||||
</div>
|
||||
<span className="material-symbols-outlined text-zinc-400">arrow_forward</span>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<div className="rounded-lg border border-slate-200 bg-slate-50 px-3.5 py-3">
|
||||
<div className="flex items-center justify-between gap-3 text-sm">
|
||||
<span className="font-medium text-zinc-900">完成进度</span>
|
||||
<span className="text-zinc-500">{completedCount}/{items.length}</span>
|
||||
</div>
|
||||
<div className="mt-3 h-2 overflow-hidden rounded-full bg-slate-200">
|
||||
<div
|
||||
className="h-full rounded-full bg-primary transition-all"
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SectionPanel>
|
||||
);
|
||||
}
|
||||
|
||||
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 <div className="card-std p-5 text-sm text-zinc-500">加载工作台中...</div>;
|
||||
}
|
||||
@@ -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 ? (
|
||||
<ErrorBoundary
|
||||
fallback={(
|
||||
<PortalSectionFallback
|
||||
title="快速上手"
|
||||
description="基础设置"
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<GettingStartedCard
|
||||
items={gettingStartedItems}
|
||||
completedCount={gettingStartedCompletedCount}
|
||||
onDismiss={handleDismissGettingStarted}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
) : null}
|
||||
|
||||
<SecondaryWorkspacePanel
|
||||
title="团队与通知"
|
||||
description="团队状态、动态和公告"
|
||||
|
||||
+139
-155
@@ -31,12 +31,31 @@ function maskExternalUserId(value: string) {
|
||||
return `${value.slice(0, 4)}****${value.slice(-4)}`;
|
||||
}
|
||||
|
||||
function statusTone(status?: string) {
|
||||
function accountStatusLabel(status?: string) {
|
||||
if (status === "active") return "正常";
|
||||
if (status === "inactive") return "停用";
|
||||
return "待确认";
|
||||
}
|
||||
|
||||
function accountStatusClassName(status?: string) {
|
||||
if (status === "active") return "badge-success";
|
||||
if (status === "inactive") return "badge-warning";
|
||||
return "badge-neutral";
|
||||
}
|
||||
|
||||
function roleLabel(roleCode: string) {
|
||||
const labels: Record<string, string> = {
|
||||
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 (
|
||||
<PageScaffold>
|
||||
<PageHeader
|
||||
title="个人设置"
|
||||
eyebrow="账号与入口"
|
||||
subtitle="维护个人资料、角色摘要与企业微信绑定状态。复杂业务仍在海音达工作台内完成,企业微信只负责进入与提醒。"
|
||||
meta={<span className={statusTone(me?.status)}>{me?.status === "active" ? "账号正常" : "账号状态待确认"}</span>}
|
||||
eyebrow="账号与通知"
|
||||
subtitle="查看自己的账号信息,设置消息提醒和企业微信入口。员工档案由对应页面维护。"
|
||||
meta={<span className={accountStatusClassName(me?.status)}>{accountStatusLabel(me?.status)}</span>}
|
||||
/>
|
||||
|
||||
<div className="page-shell-grid">
|
||||
<div className="space-y-6">
|
||||
<SectionPanel
|
||||
title="基本信息"
|
||||
description="员工可直接理解的个人资料与联系方式。"
|
||||
actions={
|
||||
me?.id ? (
|
||||
<Button asChild type="button">
|
||||
<Link to={`/people/${me.id}`}>查看完整档案</Link>
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
{me?.id ? (
|
||||
<SectionPanel title="完整员工档案" description="办公地点、紧急联系人、绩效和薪资等员工档案信息在完整档案页维护或查看。">
|
||||
<div className="flex flex-col gap-3 text-sm text-zinc-600 md:flex-row md:items-center md:justify-between">
|
||||
<p>进入完整档案页后,可按当前权限查看本人绩效、已发放工资条,并编辑自助开放的档案字段。</p>
|
||||
<Button asChild type="button" variant="secondary">
|
||||
<Link to={`/people/${me.id}`}>进入完整员工档案</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</SectionPanel>
|
||||
) : null}
|
||||
|
||||
<SectionPanel title="企业微信消息提醒" description="控制是否接收企业微信个人应用消息;关闭后仍保留 OA 站内通知,不影响企业微信绑定与工作台登录。">
|
||||
<div className="space-y-3 text-sm text-zinc-600">
|
||||
<label className="flex items-center gap-3 text-zinc-800">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-4 w-4 rounded border-zinc-300"
|
||||
checked={Boolean(me?.notificationPushEnabled ?? true)}
|
||||
disabled={!me || notificationPreferenceMutation.isPending}
|
||||
onChange={(event) => notificationPreferenceMutation.mutate(event.target.checked)}
|
||||
/>
|
||||
接收企业微信消息提醒
|
||||
</label>
|
||||
<p>适用于审批待办、报销付款、资产流转等业务提醒。关闭后不会停止站内通知,也不会移除你的业务权限。</p>
|
||||
<div className="mx-auto max-w-5xl space-y-5">
|
||||
<SectionPanel
|
||||
title="账号信息"
|
||||
description="这些信息用于识别你的账号。需要维护更多员工档案时,进入完整档案页。"
|
||||
actions={
|
||||
me?.id ? (
|
||||
<Button asChild type="button" variant="secondary">
|
||||
<Link to={`/people/${me.id}`}>查看员工档案</Link>
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<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>
|
||||
</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>
|
||||
<dt className="text-zinc-500">员工号</dt>
|
||||
<dd className="mt-1 font-medium text-zinc-900">{me?.employeeNo ?? "-"}</dd>
|
||||
</div>
|
||||
</SectionPanel>
|
||||
</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>
|
||||
|
||||
<div className="space-y-6">
|
||||
<SectionPanel title="组织信息" description="对员工展示可理解的说明,不直接暴露内部标识字段。">
|
||||
<div className="space-y-4 text-sm text-zinc-600">
|
||||
<SectionPanel title="通知设置" description="选择是否接收企业微信个人应用提醒。">
|
||||
<div className="space-y-3 text-sm text-zinc-600">
|
||||
<label className="flex items-center gap-3 text-zinc-800">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-4 w-4 rounded border-zinc-300"
|
||||
checked={Boolean(me?.notificationPushEnabled ?? true)}
|
||||
disabled={!me || notificationPreferenceMutation.isPending}
|
||||
onChange={(event) => notificationPreferenceMutation.mutate(event.target.checked)}
|
||||
/>
|
||||
接收企业微信消息提醒
|
||||
</label>
|
||||
<p>关闭后仍会保留 OA 站内通知,不影响你的业务权限和工作台登录。</p>
|
||||
</div>
|
||||
</SectionPanel>
|
||||
|
||||
<SectionPanel title="企业微信绑定" description="绑定后可从企业微信工作台直接进入 OA;企业微信只负责入口和提醒。">
|
||||
<div className="space-y-4" data-testid="profile-wecom-card">
|
||||
{bindNotice ? (
|
||||
<div className={`rounded-lg 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="font-medium text-zinc-900">组织归属</div>
|
||||
<div className="mt-2 leading-6">部门与直属汇报关系由管理员统一维护,当前个人页只展示员工可理解的设置入口,避免把内部 ID 直接暴露给员工。</div>
|
||||
<div className="text-sm text-zinc-500">企业微信入口</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>
|
||||
</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>
|
||||
|
||||
<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 className="surface-muted px-4 py-4">
|
||||
<div className="text-sm text-zinc-500">当前绑定</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 ? "下次可直接从企业微信工作台进入 OA。" : "未绑定时仍可使用账号密码登录。"}
|
||||
</div>
|
||||
{isBound ? (
|
||||
<div className="mt-3 rounded-lg 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>
|
||||
</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>
|
||||
</div>
|
||||
</SectionPanel>
|
||||
|
||||
<details className="card-std">
|
||||
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-5 py-4">
|
||||
<div>
|
||||
<div className="text-base font-semibold text-zinc-900">账号详情</div>
|
||||
<div className="mt-1 text-sm text-zinc-500">组织归属和角色摘要,通常不需要修改。</div>
|
||||
</div>
|
||||
<span className="text-sm text-zinc-500">展开 / 收起</span>
|
||||
</summary>
|
||||
<div className="grid gap-3 border-t border-zinc-200 px-5 py-5 text-sm text-zinc-700 md:grid-cols-3">
|
||||
<div className="rounded-lg 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-lg 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 className="rounded-lg border border-zinc-200 px-4 py-4">
|
||||
<div className="text-zinc-500">角色</div>
|
||||
<div className="mt-2 font-medium text-zinc-900">{roleLabels.length ? roleLabels.join("、") : "员工"}</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</PageScaffold>
|
||||
);
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -46,8 +46,10 @@ A 类页面优先保证真实试运行路径顺畅;B 类页面优先保证台
|
||||
- 同一业务对象在同一区域只展示一次主标识;同一状态在同一区域只展示一次。
|
||||
- 通知中心是事件收件箱,不是待办列表或进度页副本;通知点击应直达业务详情、审批任务或可解释的失败提示。
|
||||
- 首页只做工作台摘要,不重复完整待办列表、通知列表和进度列表。
|
||||
- 首页不承担常驻新手教学职责;首次欢迎提示只做一次性、可关闭的常用业务入口,最多 3 个,不放个人设置、通知中心或“了解流程”这类说明型入口。
|
||||
- 审批入口同时服务审批人和发起人:审批人处理待我审批,员工查看我发起的进度。
|
||||
- 普通员工不应看到不可点或无权限的审批入口;审批、财务、负责人和管理员只能看到服务端允许的动作。
|
||||
- 个人设置默认只放账号信息、通知设置和企业微信绑定;组织归属、直属汇报和角色摘要可折叠展示,不默认暴露权限 code 或完整员工档案维护内容。
|
||||
|
||||
## 业务体验事实
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户