feat(V0.2): 任务类型标签 —— 项目级自定义 + 任务标记 + 列表/看板展示
Docker Build and Push / build-image (push) Successful in 1m21s

- 数据: Project.taskKinds (JSON, 未配置回落默认 需求/Bug/FEATURE/优化),
  Task.kind (≤20字); 迁移 20261025030000_task_kind
- 服务: 项目 PUT 支持整体替换 taskKinds (去重/上限30/至少1个);
  任务 create/update 支持 kind (空串清空)
- 项目管理: 新增「任务类型」分区, chips 展示 + 弹层编辑 (删除/新增)
- 任务详情: 新增「类型」字段, 项目类型 chips 点选, 可清空
- 展示: 列表行左下与看板卡左下显示彩色小标签
  (需求蓝/Bug红/FEATURE紫/优化橙, 自定义类型灰)
This commit is contained in:
jif2.zhang
2026-10-25 02:53:25 +08:00
parent 2e6cc986a0
commit fb9e2350b3
11 changed files with 192 additions and 7 deletions
+23 -1
View File
@@ -38,6 +38,20 @@ export async function ensurePersonalProject(userId) {
return project
}
// 任务类型标签: 项目级自定义, 未配置时用默认四类
export const DEFAULT_TASK_KINDS = ['需求', 'Bug', 'FEATURE', '优化']
export function normTaskKinds(v) {
if (!Array.isArray(v)) return null
const out = []
for (const k of v) {
const s = String(k || '').trim().slice(0, 20)
if (s && !out.includes(s)) out.push(s)
if (out.length >= 30) break
}
return out
}
function projectSummary(p) {
return {
id: p.id,
@@ -47,6 +61,7 @@ function projectSummary(p) {
archived: p.archived,
createdAt: p.createdAt,
updatedAt: p.updatedAt,
taskKinds: normTaskKinds(p.taskKinds) || DEFAULT_TASK_KINDS,
memberCount: p._count?.members ?? undefined,
taskCount: p._count?.tasks ?? undefined,
}
@@ -119,7 +134,7 @@ router.put('/:id', async (req, res, next) => {
const userId = req.user.sub
const project = await loadProjectForUser(req.params.id, userId)
if (!project) return res.status(404).json({ error: '项目不存在或无权限' })
const { name, archived, notes } = req.body || {}
const { name, archived, notes, taskKinds } = req.body || {}
const data = {}
if (name !== undefined) {
const t = String(name).trim()
@@ -127,6 +142,13 @@ router.put('/:id', async (req, res, next) => {
if (t.length > MAX_NAME) return res.status(400).json({ error: `项目名称不能超过 ${MAX_NAME} 字` })
data.name = t
}
// 任务类型标签: 整体替换; 空数组回落默认
if (taskKinds !== undefined) {
if (!Array.isArray(taskKinds)) return res.status(400).json({ error: '任务类型格式非法' })
const kinds = normTaskKinds(taskKinds)
if (!kinds?.length) return res.status(400).json({ error: '至少保留一个任务类型' })
data.taskKinds = kinds
}
if (archived !== undefined) data.archived = !!archived
// 项目信息块 (T-PJ-13): 任意多个 { title, content }, 整体替换
if (notes !== undefined) {
+7 -1
View File
@@ -70,7 +70,7 @@ router.post('/projects/:id/tasks', async (req, res, next) => {
const project = await loadProjectForUser(req.params.id, userId)
if (!project) return res.status(404).json({ error: '项目不存在或无权限' })
const { title, description = '', parentId, assigneeId, memberIds = [], tags = [] } = req.body || {}
const { title, description = '', parentId, assigneeId, memberIds = [], tags = [], kind } = req.body || {}
const cleanTitle = String(title || '').trim()
if (!cleanTitle) return res.status(400).json({ error: '任务标题不能为空' })
if (cleanTitle.length > 200) return res.status(400).json({ error: '任务标题不能超过 200 字' })
@@ -107,6 +107,7 @@ router.post('/projects/:id/tasks', async (req, res, next) => {
parentId: pid,
title: cleanTitle,
description: String(description || ''),
kind: kind ? String(kind).trim().slice(0, 20) : null,
status: firstCol.boundStatus || 'todo',
assigneeId: finalAssignee,
creatorId: userId,
@@ -177,6 +178,11 @@ router.put('/tasks/:id', async (req, res, next) => {
data.title = t
}
if (body.description !== undefined) data.description = String(body.description)
// 任务类型标签: null 清空, 否则截断保存
if (body.kind !== undefined) {
const k = String(body.kind || '').trim().slice(0, 20)
data.kind = k || null
}
if (body.extras !== undefined) {
if (!Array.isArray(body.extras)) return res.status(400).json({ error: '扩展信息格式非法' })
data.extras = body.extras.map((e) => ({