feat(V0.2): 任务模块 —— 项目 / 列表与看板 / 列-状态绑定 / 一级子任务
Docker Build and Push / build-image (push) Failing after 3m51s
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 缺失、领域逻辑用例全部通过
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { Dialog, DotLoading, Empty, Input, Toast } from 'antd-mobile'
|
||||
import PageHeader from '../../components/PageHeader.jsx'
|
||||
import { projectApi } from '../../api/task.js'
|
||||
import { getLastProjectId, setLastProjectId, prunePrefs } from './prefs.js'
|
||||
import ProjectView from './ProjectView.jsx'
|
||||
|
||||
// 任务模块容器 (T-PM-06)
|
||||
// 无 :pid -> 项目列表
|
||||
// 有 :pid -> 该项目视图(列表/看板)
|
||||
export default function TaskPage() {
|
||||
const { pid } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const [projects, setProjects] = useState(null)
|
||||
const [err, setErr] = useState('')
|
||||
|
||||
const load = useCallback(() => {
|
||||
setErr('')
|
||||
projectApi.list()
|
||||
.then((d) => {
|
||||
const items = d.items || []
|
||||
setProjects(items)
|
||||
prunePrefs(items.map((p) => p.id))
|
||||
})
|
||||
.catch((e) => setErr(e.message))
|
||||
}, [])
|
||||
|
||||
useEffect(() => { load() }, [load])
|
||||
|
||||
// 记住上次访问的项目 (T-VW-07)
|
||||
useEffect(() => {
|
||||
if (pid) setLastProjectId(Number(pid))
|
||||
}, [pid])
|
||||
|
||||
if (pid) {
|
||||
return <ProjectView projectId={Number(pid)} onBack={() => navigate('/task')} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page-with-navbar">
|
||||
<PageHeader title="任务" />
|
||||
<div className="task-content">
|
||||
<ProjectList
|
||||
projects={projects}
|
||||
err={err}
|
||||
onRetry={load}
|
||||
onOpen={(id) => {
|
||||
setLastProjectId(id)
|
||||
navigate(`/task/${id}`)
|
||||
}}
|
||||
onChanged={load}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ProjectList({ projects, err, onRetry, onOpen, onChanged }) {
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [name, setName] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
if (err) {
|
||||
return (
|
||||
<div className="empty">
|
||||
<div className="ico err">!</div>
|
||||
<p>{err}</p>
|
||||
<div className="mini-btn solid" onClick={onRetry}>重试</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (!projects) {
|
||||
return (
|
||||
<div className="empty"><div className="ico">··</div><p>加载中…</p><DotLoading color="#1677ff" /></div>
|
||||
)
|
||||
}
|
||||
|
||||
const active = projects.filter((p) => !p.archived)
|
||||
const archived = projects.filter((p) => p.archived)
|
||||
|
||||
async function create() {
|
||||
const t = name.trim()
|
||||
if (!t) { Toast.show({ content: '请输入项目名称' }); return }
|
||||
setSaving(true)
|
||||
try {
|
||||
await projectApi.create({ name: t })
|
||||
setName('')
|
||||
setCreating(false)
|
||||
Toast.show({ content: '已创建' })
|
||||
onChanged?.()
|
||||
} catch (e) {
|
||||
Toast.show({ content: e.message })
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const COLORS = ['#1677ff', '#00b578', '#ff8f1f', '#a855f7', '#14b8a6', '#f43f9e']
|
||||
const colorOf = (p) => COLORS[Math.abs(p.id) % COLORS.length]
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="proj-head">
|
||||
<b>我的项目</b>
|
||||
<div className="right">
|
||||
<div className="mini-btn" onClick={() => setCreating(true)}>+ 新建</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{active.map((p) => (
|
||||
<div className="proj-item" key={p.id} onClick={() => onOpen(p.id)}>
|
||||
<div className="pico" style={{ background: colorOf(p) }}>
|
||||
{(p.name || '?').slice(0, 1)}
|
||||
</div>
|
||||
<div className="pmain">
|
||||
<div className="ptitle">
|
||||
{p.name}
|
||||
{p.isPersonal && <span className="badge">个人</span>}
|
||||
</div>
|
||||
<div className="psub">
|
||||
{p.taskCount ?? 0} 个任务 · {p.memberCount ?? 1} 人
|
||||
</div>
|
||||
</div>
|
||||
<div className="pcount">›</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{active.length === 0 && (
|
||||
<div className="empty" style={{ paddingTop: 90 }}>
|
||||
<div className="ico">▤</div>
|
||||
<p>还没有协作项目</p>
|
||||
<div className="mini-btn solid" onClick={() => setCreating(true)}>新建项目</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{archived.length > 0 && (
|
||||
<>
|
||||
<div className="proj-group">已归档({archived.length})</div>
|
||||
{archived.map((p) => (
|
||||
<div className="proj-item" key={p.id} style={{ opacity: 0.65 }} onClick={() => onOpen(p.id)}>
|
||||
<div className="pico" style={{ background: '#a8b0ba' }}>{(p.name || '?').slice(0, 1)}</div>
|
||||
<div className="pmain">
|
||||
<div className="ptitle">{p.name}</div>
|
||||
<div className="psub">已归档 · {p.taskCount ?? 0} 个任务</div>
|
||||
</div>
|
||||
<div className="pcount">›</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Dialog
|
||||
visible={creating}
|
||||
title="新建项目"
|
||||
content={
|
||||
<div className="field" style={{ marginTop: 8 }}>
|
||||
<Input
|
||||
placeholder="项目名称"
|
||||
value={name}
|
||||
onChange={setName}
|
||||
maxLength={20}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
actions={[
|
||||
{ key: 'cancel', text: '取消', onClick: () => setCreating(false) },
|
||||
{ key: 'ok', text: saving ? '创建中…' : '创建', bold: true, onClick: create },
|
||||
]}
|
||||
onClose={() => setCreating(false)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// 避免未使用告警
|
||||
void Empty
|
||||
Reference in New Issue
Block a user