feat(V0.2): 任务模块界面调整 —— 首屏/项目管理/底部安全距离
Docker Build and Push / build-image (push) Successful in 4m8s
Docker Build and Push / build-image (push) Successful in 4m8s
按 2026-10-24 评审的 5 条调整实现 (需求 T-VW-08~10 / T-PJ-11/12 / T-UI-11): 首屏 (T-VW-08/09): - /task 改为「上=最近项目条(3 个+更多入口), 下=当前项目任务管理」 - 完整项目列表迁至 /task/list, 从「更多项目」进入; /task/:pid 保留供深链 最近项目排序 (T-VW-10): - 卡片长按 300ms 拖动换位, 结果存设备端 prefs.recentOrder - 顺序 = 手动排序 > 最近访问(recentIds) > 列表序, 归档项目不展示 项目管理 (T-PJ-11/12): - 新增 ProjectManage: 成员 / 看板列 / 项目信息 三分区统一入口 - 项目信息含 改名 / 归档切换 / 删除(个人项目隐藏) / 退出(仅创建人) - 项目页头部只留「项目管理 + 新增」, 移除散置的成员/列设置按钮 底部安全距离 (T-UI-11): - 任务详情 / 人员选择 / 拷贝弹层 padding-bottom = 安全区 + 20px - 小屏下弹层 maxHeight + 内部滚动, 底部按钮完整可见 已验证: 97 文件语法 0 错误、相对导入 0 缺失
This commit is contained in:
+93
-42
@@ -1,19 +1,26 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { Dialog, DotLoading, Empty, Input, Toast } from 'antd-mobile'
|
||||
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, prunePrefs } from './prefs.js'
|
||||
import {
|
||||
getLastProjectId, setLastProjectId, setRecentIds, prunePrefs,
|
||||
} from './prefs.js'
|
||||
import RecentProjects from '../../components/task/RecentProjects.jsx'
|
||||
import ProjectView from './ProjectView.jsx'
|
||||
|
||||
// 任务模块容器 (T-PM-06)
|
||||
// 无 :pid -> 项目列表
|
||||
// 有 :pid -> 该项目视图(列表/看板)
|
||||
// 任务模块容器 (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 load = useCallback(() => {
|
||||
setErr('')
|
||||
@@ -28,34 +35,91 @@ export default function TaskPage() {
|
||||
|
||||
useEffect(() => { load() }, [load])
|
||||
|
||||
// 记住上次访问的项目 (T-VW-07)
|
||||
// 决定当前项目: 深链 pid > 上次访问 > 个人默认项目 (T-VW-09)
|
||||
useEffect(() => {
|
||||
if (pid) setLastProjectId(Number(pid))
|
||||
}, [pid])
|
||||
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])
|
||||
|
||||
if (pid) {
|
||||
return <ProjectView projectId={Number(pid)} onBack={() => navigate('/task')} />
|
||||
const selectProject = (id) => {
|
||||
setCurrentId(id)
|
||||
setLastProjectId(id)
|
||||
setRecentIds(id)
|
||||
if (pid) navigate('/task', { replace: true })
|
||||
}
|
||||
|
||||
// 项目被删除 / 退出 / 归档后: 重载并回落个人默认项目
|
||||
const handleExited = () => {
|
||||
setCurrentId(null)
|
||||
load()
|
||||
}
|
||||
|
||||
if (isList) {
|
||||
return (
|
||||
<div className="page-with-navbar">
|
||||
<div className="navbar-sub task-projbar">
|
||||
<span className="back" onClick={() => navigate('/task')}>‹</span>
|
||||
<span className="title">我的项目</span>
|
||||
</div>
|
||||
<div className="task-content">
|
||||
<ProjectList
|
||||
projects={projects}
|
||||
err={err}
|
||||
onRetry={load}
|
||||
onOpen={(id) => { selectProject(id); navigate('/task') }}
|
||||
onChanged={load}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 className="task-home">
|
||||
{err && !projects ? (
|
||||
<div className="empty">
|
||||
<div className="ico err">!</div>
|
||||
<p>{err}</p>
|
||||
<div className="mini-btn solid" onClick={load}>重试</div>
|
||||
</div>
|
||||
) : !projects ? (
|
||||
<div className="empty" style={{ padding: '40px 20px' }}>
|
||||
<div className="ico">··</div>
|
||||
<p>加载中…</p>
|
||||
<DotLoading color="#1677ff" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<RecentProjects
|
||||
projects={projects}
|
||||
currentId={currentId}
|
||||
onSelect={selectProject}
|
||||
onMore={() => navigate('/task/list')}
|
||||
/>
|
||||
{currentId != null && (
|
||||
<ProjectView
|
||||
key={currentId}
|
||||
projectId={currentId}
|
||||
onProjectChanged={load}
|
||||
onProjectExited={handleExited}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 完整项目列表 (从「更多项目」进入)
|
||||
function ProjectList({ projects, err, onRetry, onOpen, onChanged }) {
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [name, setName] = useState('')
|
||||
@@ -71,9 +135,7 @@ function ProjectList({ projects, err, onRetry, onOpen, onChanged }) {
|
||||
)
|
||||
}
|
||||
if (!projects) {
|
||||
return (
|
||||
<div className="empty"><div className="ico">··</div><p>加载中…</p><DotLoading color="#1677ff" /></div>
|
||||
)
|
||||
return <div className="empty"><div className="ico">··</div><p>加载中…</p><DotLoading color="#1677ff" /></div>
|
||||
}
|
||||
|
||||
const active = projects.filter((p) => !p.archived)
|
||||
@@ -84,11 +146,12 @@ function ProjectList({ projects, err, onRetry, onOpen, onChanged }) {
|
||||
if (!t) { Toast.show({ content: '请输入项目名称' }); return }
|
||||
setSaving(true)
|
||||
try {
|
||||
await projectApi.create({ name: t })
|
||||
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 {
|
||||
@@ -101,13 +164,7 @@ function ProjectList({ projects, err, onRetry, onOpen, onChanged }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="proj-head">
|
||||
<b>我的项目</b>
|
||||
<div className="right">
|
||||
<div className="mini-btn" onClick={() => setCreating(true)}>+ 新建</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ height: 8 }} />
|
||||
{active.map((p) => (
|
||||
<div className="proj-item" key={p.id} onClick={() => onOpen(p.id)}>
|
||||
<div className="pico" style={{ background: colorOf(p) }}>
|
||||
@@ -150,17 +207,14 @@ function ProjectList({ projects, err, onRetry, onOpen, onChanged }) {
|
||||
</>
|
||||
)}
|
||||
|
||||
<div style={{ height: 24 }} />
|
||||
|
||||
<Dialog
|
||||
visible={creating}
|
||||
title="新建项目"
|
||||
content={
|
||||
<div className="field" style={{ marginTop: 8 }}>
|
||||
<Input
|
||||
placeholder="项目名称"
|
||||
value={name}
|
||||
onChange={setName}
|
||||
maxLength={20}
|
||||
/>
|
||||
<Input placeholder="项目名称" value={name} onChange={setName} maxLength={20} />
|
||||
</div>
|
||||
}
|
||||
actions={[
|
||||
@@ -172,6 +226,3 @@ function ProjectList({ projects, err, onRetry, onOpen, onChanged }) {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// 避免未使用告警
|
||||
void Empty
|
||||
|
||||
Reference in New Issue
Block a user