feat(dev-task): 开发任务模块 V1 完整实现

- 核心库:dev-task.ts(类型+状态机+进度计算)、task-worklog.ts(工时)、task-category.ts(字典)、work-item.ts(聚合契约)
- Store:useDevTaskStore(CRUD+状态流转)、useTaskWorklogStore(工时记录)、useTaskCategoryStore(字典管理)
- UI组件:DevTaskTab、DevTaskCreateModal、DevTaskDetailDrawer、DevTaskRow、WorklogPanel、StatusBadge、CategoryChip
- 集成:版本详情页"开发任务"Tab、"与我相关"工作台开发任务分组、任务类型管理页
- 设计文档:spec + 实施计划

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Script Generator
2026-06-11 18:02:07 +08:00
parent 8bc23fbf3d
commit a560634091
20 changed files with 3054 additions and 6 deletions

93
apps/web/lib/dev-task.ts Normal file
View File

@@ -0,0 +1,93 @@
import type { Priority } from './derive';
export type DevTaskStatus = 'todo' | 'in_progress' | 'testing' | 'submitted' | 'done';
export interface DevTask {
id: string;
taskNo: string;
requirementId: string;
title: string;
description?: string;
categoryId: string;
assigneeId: string;
reviewerId?: string;
priority: Priority;
estimateHours: number;
actualHours: number;
startDate?: string;
dueDate?: string;
completedAt?: string;
status: DevTaskStatus;
isBlocked: boolean;
blockReason?: string;
blockedById?: string;
predecessorIds?: string[];
riskLevel?: 'low' | 'medium' | 'high';
createdBy: string;
createdAt: string;
updatedAt: string;
}
export const DEV_TASK_STATUS_LABEL: Record<DevTaskStatus, string> = {
todo: '待开发',
in_progress: '开发中',
testing: '自测',
submitted: '提测',
done: '已完成',
};
export const DEV_TASK_STATUS_COLOR: Record<DevTaskStatus, string> = {
todo: 'bg-zinc-100 text-zinc-600',
in_progress: 'bg-blue-50 text-blue-600',
testing: 'bg-purple-50 text-purple-600',
submitted: 'bg-orange-50 text-orange-600',
done: 'bg-emerald-50 text-emerald-600',
};
export const STATUS_PROGRESS: Record<DevTaskStatus, number> = {
todo: 0,
in_progress: 50,
testing: 80,
submitted: 90,
done: 100,
};
export const ALLOWED_TRANSITIONS: Record<DevTaskStatus, DevTaskStatus[]> = {
todo: ['in_progress'],
in_progress: ['testing'],
testing: ['submitted', 'in_progress'],
submitted: ['done', 'in_progress'],
done: [],
};
export function canTransition(from: DevTaskStatus, to: DevTaskStatus): boolean {
return ALLOWED_TRANSITIONS[from].includes(to);
}
export function calcTaskProgress(task: DevTask): number {
return STATUS_PROGRESS[task.status];
}
export function calcGroupProgress(tasks: DevTask[]): number {
if (tasks.length === 0) return 0;
const totalEstimate = tasks.reduce((sum, t) => sum + t.estimateHours, 0);
if (totalEstimate === 0) return 0;
const weighted = tasks.reduce((sum, t) => sum + t.estimateHours * STATUS_PROGRESS[t.status], 0);
return Math.round(weighted / totalEstimate);
}
export function formatHours(hours: number): string {
if (hours < 8) return `${hours}h`;
const days = Math.floor(hours / 8);
const remainder = hours % 8;
if (remainder === 0) return `${hours}h${days}人天)`;
return `${hours}h${days}人天)`;
}
export function generateTaskNo(existingTasks: DevTask[]): string {
const maxNum = existingTasks.reduce((max, t) => {
const num = parseInt(t.taskNo.replace('DEV-', ''), 10);
return isNaN(num) ? max : Math.max(max, num);
}, 0);
return `DEV-${String(maxNum + 1).padStart(3, '0')}`;
}

View File

