feat(V0.2): 标题30字+智能拆分 / 时间标记 MM-dd / 时间词前缀 (T-TK-14~16)
Docker Build and Push / build-image (push) Successful in 1m25s

- 标题上限收紧至 30 字 (服务端校验+详情编辑框); 快速添加输入框
  放宽到 200 字: 超长时在 30 字内分隔符处截断为标题, 完整原文进描述
- Task.markDate (DateTime, 迁移 20261025120000): 详情页 DatePicker
  选择/清除, 列表行与看板卡显示 MM-dd 小标签
- 快速添加时间词前缀: 今天/明天/后天/本周/下周/本月/下月 开头
  (后跟分隔符) 自动设为时间标记并移出标题; 本周=本周日, 本月=月末
- 需求/功能/数据库设计文档同步更新 T-TK-14~16 与 Task 表字段
This commit is contained in:
jif2.zhang
2026-10-25 11:47:44 +08:00
parent fd27242e4e
commit 74e1e7dd13
13 changed files with 303 additions and 16 deletions
+19 -3
View File
@@ -70,10 +70,11 @@ 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 = [], kind } = req.body || {}
const { title, description = '', parentId, assigneeId, memberIds = [], tags = [], kind, markDate } = 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 字' })
if (cleanTitle.length > 30) return res.status(400).json({ error: '任务标题不能超过 30 字' })
const mark = parseMarkDate(markDate)
const columns = await loadColumns(project.id)
if (!columns.length) return res.status(400).json({ error: '项目没有可用的看板列' })
@@ -108,6 +109,7 @@ router.post('/projects/:id/tasks', async (req, res, next) => {
title: cleanTitle,
description: String(description || ''),
kind: kind ? String(kind).trim().slice(0, 20) : null,
markDate: mark,
status: firstCol.boundStatus || 'todo',
assigneeId: finalAssignee,
creatorId: userId,
@@ -158,6 +160,14 @@ router.get('/tasks/:id', async (req, res, next) => {
} catch (e) { next(e) }
})
// 时间标记: 接受 YYYY-MM-DD (或完整 ISO), 存 DateTime; 非法输入 400 由调用方处理
function parseMarkDate(v) {
if (v === null || v === undefined || v === '') return null
const d = new Date(v)
if (Number.isNaN(d.getTime())) return undefined // 非法
return d
}
// PUT /api/tasks/:id 关键: 列/状态联动裁决
router.put('/tasks/:id', async (req, res, next) => {
try {
@@ -174,7 +184,7 @@ router.put('/tasks/:id', async (req, res, next) => {
if (body.title !== undefined) {
const t = String(body.title).trim()
if (!t) return res.status(400).json({ error: '任务标题不能为空' })
if (t.length > 200) return res.status(400).json({ error: '任务标题不能超过 200 字' })
if (t.length > 30) return res.status(400).json({ error: '任务标题不能超过 30 字' })
data.title = t
}
if (body.description !== undefined) data.description = String(body.description)
@@ -183,6 +193,12 @@ router.put('/tasks/:id', async (req, res, next) => {
const k = String(body.kind || '').trim().slice(0, 20)
data.kind = k || null
}
// 时间标记: 完整日期存储
if (body.markDate !== undefined) {
const mark = parseMarkDate(body.markDate)
if (mark === undefined) return res.status(400).json({ error: '时间标记格式非法' })
data.markDate = mark
}
if (body.extras !== undefined) {
if (!Array.isArray(body.extras)) return res.status(400).json({ error: '扩展信息格式非法' })
data.extras = body.extras.map((e) => ({