feat(V0.2): 任务类型标签 —— 项目级自定义 + 任务标记 + 列表/看板展示
Docker Build and Push / build-image (push) Successful in 1m21s
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:
@@ -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) {
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
Reference in New Issue
Block a user