@@ -0,0 +1,36 @@
export type CategoryGroup = 'development' | 'testing' | 'implementation' | 'other';
export interface TaskCategory {
id: string;
name: string;
group: CategoryGroup;
color?: string;
sortOrder: number;
isSystem: boolean;
}
export const CATEGORY_GROUP_LABEL: Record<CategoryGroup, string> = {
development: '开发',
testing: '测试',
implementation: '实施',
other: '其他',
};
export const PRESET_CATEGORIES: TaskCategory[] = [
{ id: 'cat-1', name: '前端开发', group: 'development', color: '#3b82f6', sortOrder: 1, isSystem: true },
{ id: 'cat-2', name: '后端开发', group: 'development', color: '#6366f1', sortOrder: 2, isSystem: true },
{ id: 'cat-3', name: '数据库设计', group: 'development', color: '#8b5cf6', sortOrder: 3, isSystem: true },
{ id: 'cat-4', name: '接口联调', group: 'development', color: '#0ea5e9', sortOrder: 4, isSystem: true },
{ id: 'cat-5', name: '测试验证', group: 'testing', color: '#a855f7', sortOrder: 5, isSystem: true },
{ id: 'cat-6', name: '缺陷修复', group: 'testing', color: '#ef4444', sortOrder: 6, isSystem: true },
{ id: 'cat-7', name: '数据处理', group: 'implementation', color: '#f59e0b', sortOrder: 7, isSystem: true },
{ id: 'cat-8', name: '实施支持', group: 'implementation', color: '#10b981', sortOrder: 8, isSystem: true },
];
export function getCategoryById(categories: TaskCategory[], id: string): TaskCategory | undefined {
return categories.find((c) => c.id === id);
}
export function getCategoriesByGroup(categories: TaskCategory[], group: CategoryGroup): TaskCategory[] {
return categories.filter((c) => c.group === group).sort((a, b) => a.sortOrder - b.sortOrder);
}

View File

@@ -0,0 +1,33 @@
export interface TaskWorklog {
id: string;
taskId: string;
userId: string;
date: string;
hours: number;
workContent: string;
createdAt: string;
}
export function calcActualHours(worklogs: TaskWorklog[], taskId: string): number {
return worklogs
.filter((w) => w.taskId === taskId)
.reduce((sum, w) => sum + w.hours, 0);
}
export function getTaskWorklogs(worklogs: TaskWorklog[], taskId: string): TaskWorklog[] {
return worklogs
.filter((w) => w.taskId === taskId)
.sort((a, b) => b.date.localeCompare(a.date));
}
export function getUserDailyWorklogs(worklogs: TaskWorklog[], userId: string, date: string): TaskWorklog[] {
return worklogs.filter((w) => w.userId === userId && w.date === date);
}
export function getUserDailySummary(worklogs: TaskWorklog[], userId: string, date: string): { total: number; items: { taskId: string; hours: number; workContent: string }[] } {
const items = getUserDailyWorklogs(worklogs, userId, date);
return {
total: items.reduce((sum, w) => sum + w.hours, 0),
items: items.map((w) => ({ taskId: w.taskId, hours: w.hours, workContent: w.workContent })),
};
}

49
apps/web/lib/work-item.ts Normal file
View File

@@ -0,0 +1,49 @@
import type { DevTask } from './dev-task';
import type { VersionPlan } from './version-plan';
import type { Priority } from './derive';
export type WorkItemEntityType = 'plan' | 'devTask' | 'testCase' | 'bug';
export interface WorkItem {
entityType: WorkItemEntityType;
entityId: string;
title: string;
status: string;
isBlocked?: boolean;
assigneeId: string;
reviewerId?: string;
versionId: string;
versionName?: string;
priority?: Priority;
dueDate?: string;
categoryLabel?: string;
}
export function planToWorkItem(plan: VersionPlan): WorkItem {
return {
entityType: 'plan',
entityId: plan.id,
title: plan.title,
status: plan.status,
assigneeId: plan.owner,
versionId: plan.versionId,
dueDate: plan.endTime.slice(0, 10),
categoryLabel: plan.type === 'research' ? '调研' : plan.type === 'product' ? '产品方案' : 'UI设计',
};
}
export function devTaskToWorkItem(task: DevTask, versionId: string, categoryLabel?: string): WorkItem {
return {
entityType: 'devTask',
entityId: task.id,
title: task.title,
status: task.status,
isBlocked: task.isBlocked,
assigneeId: task.assigneeId,
reviewerId: task.reviewerId,
versionId,
priority: task.priority,
dueDate: task.dueDate,
categoryLabel,
};
}