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:
73
apps/web/app/admin/categories/page.tsx
Normal file
73
apps/web/app/admin/categories/page.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Plus, Pencil, Trash2, Shield } from 'lucide-react';
|
||||
import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore';
|
||||
import { CATEGORY_GROUP_LABEL } from '@/lib/task-category';
|
||||
import type { CategoryGroup } from '@/lib/task-category';
|
||||
|
||||
export default function CategoriesPage() {
|
||||
const { categories, fetchCategories, addCategory, updateCategory, deleteCategory } = useTaskCategoryStore();
|
||||
useEffect(() => { fetchCategories(); }, [fetchCategories]);
|
||||
|
||||
const [newName, setNewName] = useState('');
|
||||
const [newGroup, setNewGroup] = useState<CategoryGroup>('development');
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editName, setEditName] = useState('');
|
||||
|
||||
const groups: CategoryGroup[] = ['development', 'testing', 'implementation', 'other'];
|
||||
|
||||
const handleAdd = () => {
|
||||
if (!newName.trim()) return;
|
||||
addCategory(newName.trim(), newGroup);
|
||||
setNewName('');
|
||||
};
|
||||
|
||||
const handleSaveEdit = (id: string) => {
|
||||
if (editName.trim()) updateCategory(id, { name: editName.trim() });
|
||||
setEditingId(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-2xl mx-auto space-y-6">
|
||||
<h1 className="text-[16px] font-semibold text-[var(--ink)]">任务类型管理</h1>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<input value={newName} onChange={(e) => setNewName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleAdd(); }} placeholder="新类型名称" className="flex-1 h-9 rounded-lg border border-[var(--line)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" />
|
||||
<select value={newGroup} onChange={(e) => setNewGroup(e.target.value as CategoryGroup)} className="h-9 rounded-lg border border-[var(--line)] px-3 text-[13px]">
|
||||
{groups.map((g) => <option key={g} value={g}>{CATEGORY_GROUP_LABEL[g]}</option>)}
|
||||
</select>
|
||||
<button onClick={handleAdd} className="h-9 px-4 rounded-lg text-[12px] font-medium bg-[var(--accent)] text-white hover:bg-[var(--accent-hover)]"><Plus className="h-3.5 w-3.5 inline mr-1" />添加</button>
|
||||
</div>
|
||||
|
||||
{groups.map((g) => {
|
||||
const items = categories.filter((c) => c.group === g);
|
||||
if (items.length === 0) return null;
|
||||
return (
|
||||
<div key={g}>
|
||||
<h2 className="text-[13px] font-medium text-[var(--ink-soft)] mb-2">{CATEGORY_GROUP_LABEL[g]}</h2>
|
||||
<div className="rounded-lg border border-[var(--line)] divide-y divide-[var(--line)]">
|
||||
{items.map((cat) => (
|
||||
<div key={cat.id} className="flex items-center gap-3 px-4 py-2.5">
|
||||
{cat.color && <span className="h-3 w-3 rounded-full shrink-0" style={{ backgroundColor: cat.color }} />}
|
||||
{editingId === cat.id ? (
|
||||
<input value={editName} onChange={(e) => setEditName(e.target.value)} onBlur={() => handleSaveEdit(cat.id)} onKeyDown={(e) => { if (e.key === 'Enter') handleSaveEdit(cat.id); }} autoFocus className="flex-1 h-7 rounded border border-[var(--line)] px-2 text-[13px] focus:border-[var(--accent)] focus:outline-none" />
|
||||
) : (
|
||||
<span className="flex-1 text-[13px] text-[var(--ink)]">{cat.name}</span>
|
||||
)}
|
||||
{cat.isSystem && <span title="系统预置"><Shield className="h-3 w-3 text-[var(--ink-muted)]" /></span>}
|
||||
{!cat.isSystem && (
|
||||
<>
|
||||
<button onClick={() => { setEditingId(cat.id); setEditName(cat.name); }} className="p-1 rounded hover:bg-[var(--bg-subtle)]"><Pencil className="h-3 w-3 text-[var(--ink-muted)]" /></button>
|
||||
<button onClick={() => deleteCategory(cat.id)} className="p-1 rounded hover:bg-red-50"><Trash2 className="h-3 w-3 text-[var(--ink-muted)] hover:text-red-500" /></button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user