重构个人设置与管理边界(05-30-profile-settings-boundary)
这个提交包含在:
@@ -1192,12 +1192,18 @@ training or permission-maintenance surfaces:
|
||||
- `个人设置` default content is personal profile, password/security,
|
||||
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.
|
||||
and full employee dossier fields stay out of the default view. Do not add a
|
||||
default `查看员工档案` / employee-maintenance jump from the ordinary profile
|
||||
page; profile fields are read-only personal context, while dossier
|
||||
maintenance belongs to `人员` / `管理`.
|
||||
- Keep account settings separate from admin configuration. `个人设置` may show
|
||||
personal profile, password/security, notification preferences and Enterprise
|
||||
WeChat binding only. Organization maintenance, role assignment, approval
|
||||
rules, feature flags, master data, audit logs, employee maintenance and
|
||||
system configuration belong under `管理`, not the ordinary profile route.
|
||||
- Mobile `我的` and `个人设置` must not expose an admin matrix or raw internal
|
||||
labels. Hide `管理入口` / `人员管理` / `组织架构` / `系统设置` from mobile `我的`,
|
||||
and map account status plus role codes to Chinese labels before display.
|
||||
|
||||
**Why**: Small teams need fewer default decisions. Persistent onboarding and
|
||||
permission-heavy profile pages make ordinary users inspect system internals
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "profile-settings-boundary",
|
||||
"title": "个人设置与管理配置边界重构",
|
||||
"description": "重构个人设置、移动我的和管理配置入口边界:个人只放个人资料/安全/通知/企微绑定,管理能力统一进管理。",
|
||||
"status": "planning",
|
||||
"status": "in_progress",
|
||||
"dev_type": null,
|
||||
"scope": "frontend-page-redesign",
|
||||
"package": null,
|
||||
|
||||
@@ -53,14 +53,6 @@ function MobileActionLink({
|
||||
|
||||
export function MobileMePage() {
|
||||
const { me, permissions, refreshMe, logout } = useAuth();
|
||||
const canAccessAdmin = hasAnyPermission(permissions, [
|
||||
"*",
|
||||
"system.settings",
|
||||
"system.users",
|
||||
"masterdata.manage",
|
||||
"notification.channel.manage",
|
||||
"integration.manage",
|
||||
]);
|
||||
const canAccessAssets = hasAnyPermission(permissions, ["asset.view_own", "asset.view_all", "asset.manage", "asset.transfer", "asset.inventory"]);
|
||||
const canAccessProjects = hasAnyPermission(permissions, ["project.view_own", "project.view_all", "project.manage"]);
|
||||
const canAccessPurchase = hasAnyPermission(permissions, [...MOBILE_PURCHASE_PERMISSIONS]);
|
||||
@@ -115,17 +107,14 @@ export function MobileMePage() {
|
||||
|
||||
<SectionPanel title="账号与绑定">
|
||||
<div className="grid gap-2">
|
||||
<MobileActionLink to="/mobile/profile" label="个人设置" description="企业微信、账号与通知" icon="manage_accounts" />
|
||||
{me?.id ? (
|
||||
<MobileActionLink to="/mobile/profile" label="账号资料" description="员工号和联系方式" icon="badge" />
|
||||
) : null}
|
||||
<MobileActionLink to="/mobile/profile" label="个人设置" description="企业微信、账号安全与通知" icon="manage_accounts" />
|
||||
</div>
|
||||
</SectionPanel>
|
||||
|
||||
<SectionPanel title="更多入口" description="常用移动入口集中在这里。">
|
||||
<SectionPanel title="移动入口" description="只保留手机上常用的个人办事入口。">
|
||||
<div className="mobile-entry-grid mobile-me-entry-grid" data-testid="mobile-more-links">
|
||||
<MobileActionLink to="/mobile/notifications" label="通知中心" icon="notifications" />
|
||||
<MobileActionLink to="/mobile/leave" label="请假" icon="event_available" />
|
||||
<MobileActionLink to="/mobile/leave" label="请假记录" icon="event_available" />
|
||||
<MobileActionLink to="/mobile/reimbursements" label="报销" icon="request_quote" />
|
||||
{canAccessPurchase ? <MobileActionLink to="/mobile/purchase-requests" label="采购" icon="shopping_cart" /> : null}
|
||||
<MobileActionLink to="/mobile/approvals" label={canAccessApprovals ? "待办" : "进度"} icon="fact_check" />
|
||||
@@ -135,16 +124,6 @@ export function MobileMePage() {
|
||||
</div>
|
||||
</SectionPanel>
|
||||
|
||||
{canAccessAdmin ? (
|
||||
<SectionPanel title="管理入口">
|
||||
<div className="mobile-entry-grid mobile-me-entry-grid">
|
||||
<MobileActionLink to="/mobile/me" label="人员管理" icon="group" />
|
||||
<MobileActionLink to="/mobile/me" label="组织架构" icon="account_tree" />
|
||||
<MobileActionLink to="/mobile/me" label="系统设置" icon="settings" />
|
||||
</div>
|
||||
</SectionPanel>
|
||||
) : null}
|
||||
|
||||
<SectionPanel title="企业微信消息提醒">
|
||||
<label className="flex items-center gap-3 text-sm text-zinc-800">
|
||||
<input
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { fetchWecomConfig, startWecomBind, unbindWecom, updateNotificationPreferences } from "../api/auth";
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
@@ -30,6 +30,25 @@ function statusTone(status?: string) {
|
||||
return "badge-neutral";
|
||||
}
|
||||
|
||||
function accountStatusLabel(status?: string) {
|
||||
if (status === "active") return "账号正常";
|
||||
if (status === "inactive") return "账号停用";
|
||||
return "账号待确认";
|
||||
}
|
||||
|
||||
function roleLabel(roleCode: string) {
|
||||
const labels: Record<string, string> = {
|
||||
admin: "管理员",
|
||||
ceo: "负责人",
|
||||
employee: "员工",
|
||||
finance: "财务",
|
||||
finance_mgr: "财务负责人",
|
||||
hr: "人事",
|
||||
manager: "审批人",
|
||||
};
|
||||
return labels[roleCode] ?? "其他角色";
|
||||
}
|
||||
|
||||
function resolveProfileError(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return `${error.code}: ${error.message}`;
|
||||
@@ -107,13 +126,14 @@ export function MobileProfilePage() {
|
||||
const isBound = Boolean(binding.externalUserId);
|
||||
const wecomStatus = resolveWecomEntryStatus(wecomConfigQuery.data);
|
||||
const bindNotice = resolveWecomBindNotice(searchParams.get("wecomBind"));
|
||||
const roleLabels = (me?.roleCodes ?? []).map(roleLabel);
|
||||
|
||||
return (
|
||||
<PageScaffold className="pb-4">
|
||||
<div className="space-y-3" data-testid="mobile-profile-page">
|
||||
<PageHeader
|
||||
title="个人设置"
|
||||
meta={<span className={statusTone(me?.status)}>{me?.status === "active" ? "账号正常" : "账号待确认"}</span>}
|
||||
meta={<span className={statusTone(me?.status)}>{accountStatusLabel(me?.status)}</span>}
|
||||
/>
|
||||
|
||||
{bindNotice ? (
|
||||
@@ -144,6 +164,18 @@ export function MobileProfilePage() {
|
||||
</dl>
|
||||
</SectionPanel>
|
||||
|
||||
<SectionPanel title="密码与安全">
|
||||
<div className="mobile-list-card flex min-h-12 items-center justify-between gap-3 text-sm">
|
||||
<div>
|
||||
<div className="font-semibold text-zinc-900">账号密码</div>
|
||||
<div className="mt-0.5 text-xs leading-5 text-zinc-500">修改密码需要输入当前密码。</div>
|
||||
</div>
|
||||
<Button asChild type="button" variant="secondary">
|
||||
<Link to="/change-password">修改密码</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</SectionPanel>
|
||||
|
||||
<SectionPanel title="企业微信消息提醒">
|
||||
<label className="mobile-list-card flex min-h-12 items-center justify-between gap-3 text-sm text-zinc-800">
|
||||
<span>接收企业微信消息提醒</span>
|
||||
@@ -206,16 +238,32 @@ export function MobileProfilePage() {
|
||||
</div>
|
||||
</SectionPanel>
|
||||
|
||||
<SectionPanel title="账号状态">
|
||||
<div className="space-y-3 text-sm">
|
||||
<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>
|
||||
<p className="text-zinc-500">角色和权限由管理员统一维护。</p>
|
||||
</div>
|
||||
<SectionPanel title="组织与角色摘要">
|
||||
<details className="mobile-list-card text-sm">
|
||||
<summary className="flex cursor-pointer list-none items-center justify-between gap-3">
|
||||
<span className="font-semibold text-zinc-900">查看组织和角色</span>
|
||||
<span className="text-xs text-zinc-500">展开</span>
|
||||
</summary>
|
||||
<dl className="mt-3 grid gap-2 text-sm">
|
||||
<div className="mobile-subcard">
|
||||
<dt className="text-xs text-zinc-500">账号状态</dt>
|
||||
<dd className="mt-1 font-medium text-zinc-900">{accountStatusLabel(me?.status)}</dd>
|
||||
</div>
|
||||
<div className="mobile-subcard">
|
||||
<dt className="text-xs text-zinc-500">组织归属</dt>
|
||||
<dd className="mt-1 font-medium text-zinc-900">{me?.departmentId ? "已关联组织" : "待管理员维护"}</dd>
|
||||
</div>
|
||||
<div className="mobile-subcard">
|
||||
<dt className="text-xs text-zinc-500">直属汇报关系</dt>
|
||||
<dd className="mt-1 font-medium text-zinc-900">{me?.directManagerId ? "已维护" : "待管理员维护"}</dd>
|
||||
</div>
|
||||
<div className="mobile-subcard">
|
||||
<dt className="text-xs text-zinc-500">角色</dt>
|
||||
<dd className="mt-1 font-medium text-zinc-900">{roleLabels.length ? roleLabels.join("、") : "员工"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p className="mt-3 text-xs leading-5 text-zinc-500">组织、角色和权限由管理员在管理入口统一维护。</p>
|
||||
</details>
|
||||
</SectionPanel>
|
||||
</div>
|
||||
</PageScaffold>
|
||||
|
||||
+16
-11
@@ -53,7 +53,7 @@ function roleLabel(roleCode: string) {
|
||||
hr: "人事",
|
||||
manager: "审批人",
|
||||
};
|
||||
return labels[roleCode] ?? roleCode;
|
||||
return labels[roleCode] ?? "其他角色";
|
||||
}
|
||||
|
||||
function resolveProfileError(error: unknown) {
|
||||
@@ -109,21 +109,14 @@ export function ProfilePage() {
|
||||
<PageHeader
|
||||
title="个人设置"
|
||||
eyebrow="账号与通知"
|
||||
subtitle="查看自己的账号信息,设置消息提醒和企业微信入口。员工档案由对应页面维护。"
|
||||
subtitle="只处理个人资料、安全、通知偏好和企业微信绑定。组织和权限维护统一在管理入口处理。"
|
||||
meta={<span className={accountStatusClassName(me?.status)}>{accountStatusLabel(me?.status)}</span>}
|
||||
/>
|
||||
|
||||
<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
|
||||
}
|
||||
title="个人资料"
|
||||
description="只展示本人基础信息。如需调整组织、岗位或角色,由管理员在管理入口维护。"
|
||||
>
|
||||
<dl className="grid gap-4 text-sm text-zinc-700 md:grid-cols-2">
|
||||
<div>
|
||||
@@ -145,6 +138,18 @@ export function ProfilePage() {
|
||||
</dl>
|
||||
</SectionPanel>
|
||||
|
||||
<SectionPanel title="密码与安全" description="修改登录密码,保持账号入口可控。">
|
||||
<div className="flex flex-col gap-3 text-sm text-zinc-600 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-zinc-900">账号密码</div>
|
||||
<p className="mt-1">修改密码需要输入当前密码;忘记密码时请从登录页找回。</p>
|
||||
</div>
|
||||
<Button asChild type="button" variant="secondary">
|
||||
<Link to="/change-password">修改密码</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</SectionPanel>
|
||||
|
||||
<SectionPanel title="通知设置" description="选择是否接收企业微信个人应用提醒。">
|
||||
<div className="space-y-3 text-sm text-zinc-600">
|
||||
<label className="flex items-center gap-3 text-zinc-800">
|
||||
|
||||
@@ -41,7 +41,10 @@ export function SystemSettingsPage() {
|
||||
|
||||
return (
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader title="管理" subtitle="低频配置集中到这里,侧边栏只保留一个管理入口。" />
|
||||
<WorkspaceHeader
|
||||
title="管理"
|
||||
subtitle="人员、组织、角色、审批规则、主数据、通知渠道、外部系统、功能开关和审计统一在这里处理。"
|
||||
/>
|
||||
<div className="finance-status-strip" data-testid="system-settings-summary">
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">可用入口</div>
|
||||
|
||||
@@ -587,10 +587,16 @@ test("个人设置默认只展示账号通知和企业微信,不展示权限 c
|
||||
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.getByRole("heading", { name: "通知设置" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "企业微信绑定" })).toBeVisible();
|
||||
await expect(page.getByText("账号详情")).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "查看员工档案" })).toHaveCount(0);
|
||||
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.getByText("权限摘要")).not.toBeVisible();
|
||||
await expect(page.getByText("expense.create")).not.toBeVisible();
|
||||
await expect(page.getByText("leave.submit")).not.toBeVisible();
|
||||
|
||||
@@ -531,6 +531,11 @@ test.describe("R124 企业微信移动入口矩阵", () => {
|
||||
await mobile.page.getByTestId("mobile-nav-me").click();
|
||||
await expect(mobile.page).toHaveURL(/\/mobile\/me$/);
|
||||
await expect(mobile.page.getByTestId("mobile-more-links")).toBeVisible();
|
||||
await expect(mobile.page.getByText("管理入口")).toHaveCount(0);
|
||||
await expect(mobile.page.getByRole("link", { name: /账号资料/ })).toHaveCount(0);
|
||||
await expect(mobile.page.getByRole("link", { name: /人员管理/ })).toHaveCount(0);
|
||||
await expect(mobile.page.getByRole("link", { name: /组织架构/ })).toHaveCount(0);
|
||||
await expect(mobile.page.getByRole("link", { name: /系统设置/ })).toHaveCount(0);
|
||||
await mobile.page.screenshot({ path: screenshotPath("mobile-me") });
|
||||
await expectMobilePageContract(mobile.page);
|
||||
|
||||
@@ -546,6 +551,12 @@ test.describe("R124 企业微信移动入口矩阵", () => {
|
||||
await expect(mobile.page).toHaveURL(/\/mobile\/profile$/);
|
||||
await expect(mobile.page.getByTestId("mobile-profile-page")).toBeVisible();
|
||||
await expect(mobile.page.getByTestId("mobile-profile-wecom-card")).toBeVisible();
|
||||
await expect(mobile.page.getByRole("heading", { name: "密码与安全" })).toBeVisible();
|
||||
await expect(mobile.page.getByRole("heading", { name: "组织与角色摘要" })).toBeVisible();
|
||||
await expect(mobile.page.getByText("active", { exact: true })).toHaveCount(0);
|
||||
await expect(mobile.page.getByText("manager", { exact: true })).toHaveCount(0);
|
||||
await mobile.page.getByText("查看组织和角色").click();
|
||||
await expect(mobile.page.getByText("审批人、财务")).toBeVisible();
|
||||
await mobile.page.screenshot({ path: screenshotPath("mobile-profile") });
|
||||
await expectMobilePageContract(mobile.page);
|
||||
|
||||
|
||||
+2
-1
@@ -121,8 +121,9 @@ A 类页面优先保证真实试运行路径顺畅;B 类页面优先保证台
|
||||
- 首页不承担常驻新手教学职责;首次欢迎提示只做一次性、可关闭的常用业务入口,最多 3 个,不放个人设置、通知中心或“了解流程”这类说明型入口。
|
||||
- 审批入口同时服务审批人和发起人:审批人处理待我审批,员工查看我发起的进度。
|
||||
- 普通员工不应看到不可点或无权限的审批入口;审批、财务、负责人和管理员只能看到服务端允许的动作。
|
||||
- 个人设置默认只放个人资料、密码/安全、通知偏好和企业微信绑定;组织归属、直属汇报和角色摘要可折叠展示,不默认暴露权限 code 或完整员工档案维护内容。
|
||||
- 个人设置默认只放个人资料、密码/安全、通知偏好和企业微信绑定;组织归属、直属汇报和角色摘要可折叠展示,不默认暴露权限 code、role code、原始账号状态或完整员工档案维护入口。
|
||||
- 个人设置和管理配置彻底分离:组织、角色、审批规则、功能开关、主数据、审计日志、人员维护和系统配置统一进入 `管理`,不出现在普通员工路径或个人资料页里。
|
||||
- 移动端 `我的` 不放管理矩阵;`人员管理`、`组织架构`、`系统设置` 等低频配置只从 PC 管理入口承接。
|
||||
- PC 侧边栏是小团队任务入口,不是全量模块目录;一级入口必须按角色、权限、频次和真实办事价值收敛。已启用且有权限的 OA 主办业务台账,例如 `合同`、`采购`、`资产`、`项目`,不默认藏到 `更多`,否则会给常用业务增加二次点击负担。
|
||||
- 审批人、负责人和财务看到 `待我处理`;普通员工默认不显示 `待办` 或 `审批待办` 入口;通知只保留顶部铃铛或通知中心,不进入侧边栏。
|
||||
- PC 请假入口默认表达为 `请假记录`;提交请假、补卡、外出、加班仍优先走企业微信或移动入口,只有 OA 应急接管或指定管理场景才突出提交入口。
|
||||
|
||||
在新工单中引用
屏蔽一个用户