'use client'; import { useState, useMemo, useEffect } from 'react'; import { Bug as BugIcon, Filter } from 'lucide-react'; import { useBugStore } from '@/stores/useBugStore'; import { useTestCaseStore } from '@/stores/useTestCaseStore'; import { useRequirementStore } from '@/stores/useRequirementStore'; import { useAuthStore } from '@/stores/useAuthStore'; import { BugRow } from './BugRow'; import { BugDetailDrawer } from './BugDetailDrawer'; import { Pagination, usePagination } from '@/components/Pagination'; import { SearchInput, matchTitleOrNo } from '@/components/SearchInput'; import { useDebouncedValue } from '@/hooks/useDebouncedValue'; import { BUG_STATUS_LABEL, BUG_SEVERITY_LABEL, bugIntervals } from '@/lib/bug'; import { formatWorkHours, calcTwoMetrics } from '@/lib/work-hours'; import type { BugStatus, BugSeverity } from '@/lib/bug'; interface Props { versionId: string; requirementIds: string[]; } export function BugTab({ versionId, requirementIds }: Props) { const { bugs, fetchBugs } = useBugStore(); const { testCases, fetchTestCases } = useTestCaseStore(); const { requirements } = useRequirementStore(); const user = useAuthStore((s) => s.user); useEffect(() => { fetchBugs(); }, [fetchBugs]); useEffect(() => { fetchTestCases(); }, [fetchTestCases]); const versionBugs = useMemo( () => bugs.filter((b) => b.versionId === versionId), [bugs, versionId], ); const openCount = versionBugs.filter((b) => b.status === 'open').length; const fixingCount = versionBugs.filter((b) => b.status === 'fixing').length; const fixedCount = versionBugs.filter((b) => b.status === 'fixed' || b.status === 'verifying').length; const closedCount = versionBugs.filter((b) => b.status === 'closed' || b.status === 'rejected').length; const criticalCount = versionBugs.filter((b) => b.severity === 'critical' && b.status !== 'closed' && b.status !== 'rejected').length; const resolveRate = versionBugs.length > 0 ? Math.round(((fixedCount + closedCount) / versionBugs.length) * 100) : 0; const { calendarHours: bugCalendarHours, manhours: bugManhours } = useMemo(() => calcTwoMetrics(bugIntervals(versionBugs)), [versionBugs]); const [filterAssignee, setFilterAssignee] = useState(''); const [filterStatus, setFilterStatus] = useState(''); const [filterSeverity, setFilterSeverity] = useState(''); const [keyword, setKeyword] = useState(''); const debouncedKeyword = useDebouncedValue(keyword, 300); const filteredBugs = useMemo(() => { let result = versionBugs; if (filterAssignee) result = result.filter((b) => b.assigneeId === filterAssignee); if (filterStatus) result = result.filter((b) => b.status === filterStatus); if (filterSeverity) result = result.filter((b) => b.severity === filterSeverity); if (debouncedKeyword) result = result.filter((b) => matchTitleOrNo({ title: b.title, no: b.bugNo }, debouncedKeyword)); return result; }, [versionBugs, filterAssignee, filterStatus, filterSeverity, debouncedKeyword]); const { paged, page, setPage, total, pageSize, setPageSize } = usePagination(filteredBugs, 20); const [selectedBugId, setSelectedBugId] = useState(null); const testCaseMap = useMemo(() => new Map(testCases.map((c) => [c.id, c])), [testCases]); const assignees = useMemo(() => { const names = new Set(versionBugs.map((b) => b.assigneeId)); return Array.from(names); }, [versionBugs]); const hasFilter = !!(filterAssignee || filterStatus || filterSeverity || debouncedKeyword); return (
Bug {versionBugs.length} 个
待修复 {openCount} · 修复中 {fixingCount} · 已修复 {fixedCount} · 已关闭 {closedCount} {criticalCount > 0 && ( {criticalCount} 致命 )} 解决率 {resolveRate}% {bugManhours > 0 && ( 日历 {formatWorkHours(bugCalendarHours)} / 人力 {formatWorkHours(bugManhours)} )}
{ setKeyword(v); setPage(1); }} placeholder="搜索标题/编号" /> {hasFilter && } {filteredBugs.length} 条
{filteredBugs.length === 0 ? (

{hasFilter ? '没有匹配的 Bug' : '暂无 Bug,很好'}

) : (
{paged.map((bug) => { const tc = testCaseMap.get(bug.testCaseId); return setSelectedBugId(bug.id)} />; })}
)} {total > 20 && } {selectedBugId && setSelectedBugId(null)} />}
); }