feat: 加班/版本/项目调整 + 角色权限设计
加班记录: - 加班人默认回显当前用户且不可改 - 列表加创建日期列 - 移除编辑功能(创建即提交,仅可删除) - 提交时校验结束>开始 版本列表: - 移除顶部 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>
This commit is contained in:
@@ -1,26 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import { RouteGuard } from '@/components/auth/Guard';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Search, FolderKanban, Package, ChevronDown, Plus, X } from 'lucide-react';
|
||||
import { useProductStore } from '@/stores/useProductStore';
|
||||
import { flattenProjects, ProjectWithContext } from '@/lib/derive';
|
||||
import { useRequirementStore } from '@/stores/useRequirementStore';
|
||||
import { useVersionPlanStore } from '@/stores/useVersionPlanStore';
|
||||
import { useDevTaskStore } from '@/stores/useDevTaskStore';
|
||||
import { useTestCaseStore } from '@/stores/useTestCaseStore';
|
||||
import { flattenProjects, flattenVersions, ProjectWithContext } from '@/lib/derive';
|
||||
import { VersionChip } from '@/components/version/VersionChip';
|
||||
import { VersionStatus } from '@/lib/version-status';
|
||||
import { Pagination, usePagination } from '@/components/Pagination';
|
||||
import { buildVersionProgressMap } from '@/lib/version-progress';
|
||||
|
||||
export default function ProjectsPage() {
|
||||
return (
|
||||
<RouteGuard permission="project:view">
|
||||
<ProjectsPageContent />
|
||||
</RouteGuard>
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectsPageContent() {
|
||||
const router = useRouter();
|
||||
const { overview, fetchOverview, createProject } = useProductStore();
|
||||
const { requirements, fetchRequirements } = useRequirementStore();
|
||||
const { plans, fetchPlans } = useVersionPlanStore();
|
||||
const { tasks: devTasks, fetchTasks: fetchDevTasks } = useDevTaskStore();
|
||||
const { testCases, fetchTestCases } = useTestCaseStore();
|
||||
const [search, setSearch] = useState('');
|
||||
const [productFilter, setProductFilter] = useState<string>('all');
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchOverview();
|
||||
}, [fetchOverview]);
|
||||
useEffect(() => { fetchOverview(); }, [fetchOverview]);
|
||||
useEffect(() => { fetchRequirements(); }, [fetchRequirements]);
|
||||
useEffect(() => { fetchPlans(); }, [fetchPlans]);
|
||||
useEffect(() => { fetchDevTasks(); }, [fetchDevTasks]);
|
||||
useEffect(() => { fetchTestCases(); }, [fetchTestCases]);
|
||||
|
||||
const allProjects = useMemo(() => flattenProjects(overview), [overview]);
|
||||
const allVersions = useMemo(() => flattenVersions(overview), [overview]);
|
||||
|
||||
const versionProgressMap = useMemo(
|
||||
() => buildVersionProgressMap(allVersions, plans, requirements, devTasks, testCases),
|
||||
[allVersions, plans, requirements, devTasks, testCases],
|
||||
);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
return allProjects.filter((p) => {
|
||||
@@ -96,7 +121,7 @@ export default function ProjectsPage() {
|
||||
<div>
|
||||
<div className="overflow-hidden rounded-2xl border border-[var(--line)] bg-[var(--bg-card)] shadow-[var(--shadow-sm)]">
|
||||
{paged.map((proj) => (
|
||||
<ProjectRow key={proj.id} proj={proj} onClick={() => router.push(`/projects/${proj.id}`)} />
|
||||
<ProjectRow key={proj.id} proj={proj} versionProgressMap={versionProgressMap} onClick={() => router.push(`/projects/${proj.id}`)} />
|
||||
))}
|
||||
</div>
|
||||
<Pagination total={total} page={page} pageSize={pageSize} onChange={setPage} onPageSizeChange={setPageSize} />
|
||||
@@ -123,13 +148,15 @@ export default function ProjectsPage() {
|
||||
|
||||
function ProjectRow({
|
||||
proj,
|
||||
versionProgressMap,
|
||||
onClick,
|
||||
}: {
|
||||
proj: ProjectWithContext;
|
||||
versionProgressMap: Record<string, number>;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
const activeVersions = proj.versions.filter((v) => v.status !== 'released');
|
||||
const releasedCount = proj.versions.filter((v) => v.status === 'released').length;
|
||||
const activeVersions = proj.versions.filter((v) => (versionProgressMap[v.id] ?? 0) < 100);
|
||||
const completedCount = proj.versions.filter((v) => (versionProgressMap[v.id] ?? 0) >= 100).length;
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -152,9 +179,9 @@ function ProjectRow({
|
||||
{activeVersions.map((v) => (
|
||||
<VersionChip key={v.id} name={v.name} status={v.status} />
|
||||
))}
|
||||
{releasedCount > 0 && (
|
||||
{completedCount > 0 && (
|
||||
<span className="text-xs text-[var(--ink-muted)] ml-1">
|
||||
+{releasedCount} 已发布
|
||||
+{completedCount} 已完成
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user