加班记录: - 加班人默认回显当前用户且不可改 - 列表加创建日期列 - 移除编辑功能(创建即提交,仅可删除) - 提交时校验结束>开始 版本列表: - 移除顶部 9 个状态 tab + 表格状态列 - 仅保留搜索、项目、优先级筛选 项目列表: - 行内显示进度未达 100% 的版本(与版本页同源算法) - 新增 lib/version-progress.ts 抽出公共进度计算 角色权限: - RoleItem 加 permissions 字段 - 新建 lib/permissions.ts: 14 组 37 个权限点 + 默认 5 角色映射 + hasPermission - 角色表单内嵌权限矩阵(主模块 4 件套 + Tab 二档 + Bug Tab 4 档)+ 全选/反选/仅查看快捷 - 新建 components/auth/Guard.tsx: RouteGuard + PermissionGuard + useHasPermission + AccessDenied - Sidebar 菜单按 view 权限过滤 - 7 个主路由(products/projects/versions/requirements/overtime/admin/members/roles)包 RouteGuard - 版本详情 Tab 按 view 权限过滤,自动跳转到第一个有权限的 Tab - 默认未登录/无角色按只读最严格 - 超管 role-admin: ['*'] 通配符,permissions 不可改 通用: - 新建 components/FieldError.tsx 统一表单错误文案 - PlanTab 提交时校验结束>开始 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
59 lines
1.8 KiB
TypeScript
59 lines
1.8 KiB
TypeScript
'use client';
|
||
|
||
import { ReactNode } from 'react';
|
||
import { Lock } from 'lucide-react';
|
||
import { useAuthStore } from '@/stores/useAuthStore';
|
||
import { useMemberStore } from '@/stores/useMemberStore';
|
||
import { hasPermission } from '@/lib/permissions';
|
||
|
||
/**
|
||
* 当前登录用户对某权限点的判定
|
||
* 未登录 / 找不到 role 时一律 false(最严格只读)
|
||
*/
|
||
export function useHasPermission(permission: string): boolean {
|
||
const user = useAuthStore((s) => s.user);
|
||
const role = useMemberStore((s) => s.roles.find((r) => r.id === user?.roleId));
|
||
return hasPermission(role, permission);
|
||
}
|
||
|
||
/**
|
||
* 路由级拦截:包裹整页,无权限显示 AccessDenied
|
||
*/
|
||
export function RouteGuard({ permission, children }: { permission: string; children: ReactNode }) {
|
||
const allowed = useHasPermission(permission);
|
||
if (!allowed) return <AccessDenied />;
|
||
return <>{children}</>;
|
||
}
|
||
|
||
/**
|
||
* 按钮级拦截:无权限渲染 fallback(默认 null)
|
||
*/
|
||
export function PermissionGuard({
|
||
permission,
|
||
fallback = null,
|
||
children,
|
||
}: {
|
||
permission: string;
|
||
fallback?: ReactNode;
|
||
children: ReactNode;
|
||
}) {
|
||
const allowed = useHasPermission(permission);
|
||
if (!allowed) return <>{fallback}</>;
|
||
return <>{children}</>;
|
||
}
|
||
|
||
/**
|
||
* 路由级"无权访问"占位
|
||
*/
|
||
export function AccessDenied() {
|
||
return (
|
||
<div className="flex h-full items-center justify-center bg-[var(--bg)]">
|
||
<div className="flex flex-col items-center gap-3 rounded-2xl border border-dashed border-[var(--line)] bg-[var(--bg-card)] px-12 py-16">
|
||
<Lock className="h-8 w-8 text-[var(--ink-muted)]" strokeWidth={1.5} />
|
||
<p className="text-[14px] font-medium text-[var(--ink-soft)]">无权访问该模块</p>
|
||
<p className="text-[12px] text-[var(--ink-muted)]">请联系管理员开通对应权限</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|