feat(版本): 完善计划覆盖与工作台待办
关键改动: - 增加计划日志汇总、需求覆盖草稿校验与对应测试 - 抽取工作台工作项 Hook,补充待办计数能力 - 优化版本详情中计划、任务、测试用例和 Bug 的筛选与展示 Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import { useRequirementStore } from '@/stores/useRequirementStore';
|
||||
import { useMemberStore } from '@/stores/useMemberStore';
|
||||
import { useAuthStore } from '@/stores/useAuthStore';
|
||||
import { resolveMemberDisplayName } from '@/lib/member-system';
|
||||
import { FilterSelect } from '@/components/FilterSelect';
|
||||
import { WorkDateTimePicker } from '@/components/WorkDateTimePicker';
|
||||
import type { Priority } from '@/lib/derive';
|
||||
import type { BugSeverity } from '@/lib/bug';
|
||||
@@ -144,25 +145,42 @@ export function BugCreateModal({ testCaseId, onClose }: Props) {
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="block text-[12px] text-[var(--ink-soft)] mb-1">严重程度</label>
|
||||
<select value={severity} onChange={(e) => setSeverity(e.target.value as BugSeverity)} className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none">
|
||||
<option value="critical">致命</option>
|
||||
<option value="major">严重</option>
|
||||
<option value="minor">一般</option>
|
||||
<option value="trivial">轻微</option>
|
||||
</select>
|
||||
<FilterSelect
|
||||
value={severity}
|
||||
onChange={(value) => setSeverity(value as BugSeverity)}
|
||||
options={[
|
||||
{ value: 'critical', label: '致命' },
|
||||
{ value: 'major', label: '严重' },
|
||||
{ value: 'minor', label: '一般' },
|
||||
{ value: 'trivial', label: '轻微' },
|
||||
]}
|
||||
showAllOption={false}
|
||||
className="w-full"
|
||||
labelClassName="max-w-[calc(100%-20px)]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[12px] text-[var(--ink-soft)] mb-1">优先级</label>
|
||||
<select value={priority} onChange={(e) => setPriority(e.target.value as Priority)} className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none">
|
||||
{(['P0','P1','P2','P3'] as Priority[]).map((p) => <option key={p} value={p}>{p}</option>)}
|
||||
</select>
|
||||
<FilterSelect
|
||||
value={priority}
|
||||
onChange={(value) => setPriority(value as Priority)}
|
||||
options={(['P0', 'P1', 'P2', 'P3'] as Priority[]).map((p) => ({ value: p, label: p }))}
|
||||
showAllOption={false}
|
||||
className="w-full"
|
||||
labelClassName="max-w-[calc(100%-20px)]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[12px] text-[var(--ink-soft)] mb-1">修复人 *</label>
|
||||
<select value={assigneeId} onChange={(e) => setAssigneeId(e.target.value)} className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none">
|
||||
<option value="">选择修复人</option>
|
||||
{members.map((m) => <option key={m.id} value={m.name}>{m.name}</option>)}
|
||||
</select>
|
||||
<FilterSelect
|
||||
value={assigneeId}
|
||||
onChange={setAssigneeId}
|
||||
options={members.map((m) => ({ value: m.name, label: m.name }))}
|
||||
placeholder="选择修复人"
|
||||
showAllOption={false}
|
||||
className="w-full"
|
||||
labelClassName="max-w-[calc(100%-20px)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useMemo, useState } from 'react';
|
||||
import { X, Link2, ChevronRight, ArrowRightLeft } from 'lucide-react';
|
||||
import { BugStatusBadge } from './BugStatusBadge';
|
||||
import { ActivityLogPanel } from '@/components/ActivityLogPanel';
|
||||
import { FilterSelect } from '@/components/FilterSelect';
|
||||
import { useBugStore } from '@/stores/useBugStore';
|
||||
import { useTestCaseStore } from '@/stores/useTestCaseStore';
|
||||
import { useRequirementStore } from '@/stores/useRequirementStore';
|
||||
@@ -166,10 +167,14 @@ export function BugDetailDrawer({ bugId, onClose, contextLabel }: Props) {
|
||||
{showTransfer && (
|
||||
<div className="space-y-2 pt-1 border-t border-[var(--line)]">
|
||||
<div className="text-[11px] text-[var(--ink-muted)]">转交给:</div>
|
||||
<select value={transferTo} onChange={(e) => setTransferTo(e.target.value)} className="h-8 w-full rounded-lg border border-[var(--line)] px-3 text-[12px] focus:border-[var(--accent)] focus:outline-none">
|
||||
<option value="">选择接收人</option>
|
||||
{members.filter((m) => !isMemberReference(bug.assigneeId, m)).map((m) => <option key={m.id} value={m.name}>{m.name}</option>)}
|
||||
</select>
|
||||
<FilterSelect
|
||||
value={transferTo || 'all'}
|
||||
onChange={(value) => setTransferTo(value === 'all' ? '' : value)}
|
||||
options={members.filter((m) => !isMemberReference(bug.assigneeId, m)).map((m) => ({ value: m.name, label: m.name }))}
|
||||
allLabel="选择接收人"
|
||||
className="w-full"
|
||||
labelClassName="max-w-[calc(100%-20px)]"
|
||||
/>
|
||||
<input value={transferRemark} onChange={(e) => setTransferRemark(e.target.value)} placeholder="转交备注(可选)" className="h-8 w-full rounded-lg border border-[var(--line)] px-3 text-[12px] focus:border-[var(--accent)] focus:outline-none" />
|
||||
<div className="flex gap-2">
|
||||
<button onClick={handleTransfer} disabled={!transferTo} className="h-8 px-3 rounded-lg text-[11px] font-medium bg-blue-500 text-white disabled:opacity-50">确认转交</button>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useMemberStore } from '@/stores/useMemberStore';
|
||||
import { BugRow } from './BugRow';
|
||||
import { BugDetailDrawer } from './BugDetailDrawer';
|
||||
import { Pagination, usePagination } from '@/components/Pagination';
|
||||
import { FilterSelect } from '@/components/FilterSelect';
|
||||
import { SearchInput, matchTitleOrNo } from '@/components/SearchInput';
|
||||
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
|
||||
import { BUG_STATUS_LABEL, BUG_SEVERITY_LABEL, aggregateBugActualHours, bugIntervals } from '@/lib/bug';
|
||||
@@ -118,19 +119,27 @@ export function BugTab({ versionId, requirementIds }: Props) {
|
||||
<div className="flex items-center gap-2 px-1 flex-wrap">
|
||||
<Filter className="h-3.5 w-3.5 text-[var(--ink-muted)]" />
|
||||
<SearchInput value={keyword} onChange={(v) => { setKeyword(v); setPage(1); }} placeholder="搜索标题/编号" />
|
||||
<select value={filterAssignee} onChange={(e) => { setFilterAssignee(e.target.value); setPage(1); }} className="h-7 rounded-md border border-[var(--line)] bg-[var(--bg-card)] px-2 text-[11px] text-[var(--ink-soft)] focus:border-[var(--accent)] focus:outline-none">
|
||||
<option value="">全部修复人</option>
|
||||
{currentMember && <option value="__me">我的Bug</option>}
|
||||
{assignees.filter((a) => a !== currentUserName).map((a) => <option key={a} value={a}>{a}</option>)}
|
||||
</select>
|
||||
<select value={filterStatus} onChange={(e) => { setFilterStatus(e.target.value); setPage(1); }} className="h-7 rounded-md border border-[var(--line)] bg-[var(--bg-card)] px-2 text-[11px] text-[var(--ink-soft)] focus:border-[var(--accent)] focus:outline-none">
|
||||
<option value="">全部状态</option>
|
||||
{(Object.entries(BUG_STATUS_LABEL) as [BugStatus, string][]).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
||||
</select>
|
||||
<select value={filterSeverity} onChange={(e) => { setFilterSeverity(e.target.value); setPage(1); }} className="h-7 rounded-md border border-[var(--line)] bg-[var(--bg-card)] px-2 text-[11px] text-[var(--ink-soft)] focus:border-[var(--accent)] focus:outline-none">
|
||||
<option value="">全部严重程度</option>
|
||||
{(Object.entries(BUG_SEVERITY_LABEL) as [BugSeverity, string][]).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
|
||||
</select>
|
||||
<FilterSelect
|
||||
value={filterAssignee || 'all'}
|
||||
onChange={(value) => { setFilterAssignee(value === 'all' ? '' : value); setPage(1); }}
|
||||
options={[
|
||||
...(currentMember ? [{ value: '__me', label: '我的Bug' }] : []),
|
||||
...assignees.filter((assignee) => assignee !== currentUserName).map((assignee) => ({ value: assignee, label: assignee })),
|
||||
]}
|
||||
allLabel="全部修复人"
|
||||
/>
|
||||
<FilterSelect
|
||||
value={filterStatus || 'all'}
|
||||
onChange={(value) => { setFilterStatus(value === 'all' ? '' : value); setPage(1); }}
|
||||
options={(Object.entries(BUG_STATUS_LABEL) as [BugStatus, string][]).map(([value, label]) => ({ value, label }))}
|
||||
allLabel="全部状态"
|
||||
/>
|
||||
<FilterSelect
|
||||
value={filterSeverity || 'all'}
|
||||
onChange={(value) => { setFilterSeverity(value === 'all' ? '' : value); setPage(1); }}
|
||||
options={(Object.entries(BUG_SEVERITY_LABEL) as [BugSeverity, string][]).map(([value, label]) => ({ value, label }))}
|
||||
allLabel="全部严重程度"
|
||||
/>
|
||||
{hasFilter && <button onClick={() => { setFilterAssignee(''); setFilterStatus(''); setFilterSeverity(''); setKeyword(''); setPage(1); }} className="text-[11px] text-[var(--accent)] hover:underline">清除</button>}
|
||||
<span className="ml-auto text-[11px] text-[var(--ink-muted)]">{filteredBugs.length} 条</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user