export type CategoryGroup = 'development' | 'testing' | 'implementation' | 'other'; export const DEFAULT_TEST_CATEGORY_ID = 'cat-test-functional'; export interface TaskCategory { id: string; code: string; name: string; group: CategoryGroup; color?: string; sortOrder: number; isSystem: boolean; } export const CATEGORY_GROUP_LABEL: Record = { development: '开发', testing: '测试', implementation: '实施', other: '其他', }; export const PRESET_CATEGORIES: TaskCategory[] = [ { id: 'cat-1', code: 'frontend_development', name: '前端开发', group: 'development', color: '#3b82f6', sortOrder: 1, isSystem: true }, { id: 'cat-frontend-interaction', code: 'frontend_interaction', name: '前端交互', group: 'development', color: '#0ea5e9', sortOrder: 2, isSystem: true }, { id: 'cat-2', code: 'backend_development', name: '后端开发', group: 'development', color: '#6366f1', sortOrder: 3, isSystem: true }, { id: 'cat-backend-api', code: 'backend_api', name: '后端接口', group: 'development', color: '#2563eb', sortOrder: 4, isSystem: true }, { id: 'cat-3', code: 'database_schema', name: '数据库设计', group: 'development', color: '#8b5cf6', sortOrder: 5, isSystem: true }, { id: 'cat-4', code: 'api_integration', name: '接口联调', group: 'development', color: '#06b6d4', sortOrder: 6, isSystem: true }, { id: DEFAULT_TEST_CATEGORY_ID, code: 'test_functional', name: '功能测试', group: 'testing', color: '#22c55e', sortOrder: 20, isSystem: true }, { id: 'cat-test-api', code: 'test_api', name: '接口测试', group: 'testing', color: '#14b8a6', sortOrder: 21, isSystem: true }, { id: 'cat-test-exception', code: 'test_exception', name: '异常场景测试', group: 'testing', color: '#f97316', sortOrder: 22, isSystem: true }, { id: 'cat-test-compatibility', code: 'test_compatibility', name: '兼容性测试', group: 'testing', color: '#a855f7', sortOrder: 23, isSystem: true }, { id: 'cat-5', code: 'data_processing', name: '数据处理', group: 'implementation', color: '#f59e0b', sortOrder: 40, isSystem: true }, { id: 'cat-6', code: 'implementation_support', name: '实施支持', group: 'implementation', color: '#10b981', sortOrder: 41, isSystem: true }, { id: 'cat-other-doc', code: 'documentation', name: '文档', group: 'other', color: '#64748b', sortOrder: 60, isSystem: true }, ]; const LEGACY_CODE_BY_ID: Record = { 'cat-1': 'frontend_development', 'cat-2': 'backend_development', 'cat-3': 'database_schema', 'cat-4': 'api_integration', 'cat-5': 'data_processing', 'cat-6': 'implementation_support', }; function slugifyCategoryName(name: string, index: number): string { return ( name .trim() .toLowerCase() .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '_') .replace(/^_+|_+$/g, '') || `category_${index + 1}` ); } export function normalizeTaskCategory(category: Partial | undefined, index = 0): TaskCategory { const preset = PRESET_CATEGORIES.find((c) => c.id === category?.id); const name = category?.name || preset?.name || `分类 ${index + 1}`; return { id: category?.id || preset?.id || `cat-${index + 1}`, code: category?.code || LEGACY_CODE_BY_ID[category?.id ?? ''] || preset?.code || slugifyCategoryName(name, index), name, group: category?.group || preset?.group || 'other', color: category?.color ?? preset?.color, sortOrder: typeof category?.sortOrder === 'number' ? category.sortOrder : preset?.sortOrder ?? index + 1, isSystem: typeof category?.isSystem === 'boolean' ? category.isSystem : Boolean(preset?.isSystem), }; } export function normalizeTaskCategories(categories: Partial[] = []): TaskCategory[] { const merged: Partial[] = Array.isArray(categories) ? [...categories] : []; for (const preset of PRESET_CATEGORIES) { if (!merged.some((c) => c.id === preset.id)) merged.push(preset); } return merged.map((category, index) => normalizeTaskCategory(category, index)); } 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); } export function findCategoryByCode(categories: TaskCategory[], code?: string): TaskCategory | undefined { if (!code) return undefined; return categories.find((c) => c.code === code); } export function getDefaultCategoryByGroup(categories: TaskCategory[], group: CategoryGroup): TaskCategory { return getCategoriesByGroup(categories, group)[0] ?? categories[0] ?? PRESET_CATEGORIES[0]; } export function resolveCategoryIdFromCode(categories: TaskCategory[], code: string | undefined, fallbackGroup: CategoryGroup): string { return findCategoryByCode(categories, code)?.id ?? getDefaultCategoryByGroup(categories, fallbackGroup).id; }