feat(版本详情): 完善流程日志与需求覆盖

This commit is contained in:
Script Generator
2026-06-30 11:00:07 +08:00
parent d754585fe0
commit 1cd595c42e
38 changed files with 4123 additions and 237 deletions

View File

@@ -1,8 +1,9 @@
'use client';
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { X, Link2, ChevronRight, ArrowRightLeft } from 'lucide-react';
import { BugStatusBadge } from './BugStatusBadge';
import { ActivityLogPanel } from '@/components/ActivityLogPanel';
import { useBugStore } from '@/stores/useBugStore';
import { useTestCaseStore } from '@/stores/useTestCaseStore';
import { useRequirementStore } from '@/stores/useRequirementStore';
@@ -11,6 +12,7 @@ import { useAuthStore } from '@/stores/useAuthStore';
import { BUG_ALLOWED_TRANSITIONS, BUG_STATUS_LABEL, BUG_SEVERITY_LABEL, BUG_SEVERITY_COLOR } from '@/lib/bug';
import { formatDateTime } from '@/lib/format';
import { isMemberReference, resolveMemberDisplayName } from '@/lib/member-system';
import type { EntityActivityLogEntry } from '@/lib/entity-activity-log';
import type { BugStatus } from '@/lib/bug';
const LOG_ACTION_LABEL: Record<string, string> = {
@@ -53,6 +55,21 @@ export function BugDetailDrawer({ bugId, onClose, contextLabel }: Props) {
const [transferTo, setTransferTo] = useState('');
const [transferRemark, setTransferRemark] = useState('');
const [lightboxSrc, setLightboxSrc] = useState<string | null>(null);
const legacyLogEntries = useMemo<EntityActivityLogEntry[]>(() => {
return (bug.logs || []).map((log) => {
const from = log.fromValue ? resolveMemberDisplayName(log.fromValue, members) : '';
const to = log.toValue ? resolveMemberDisplayName(log.toValue, members) : '';
const change = from && to ? `${from}${to}` : '';
const remark = log.remark ? `${log.remark}` : '';
return {
id: `bug-log-${log.id}`,
occurredAt: log.createdAt,
actorId: resolveMemberDisplayName(log.operator, members),
label: LOG_ACTION_LABEL[log.action] || log.action,
summary: [change, remark].filter(Boolean).join(' ') || bug.title,
};
});
}, [bug.logs, bug.title, members]);
const handleTransition = (to: BugStatus) => {
if (to === 'fixed') { setShowResolutionInput(true); return; }
@@ -201,27 +218,7 @@ export function BugDetailDrawer({ bugId, onClose, contextLabel }: Props) {
</div>
)}
{/* 操作日志 */}
{bug.logs && bug.logs.length > 0 && (
<div className="rounded-xl border border-[var(--line)] bg-[var(--bg-card)] p-4">
<div className="text-[10px] text-[var(--ink-muted)] uppercase tracking-wide mb-3"></div>
<div className="space-y-2.5">
{[...bug.logs].reverse().map((log) => (
<div key={log.id} className="flex gap-2.5 text-[11px]">
<span className="text-[var(--ink-muted)] tabular-nums shrink-0 w-[110px]">{formatDateTime(log.createdAt)}</span>
<div className="flex-1">
<span className="font-medium text-[var(--ink)]">{resolveMemberDisplayName(log.operator, members)}</span>
<span className="text-[var(--ink-soft)]"> {LOG_ACTION_LABEL[log.action] || log.action}</span>
{log.fromValue && log.toValue && (
<span className="text-[var(--ink-muted)]"> {resolveMemberDisplayName(log.fromValue, members)} {resolveMemberDisplayName(log.toValue, members)}</span>
)}
{log.remark && <span className="text-[var(--ink-muted)]"> ({log.remark})</span>}
</div>
</div>
))}
</div>
</div>
)}
<ActivityLogPanel sourceType="bug" sourceId={bug.id} legacyEntries={legacyLogEntries} />
</div>
</div>

View File

@@ -27,20 +27,30 @@ function BugRowImpl({ bug, testCaseNo, onClick }: Props) {
const members = useMemberStore((s) => s.members);
const assigneeName = resolveMemberDisplayName(bug.assigneeId, members);
return (
<div onClick={onClick} className="flex items-center gap-3 px-4 py-2.5 border-b border-[var(--line)] hover:bg-[var(--bg-subtle)] cursor-pointer transition-colors last:border-b-0">
<span className={`h-2 w-2 rounded-full shrink-0 ${PRIORITY_DOT[bug.priority] || 'bg-zinc-300'}`} />
<span className="text-[11px] font-mono text-[var(--ink-muted)] w-16 shrink-0">{bug.bugNo}</span>
<span className="text-[13px] text-[var(--ink)] flex-1 truncate">{bug.title}</span>
<span className={`text-[10px] px-1.5 py-0.5 rounded shrink-0 ${BUG_SEVERITY_COLOR[bug.severity]}`}>{BUG_SEVERITY_LABEL[bug.severity]}</span>
<BugStatusBadge status={bug.status} />
<span className="text-[11px] text-[var(--ink-muted)] tabular-nums w-24 text-right shrink-0 whitespace-nowrap" title="计划修复时间">
{bug.plannedFixAt ? formatDateTimeShort(bug.plannedFixAt) : '待排期'}
</span>
{actualHours > 0 && (
<span className="text-[11px] text-[var(--ink-muted)] tabular-nums w-28 text-right shrink-0 whitespace-nowrap">{formatWorkHours(actualHours)}</span>
)}
{testCaseNo && <span className="text-[10px] font-mono text-[var(--ink-muted)] w-14 text-right shrink-0">{testCaseNo}</span>}
<span className="text-[11px] text-[var(--ink-soft)] w-14 text-right truncate shrink-0">{assigneeName}</span>
<div onClick={onClick} className="px-4 py-2 border-b border-[var(--line)] hover:bg-[var(--bg-subtle)] cursor-pointer transition-colors last:border-b-0">
<div className="flex min-w-0 items-start gap-2">
<span className={`mt-1.5 h-2 w-2 shrink-0 rounded-full ${PRIORITY_DOT[bug.priority] || 'bg-zinc-300'}`} />
<span className="mt-0.5 w-16 shrink-0 text-[11px] font-mono text-[var(--ink-muted)]">{bug.bugNo}</span>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-center gap-1.5">
<span className="truncate text-[13px] font-medium leading-5 text-[var(--ink)]">{bug.title}</span>
<span className="ml-auto inline-flex min-w-0 shrink-0 flex-wrap items-center justify-end gap-x-2.5 gap-y-1 text-[11px]">
<span className="tabular-nums whitespace-nowrap text-[var(--ink-muted)]" title="计划修复时间">
{bug.plannedFixAt ? formatDateTimeShort(bug.plannedFixAt) : '待排期'}
</span>
{actualHours > 0 && (
<span className="tabular-nums whitespace-nowrap text-[var(--ink-muted)]"> {formatWorkHours(actualHours)}</span>
)}
</span>
</div>
<div className="mt-1 flex min-w-0 flex-wrap items-center justify-end gap-x-2.5 gap-y-1 text-[11px]">
<span className={`shrink-0 rounded px-1.5 py-0.5 text-[10px] ${BUG_SEVERITY_COLOR[bug.severity]}`}>{BUG_SEVERITY_LABEL[bug.severity]}</span>
<BugStatusBadge status={bug.status} />
<span className="max-w-[120px] truncate whitespace-nowrap text-[var(--ink-soft)]">{assigneeName}</span>
{testCaseNo && <span className="font-mono text-[10px] text-[var(--ink-muted)]">{testCaseNo}</span>}
</div>
</div>
</div>
</div>
);
}

View File

@@ -5,7 +5,7 @@ import type { BugStatus } from '@/lib/bug';
export function BugStatusBadge({ status }: { status: BugStatus }) {
return (
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium ${BUG_STATUS_COLOR[status]}`}>
<span className={`inline-flex h-5 shrink-0 items-center whitespace-nowrap rounded px-1.5 text-[10px] font-medium ${BUG_STATUS_COLOR[status]}`}>
{BUG_STATUS_LABEL[status]}
</span>
);