import { useCallback, useEffect, useMemo, useState } from 'react' import { DotLoading, Toast } from 'antd-mobile' import { projectApi, taskApi } from '../../api/task.js' import { filterVisible, msToNextBoundary } from './date.js' import { getViewMode, setViewMode } from './prefs.js' import ListView from './ListView.jsx' import BoardView from './BoardView.jsx' import ColumnSettings from './ColumnSettings.jsx' import TaskDetail from './TaskDetail.jsx' import ProjectMembers from './ProjectMembers.jsx' // 项目视图容器: 列表 / 看板 切换 + 数据装载 + 乐观更新 export default function ProjectView({ projectId, onBack }) { const [project, setProject] = useState(null) const [tasks, setTasks] = useState(null) const [columns, setColumns] = useState([]) const [err, setErr] = useState('') const [mode, setMode] = useState(() => getViewMode(projectId) || 'list') const [openTask, setOpenTask] = useState(null) const [showColumns, setShowColumns] = useState(false) const [showMembers, setShowMembers] = useState(false) const [adding, setAdding] = useState(false) const [tick, setTick] = useState(0) // 保留窗口重算触发 const load = useCallback(() => { setErr('') Promise.all([projectApi.detail(projectId), taskApi.list(projectId)]) .then(([p, t]) => { setProject(p.item) setColumns(p.item.columns || []) setTasks(t.items || []) }) .catch((e) => setErr(e.message)) }, [projectId]) useEffect(() => { setTasks(null); load() }, [load]) // 切换视图并记忆 (T-VW-02/03/06) const switchMode = (next) => { setMode(next) setViewMode(projectId, next) } // 保留窗口到点重算 (T-EX-18) useEffect(() => { const options = (columns || []).map((c) => c.retention) const near = options .map((o) => msToNextBoundary(o)) .filter((v) => v != null) .sort((a, b) => a - b)[0] const t = setTimeout(() => setTick((v) => v + 1), near ? Math.min(near, 60_000) : 60_000) return () => clearTimeout(t) }, [columns, tick]) const visibleTasks = useMemo( () => filterVisible(tasks || [], columns), // eslint-disable-next-line react-hooks/exhaustive-deps [tasks, columns, tick], ) // ---- 乐观更新 ---- const patchTask = (id, patch) => setTasks((prev) => (prev || []).map((t) => { if (t.id === id) return { ...t, ...patch } const kids = (t.children || []).map((c) => (c.id === id ? { ...c, ...patch } : c)) return { ...t, children: kids } })) const replaceTask = (fresh) => setTasks((prev) => (prev || []).map((t) => (t.id === fresh.id ? fresh : t))) /** 更新任务(列/状态/字段), 服务端裁决后以返回值为准 */ async function updateTask(id, patch) { const snapshot = tasks patchTask(id, patch) try { const r = await taskApi.update(id, patch) replaceTask(r.item) return r.item } catch (e) { setTasks(snapshot) Toast.show({ content: e.message }) throw e } } /** 勾选完成 / 取消完成 */ const toggleDone = (task) => updateTask(task.id, { status: task.status === 'done' ? 'todo' : 'done', }) /** 拖拽换列 (T-BD-04/07) */ const moveToColumn = async (task, columnId) => { const col = columns.find((c) => c.id === columnId) try { await updateTask(task.id, { columnId }) const target = columns.find((c) => c.id === columnId) if (target?.boundStatus && target.boundStatus !== task.status) { Toast.show({ content: `已移入 ${col?.name},状态改为「${target.boundStatus === 'done' ? '完成' : target.boundStatus === 'doing' ? '执行中' : '未开始'}」` }) } else { Toast.show({ content: `已移入 ${col?.name}` }) } } catch { /* 已回滚并提示 */ } } /** 改状态 (T-BD-14~16) */ const changeStatus = async (task, status) => { try { const fresh = await updateTask(task.id, { status }) const moved = fresh && fresh.columnId !== task.columnId if (moved) { const col = columns.find((c) => c.id === fresh.columnId) Toast.show({ content: `已移至 ${col?.name || '目标列'}` }) } } catch { /* ignore */ } } /** 拖拽降为子任务 (T-ST-12) */ const makeSubtask = async (task, parentId) => { try { await updateTask(task.id, { parentId }) // 成为子任务后不再作为顶层卡片出现 setTasks((prev) => (prev || []).filter((t) => t.id !== task.id)) Toast.show({ content: '已转为子任务' }) load() } catch { /* 错误已提示 */ } } /** 新建任务 */ async function createTask(title, parentId = null) { const t = String(title || '').trim() if (!t) return try { await taskApi.create(projectId, { title: t, parentId }) setAdding(false) load() } catch (e) { Toast.show({ content: e.message }) } } if (err) { return (
‹ 任务
!

{err}

重试
) } if (!project || !tasks) { return (
‹ 任务
··

加载中…

) } return (
‹ {project.name}
setShowMembers(true)}>成员 setAdding((v) => !v)}>+ 新增
switchMode('list')}>列表
switchMode('board')}>看板
setShowColumns(true)}>列设置
{adding && (
{ if (e.key === 'Enter') createTask(e.currentTarget.value) if (e.key === 'Escape') setAdding(false) }} />
)} {mode === 'list' ? ( setOpenTask(t)} onAddSub={(parent) => setOpenTask(parent)} /> ) : ( setOpenTask(t)} onToggleSub={(c) => toggleDone(c)} onToggle={(t) => toggleDone(t)} onQuickAdd={() => { setAdding(true) }} /> )}
{openTask && ( setOpenTask(null)} onSaved={() => { setOpenTask(null); load() }} onChanged={load} /> )} {showColumns && ( setShowColumns(false)} onSaved={() => { setShowColumns(false); load() }} /> )} {showMembers && ( setShowMembers(false)} onChanged={load} /> )}
) }