feat(分组权限): 分组管理接口 + 管理页 + 端到端验收
后端 endpoints/groups.py(**仅 SUPER_ADMIN**): · 组的 CRUD(两级,子组不配范围则继承父组 —— 生产大组配一次,下面的 生产/测试小组都不用再配) · 成员增删 / 设组长 / 候选人下拉(复用 MOM 查询口径,部门已钉死为 ORG_DEPARTMENT) · **删组仅限空组**:级联删是一次静默的批量权限变更,误点一下一批人就突然 看不到数据了;强制「先移人再删组」多一步,但出错时是可见的 · 停用组的语义写死在接口文档:成员**立即**退回未分组状态 为什么只有超管能管分组(不是偏好,是必须): 被显式分进组的 SUPERVISOR 会从「全厂」降级为只看本组;若允许主管管理分组, 他把自己移出组就能恢复全厂视野 —— 这是一条现成的提权路径,分组对他无效。 前端: · AdminGroupsPage:组列表 + 可见范围勾选 + 成员管理 + 组长标记 + 二次确认 · AdminLayout 加菜单项,页头显示数据范围徽标 —— 空范围(未分组)用橙色显眼 提示,否则用户看到空列表会以为系统坏了,这是最难排查的一类反馈 · AuthContext 登录后补拉一次 /auth/me 拿 scope(登录接口不查库、不返回它) · constants/task.ts 新增 isSuperAdmin,不手写 === 比较 端到端验收(实测):先造 1 生产 + 1 售后产品,然后 生产组员 → 1 条,全 PRODUCTION;范围经「生产小组 → 生产大组」继承而来 维修组员 → 1 条,全 AFTER_SALES 超管 → 2 条,全量 任务列表 total 与 returned 一致(验证 count/select 双过滤) 扫码跨组仍 200(符合「能看、不能操作」的既定决策) 停用维修大组 → 成员立即退回未分组 上述测试数据已还原
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
import { NavLink, Outlet, useLocation, useNavigate, Navigate } from "react-router-dom";
|
||||
import { QrCode, Package, ArrowLeft, LayoutDashboard, Smartphone, GitBranch, LogOut, User, Users, BarChart3, Table2, Tv, ScrollText } from "lucide-react";
|
||||
import { QrCode, Package, ArrowLeft, LayoutDashboard, Smartphone, GitBranch, LogOut, User, Users, BarChart3, Table2, Tv, ScrollText, ShieldCheck } from "lucide-react";
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
|
||||
const MENU = [
|
||||
@ -39,6 +39,12 @@ const MENU = [
|
||||
icon: Table2,
|
||||
description: "规格型号 × 人员/工序 在制品透视表",
|
||||
},
|
||||
{
|
||||
title: "业务分组",
|
||||
path: "/admin/groups",
|
||||
icon: ShieldCheck,
|
||||
description: "生产组 / 维修组 · 决定成员可见的数据范围",
|
||||
},
|
||||
{
|
||||
title: "操作审计",
|
||||
path: "/admin/audit",
|
||||
@ -58,6 +64,26 @@ export default function AdminLayout() {
|
||||
const navigate = useNavigate();
|
||||
const { user, logout, isAuthenticated, loading } = useAuth();
|
||||
|
||||
// 数据范围徽标 —— 让用户看得见「我为什么只看到这些」。
|
||||
// 空范围(未分组)必须显眼:列表全空却没有任何提示,用户会以为系统坏了,
|
||||
// 这是最难排查的一类反馈。
|
||||
const scope = user?.scope;
|
||||
const scopeBadge = !scope
|
||||
? null
|
||||
: scope.is_empty
|
||||
? {
|
||||
text: "未分组 · 暂无数据权限",
|
||||
cls: "bg-orange-50 text-orange-600",
|
||||
tip: "你未被分入任何业务组,列表会是空的。请联系管理员把你分配到对应分组。",
|
||||
}
|
||||
: scope.is_unrestricted
|
||||
? { text: "全厂", cls: "bg-gray-100 text-gray-500", tip: "可查看全部数据" }
|
||||
: {
|
||||
text: `${scope.groups.join(" / ") || "已分组"} · ${scope.phase_labels.join(" / ")}`,
|
||||
cls: "bg-blue-50 text-blue-600",
|
||||
tip: `可见数据范围:${scope.phase_labels.join("、")}`,
|
||||
};
|
||||
|
||||
// 认证加载中
|
||||
if (loading) {
|
||||
return (
|
||||
@ -138,8 +164,21 @@ export default function AdminLayout() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 用户信息 + 登出 */}
|
||||
{/* 数据范围 + 用户信息 + 登出 */}
|
||||
<div className="flex items-center gap-3">
|
||||
{scopeBadge && (
|
||||
<span
|
||||
className={`rounded px-2 py-0.5 text-xs ${scopeBadge.cls}`}
|
||||
title={scopeBadge.tip}
|
||||
>
|
||||
{scopeBadge.text}
|
||||
</span>
|
||||
)}
|
||||
{scope?.is_leader && (
|
||||
<span className="rounded bg-emerald-50 px-1.5 py-0.5 text-xs text-emerald-600" title="你是某个业务组的组长">
|
||||
组长
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5 text-xs text-gray-500">
|
||||
<User className="h-3.5 w-3.5" />
|
||||
<span className="font-medium text-gray-700">
|
||||
|
||||
Reference in New Issue
Block a user