Docker Build and Push / build-image (push) Failing after 3m51s
需求 142 条 (T-*), 设计见 docs/V0.2/任务模块/ 数据层: - 新增 6 张表: Project / ProjectMember / BoardColumn / Task / TaskMember / TaskTag - 看板列是实体(可增删改排序), Task.columnId 外键 RESTRICT 防误删列丢任务 - Task.status(todo/doing/done) 与 columnId 双落库, 二者正交 领域层 server/task-domain/ (唯一口径, 前后端不重复实现): - resolveStatus: 列决定状态(只看目标列, 无拖出回退); 状态与列不匹配时按列序取第一个绑定该状态的列自动归位, 无匹配列则不移动; 子任务完全豁免 - retention: 逐列 6 档保留窗口(永不显示/当天/3天/本周/本月/永久显示), 以 doneAt 判定 - copyTask: 跨项目深拷贝, 人员按目标项目成员过滤并回报丢弃名单 接口: /api/projects, /api/columns, /api/tasks (均校验项目成员) 前端: - 底部 Tab 插入「任务」, 位于「打卡」之后; 默认首页仍为打卡 - 列表视图(子任务缩进+折叠) / 看板视图(长按拖拽换列、拖到卡片降为子任务) - 列设置(列管理+状态绑定+保留选项) / 任务详情(扩展信息卡片默认收起) / 成员管理 - 视图记忆与上次项目存设备端 已验证: 95 文件语法 0 错误、相对导入 0 缺失、领域逻辑用例全部通过
76 lines
2.5 KiB
React
76 lines
2.5 KiB
React
import { useState } from 'react'
|
|
import TaskRow from '../../components/task/TaskRow.jsx'
|
|
|
|
// 列表视图 (T-VW-01, T-ST-04/05)
|
|
// 按看板列分组展示(列表不分组也允许, 这里默认分组更接近 Todoist)
|
|
// 子任务缩进 + 折叠展开
|
|
export default function ListView({
|
|
tasks, columns, onToggle, onOpen, onAddSub,
|
|
}) {
|
|
const [collapsed, setCollapsed] = useState({})
|
|
|
|
if (!tasks || tasks.length === 0) {
|
|
return (
|
|
<div className="empty" style={{ paddingTop: 90 }}>
|
|
<div className="ico">✓</div>
|
|
<p>这个项目还没有任务</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// 分组: 按列顺序, 未匹配列的任务归到一起
|
|
const groups = []
|
|
const orderedCols = [...(columns || [])].sort((a, b) => a.sortOrder - b.sortOrder)
|
|
for (const c of orderedCols) {
|
|
const list = tasks.filter((t) => t.columnId === c.id)
|
|
if (list.length) groups.push({ key: c.id, name: c.name, list })
|
|
}
|
|
const known = new Set(orderedCols.map((c) => c.id))
|
|
const orphans = tasks.filter((t) => !known.has(t.columnId))
|
|
if (orphans.length) groups.push({ key: 'other', name: '其他', list: orphans })
|
|
|
|
return (
|
|
<div className="list-body">
|
|
{groups.map((g) => (
|
|
<div key={g.key}>
|
|
<div className="group-title">
|
|
{g.name}
|
|
<span className="cnt">{g.list.length}</span>
|
|
</div>
|
|
|
|
{g.list.map((t) => {
|
|
const kids = t.children || []
|
|
const isCollapsed = !!collapsed[t.id]
|
|
return (
|
|
<div key={t.id}>
|
|
<TaskRow
|
|
task={t}
|
|
onToggle={onToggle}
|
|
onClick={() => onOpen(t)}
|
|
onMore={() => onOpen(t)}
|
|
/>
|
|
{kids.length > 0 && (
|
|
<>
|
|
{!isCollapsed && kids.map((c) => (
|
|
<TaskRow key={c.id} task={c} indent onToggle={onToggle} onClick={() => onOpen(c)} />
|
|
))}
|
|
<div
|
|
className="collapse"
|
|
onClick={() => setCollapsed((p) => ({ ...p, [t.id]: !p[t.id] }))}
|
|
>
|
|
{isCollapsed ? `展开 ${kids.length} 个子任务 ⌄` : `收起 ${kids.length} 个子任务 ⌃`}
|
|
</div>
|
|
</>
|
|
)}
|
|
{kids.length === 0 && onAddSub && (
|
|
<div className="sub-add-hint" onClick={() => onAddSub(t)}>+ 子任务</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|