feat: Bug 图片上传 + 转交 + 操作日志 + 开发中含自测

1. 开发中统计包含 in_progress + testing(自测)

2. Bug 支持上传图片:
   - BugCreateModal 加宽为 max-w-2xl
   - 新增拖拽/点击上传图片区域,转 base64 存储
   - 上传后可预览缩略图、可删除
   - BugDetailDrawer 展示截图列表,点击可放大

3. Bug 支持转交:
   - BugDetailDrawer 操作栏新增"转交"按钮
   - 选人下拉 + 备注输入 → 确认转交
   - transferBug store 方法:更新 assigneeId + 追加日志

4. Bug 操作日志:
   - BugLog interface: create/status_change/transfer/resolve
   - createBug 时记录创建日志
   - changeStatus 时记录状态变更/修复日志
   - transferBug 时记录转交日志
   - BugDetailDrawer 底部展示操作时间线

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Script Generator
2026-06-15 13:37:01 +08:00
parent 0ed7ff1d53
commit 273182bb0f
5 changed files with 182 additions and 23 deletions

View File

@@ -1,6 +1,6 @@
'use client';
import { create } from 'zustand';
import type { Bug, BugStatus } from '@/lib/bug';
import type { Bug, BugStatus, BugLog } from '@/lib/bug';
import { canBugTransition, generateBugNo } from '@/lib/bug';
const STORAGE_KEY = 'ftb_bugs_v1';
@@ -17,13 +17,18 @@ function loadLocal(): Bug[] | null {
return null;
}
function makeLog(action: BugLog['action'], operator: string, from?: string, to?: string, remark?: string): BugLog {
return { id: `log-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, action, fromValue: from, toValue: to, operator, remark, createdAt: new Date().toISOString() };
}
interface BugState {
bugs: Bug[];
fetchBugs: () => void;
createBug: (data: Omit<Bug, 'id' | 'bugNo' | 'createdAt' | 'updatedAt' | 'status'>) => Bug;
createBug: (data: Omit<Bug, 'id' | 'bugNo' | 'createdAt' | 'updatedAt' | 'status' | 'logs'>, operator: string) => Bug;
updateBug: (id: string, data: Partial<Bug>) => void;
deleteBug: (id: string) => void;
changeStatus: (id: string, to: BugStatus, extra?: { resolution?: string }) => { ok: boolean; message?: string };
changeStatus: (id: string, to: BugStatus, operator: string, extra?: { resolution?: string }) => { ok: boolean; message?: string };
transferBug: (id: string, newAssigneeId: string, operator: string, remark?: string) => { ok: boolean; message?: string };
getByTestCase: (caseId: string) => Bug[];
getByVersion: (versionId: string) => Bug[];
getByAssignee: (assigneeId: string) => Bug[];
@@ -37,14 +42,16 @@ export const useBugStore = create<BugState>((set, get) => ({
if (cached) set({ bugs: cached });
},
createBug: (data) => {
createBug: (data, operator) => {
const list = get().bugs;
const now = new Date().toISOString();
const log = makeLog('create', operator);
const bug: Bug = {
...data,
id: `bug-${Date.now()}`,
bugNo: generateBugNo(list),
status: 'open',
logs: [log],
createdAt: now,
updatedAt: now,
};
@@ -68,21 +75,32 @@ export const useBugStore = create<BugState>((set, get) => ({
saveLocal(updated);
},
changeStatus: (id, to, extra) => {
changeStatus: (id, to, operator, extra) => {
const bug = get().bugs.find((b) => b.id === id);
if (!bug) return { ok: false, message: 'Bug不存在' };
if (!canBugTransition(bug.status, to)) {
return { ok: false, message: `不允许从「${bug.status}」流转到「${to}` };
}
const today = new Date().toISOString().slice(0, 10);
const now = new Date().toISOString();
const patch: Partial<Bug> = { status: to };
if (to === 'fixed') patch.resolvedAt = today;
if (to === 'closed') patch.closedAt = today;
if (to === 'fixed') patch.resolvedAt = now;
if (to === 'closed') patch.closedAt = now;
if (extra?.resolution) patch.resolution = extra.resolution;
const log = makeLog(to === 'fixed' ? 'resolve' : 'status_change', operator, bug.status, to, extra?.resolution);
patch.logs = [...(bug.logs || []), log];
get().updateBug(id, patch);
return { ok: true };
},
transferBug: (id, newAssigneeId, operator, remark) => {
const bug = get().bugs.find((b) => b.id === id);
if (!bug) return { ok: false, message: 'Bug不存在' };
if (bug.assigneeId === newAssigneeId) return { ok: false, message: '已是当前负责人' };
const log = makeLog('transfer', operator, bug.assigneeId, newAssigneeId, remark);
get().updateBug(id, { assigneeId: newAssigneeId, logs: [...(bug.logs || []), log] });
return { ok: true };
},
getByTestCase: (caseId) => {
return get().bugs.filter((b) => b.testCaseId === caseId);
},