feat(任务模块): 任务归档 T-ARC-01~08 实现
Docker Build and Push / build-image (push) Successful in 1m27s

- 数据: Project.autoArchive + Task.archived/archivedAt/archiveSource,
  迁移 20261026000000_task_archive (两索引)
- 领域: server/task-domain/archive.js —— hiddenAtOf 与保留口径精确同源,
  autoArchiveDue 到期判定, runAutoArchiveForProject (读时惰性) +
  runAutoArchiveSweep (每小时 cron 兜底)
- 接口: POST /tasks/:id/archive|unarchive (连带子任务, 幂等);
  GET /projects/:id/tasks/archived (keyword/kind/columnId/from/to 过滤,
  archivedAt+id 游标分页); 项目 PUT 收 autoArchive (枚举校验);
  主列表/子任务/项目 taskCount 全部排除归档
- 前端: ⋯ 菜单「归档」(确认含子任务条数); 项目栏「已归档」入口 →
  ArchivedTasks 弹层 (关键词/类型/列/归档时段过滤 + 加载更多 + 恢复/删除);
  项目管理「归档设置」四档 Picker
This commit is contained in:
jif2.zhang
2026-10-25 23:09:52 +08:00
parent 39282b9633
commit 368e99a380
10 changed files with 606 additions and 99 deletions
+12 -3
View File
@@ -41,6 +41,9 @@ export async function ensurePersonalProject(userId) {
// 任务类型标签: 项目级自定义, 未配置时用默认四类
export const DEFAULT_TASK_KINDS = ['需求', 'Bug', 'FEATURE', '优化']
// 自动归档档位 (T-ARC-02)
export const AUTO_ARCHIVE_OPTIONS = ['3d', '1w', '1m', 'never']
export function normTaskKinds(v) {
if (!Array.isArray(v)) return null
const out = []
@@ -63,6 +66,7 @@ function projectSummary(p) {
updatedAt: p.updatedAt,
taskKinds: normTaskKinds(p.taskKinds) || DEFAULT_TASK_KINDS,
icon: p.icon || null,
autoArchive: AUTO_ARCHIVE_OPTIONS.includes(p.autoArchive) ? p.autoArchive : 'never',
memberCount: p._count?.members ?? undefined,
taskCount: p._count?.tasks ?? undefined,
}
@@ -77,7 +81,7 @@ router.get('/', async (req, res, next) => {
await ensurePersonalProject(userId)
const rows = await prisma.project.findMany({
where: { members: { some: { userId } } },
include: { _count: { select: { members: true, tasks: true } } },
include: { _count: { select: { members: true, tasks: { where: { archived: false } } } } },
orderBy: [{ archived: 'asc' }, { updatedAt: 'desc' }],
})
res.json({ items: rows.map(projectSummary) })
@@ -98,7 +102,7 @@ router.post('/', async (req, res, next) => {
await createDefaultColumns(project.id)
const full = await prisma.project.findUnique({
where: { id: project.id },
include: { _count: { select: { members: true, tasks: true } } },
include: { _count: { select: { members: true, tasks: { where: { archived: false } } } } },
})
res.json({ item: projectSummary(full) })
} catch (e) { next(e) }
@@ -135,7 +139,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, taskKinds, icon } = req.body || {}
const { name, archived, notes, taskKinds, icon, autoArchive } = req.body || {}
const data = {}
if (name !== undefined) {
const t = String(name).trim()
@@ -159,6 +163,11 @@ router.put('/:id', async (req, res, next) => {
} else if (icon === null) {
data.icon = null
}
// 自动归档档位 (T-ARC-02)
if (autoArchive !== undefined) {
if (!AUTO_ARCHIVE_OPTIONS.includes(autoArchive)) return res.status(400).json({ error: '自动归档档位非法' })
data.autoArchive = autoArchive
}
if (archived !== undefined) data.archived = !!archived
// 项目信息块 (T-PJ-13): 任意多个 { title, content }, 整体替换
if (notes !== undefined) {