import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { Dialog, DotLoading, Input, Toast } from 'antd-mobile'
import PageHeader from '../../components/PageHeader.jsx'
import { projectApi } from '../../api/task.js'
import {
getLastProjectId, setLastProjectId, setRecentIds, prunePrefs,
getRecentIds, getRecentOrder, setRecentOrder,
} from './prefs.js'
import RecentProjects from '../../components/task/RecentProjects.jsx'
import ProjectView from './ProjectView.jsx'
// 任务模块容器 (T-PM-06, T-VW-08/09)
// /task 首屏: 上 = 最近项目条(3 个 + 更多入口), 下 = 当前项目的任务管理
// /task/list 完整项目列表(从「更多项目」进入; 支持拖动排序 + 新建)
// /task/:pid 打开指定项目(深链), 布局同首屏
export default function TaskPage() {
const { pid } = useParams()
const { pathname } = useLocation()
const navigate = useNavigate()
const isList = pathname === '/task/list'
const [projects, setProjects] = useState(null)
const [err, setErr] = useState('')
const [currentId, setCurrentId] = useState(null)
const [createSignal, setCreateSignal] = useState(0) // 通知项目列表弹起新建对话框
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])
// 决定当前项目: 深链 pid > 上次访问 > 个人默认项目 (T-VW-09)
useEffect(() => {
if (!projects) return
const ok = (x) => x && projects.some((p) => p.id === x && !p.archived)
let id = pid ? Number(pid) : getLastProjectId()
if (!ok(id)) id = projects.find((p) => p.isPersonal)?.id ?? projects[0]?.id ?? null
if (!ok(id)) id = null
setCurrentId(id)
if (id) { setLastProjectId(id); setRecentIds(id) }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projects, pid])
const selectProject = (id) => {
setCurrentId(id)
setLastProjectId(id)
setRecentIds(id)
if (pid) navigate('/task', { replace: true })
}
// 项目被删除 / 退出 / 归档后: 重载并回落个人默认项目
const handleExited = () => {
setCurrentId(null)
load()
}
if (isList) {
return (
navigate('/task')}>‹
我的项目
setCreateSignal((v) => v + 1)}>+ 新建
{ selectProject(id); navigate('/task') }}
onChanged={load}
createSignal={createSignal}
onCreateConsumed={() => setCreateSignal(0)}
/>
)
}
return (
{err && !projects ? (
) : !projects ? (
) : (
<>
navigate('/task/list')}
/>
{currentId != null && (
)}
>
)}
)
}
const COLORS = ['#1677ff', '#00b578', '#ff8f1f', '#a855f7', '#14b8a6', '#f43f9e']
const colorOf = (p) => COLORS[Math.abs(p.id) % COLORS.length]
const LONG_PRESS = 300
const MOVE_TOLERANCE = 8
// 完整项目列表 (从「更多项目」进入)
// 顺序: 手动排序(recentOrder, 拖动结果)优先 > 最近访问 > 列表序; 已归档固定在底部
function ProjectList({ projects, err, onRetry, onOpen, onChanged, createSignal, onCreateConsumed }) {
const [creating, setCreating] = useState(false)
const [name, setName] = useState('')
const [saving, setSaving] = useState(false)
const [ver, setVer] = useState(0) // 拖动排序后强制重算
const [dragId, setDragId] = useState(null) // 拖拽中的项目 id (视觉态)
const listRef = useRef(null)
const timer = useRef(null)
const startRef = useRef(null)
const activeRef = useRef(false)
const dragIdRef = useRef(null)
// 头部「+ 新建」→ 弹起新建对话框 (一次性: 消费后清零, 否则下次进入该页会再次弹出)
useEffect(() => {
if (createSignal) {
setCreating(true)
onCreateConsumed?.()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [createSignal])
const active = useMemo(() => {
const manual = getRecentOrder()
const score = new Map(manual.map((id, i) => [id, i]))
let n = score.size
getRecentIds().forEach((id) => { if (!score.has(id)) score.set(id, n++) })
const fb = n + 1000
return projects
.filter((p) => !p.archived)
.slice()
.sort((a, b) => (score.get(a.id) ?? fb) - (score.get(b.id) ?? fb))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [projects, ver])
if (err) {
return (
)
}
if (!projects) {
return
}
const archived = projects.filter((p) => p.archived)
async function create() {
const t = name.trim()
if (!t) { Toast.show({ content: '请输入项目名称' }); return }
setSaving(true)
try {
const r = await projectApi.create({ name: t })
setName('')
setCreating(false)
Toast.show({ content: '已创建' })
onChanged?.()
onOpen?.(r.item.id)
} catch (e) {
Toast.show({ content: e.message })
} finally {
setSaving(false)
}
}
// ---- 长按拖动排序 (纵向): 复用首屏卡片同一套 prefs.recentOrder ----
function cleanup() {
if (timer.current) { clearTimeout(timer.current); timer.current = null }
activeRef.current = false
startRef.current = null
dragIdRef.current = null
document.removeEventListener('touchmove', onMove, { passive: false })
document.removeEventListener('touchend', onEnd)
document.removeEventListener('mousemove', onMove)
document.removeEventListener('mouseup', onEnd)
}
const pointOf = (ev) => {
const t = ev.touches?.[0] || ev.changedTouches?.[0] || ev
return { x: t.clientX, y: t.clientY }
}
function indexAt(y) {
const items = Array.from(listRef.current?.querySelectorAll('[data-pid]') || [])
for (let i = 0; i < items.length; i++) {
const r = items[i].getBoundingClientRect()
if (y < r.top + r.height / 2) return i
}
return Math.max(0, items.length - 1)
}
function onMove(ev) {
const p = pointOf(ev)
if (!activeRef.current) {
if (startRef.current && (Math.abs(p.x - startRef.current.x) > MOVE_TOLERANCE || Math.abs(p.y - startRef.current.y) > MOVE_TOLERANCE)) {
if (timer.current) { clearTimeout(timer.current); timer.current = null }
}
return
}
if (ev.cancelable) ev.preventDefault()
const ids = active.map((x) => x.id)
const from = ids.indexOf(dragIdRef.current)
if (from < 0) return
const to = indexAt(p.y)
if (to !== from) {
const [moved] = ids.splice(from, 1)
ids.splice(to, 0, moved)
setRecentOrder(ids)
setVer((v) => v + 1)
}
}
function onEnd() {
cleanup()
setDragId(null)
}
function beginPress(p, ev) {
if (ev.button && ev.button !== 0) return
const pt = pointOf(ev)
startRef.current = pt
activeRef.current = false
cleanup()
dragIdRef.current = p.id
timer.current = setTimeout(() => {
activeRef.current = true
setDragId(p.id)
if (navigator.vibrate) navigator.vibrate(10)
}, LONG_PRESS)
document.addEventListener('touchmove', onMove, { passive: false })
document.addEventListener('touchend', onEnd)
document.addEventListener('mousemove', onMove)
document.addEventListener('mouseup', onEnd)
}
useEffect(() => () => cleanup(), [])
return (
长按项目可拖动排序(本机记忆,与首屏最近项目一致)
{active.map((p) => (
beginPress(p, ev)}
onMouseDown={(ev) => beginPress(p, ev)}
onClick={() => { if (!activeRef.current && !dragId) onOpen(p.id) }}
>
{(p.name || '?').slice(0, 1)}
{p.name}
{p.isPersonal && 个人}
{p.taskCount ?? 0} 个任务 · {p.memberCount ?? 1} 人
⠿
))}
{active.length === 0 && (
▤
还没有协作项目
setCreating(true)}>新建项目
)}
{archived.length > 0 && (
<>
已归档({archived.length})
{archived.map((p) => (
onOpen(p.id)}>
{(p.name || '?').slice(0, 1)}
{p.name}
已归档 · {p.taskCount ?? 0} 个任务
›
))}
>
)}
}
actions={[
{ key: 'cancel', text: '取消', onClick: () => setCreating(false) },
{ key: 'ok', text: saving ? '创建中…' : '创建', bold: true, onClick: create },
]}
onClose={() => setCreating(false)}
/>
)
}