@@ -1249,14 +1400,15 @@
9 需求点对照 界面稿 ↔ 需求编号
| 界面 | 对应需求点 |
- | 2 项目列表 | T-PJ-01~10、T-PM-06、T-PM-07、T-UI-08 |
- | 3 列表视图 | T-VW-01~07、T-ST-04/05/09、T-AS-01/02、T-FL-11、T-TK-12 |
+ | 2 任务首屏 | T-VW-08/09/10、T-VW-03~07、T-PM-06/07、T-PJ-11(头部入口) |
+ | 2′ 我的项目列表 | T-PJ-01~10、T-UI-08 |
+ | 3 列表视图 | T-VW-01/02、T-ST-04/05/09、T-AS-01/02、T-FL-11、T-TK-12 |
| 3′ 保留退场 | T-FL-05/09、T-EX-18 |
| 4 看板视图 | T-BD-01~16、T-UI-02/07、T-ST-06/07/09、T-ST-12~15 |
| 5 列设置 | T-COL-01~10、T-BD-02/09/10、T-FL-01~15 |
| 5′ 自动归位 | T-BD-14/16、T-EX-04/05 |
- | 6 任务详情 | T-TK-01~13、T-ST-03、T-AS-05、T-EX-07 |
- | 7 成员 / 拷贝 | T-AS-03/04/06/07、T-PM-01~05、T-CP-01~09、T-EX-06/08 |
+ | 6 任务详情 | T-TK-01~13、T-ST-03、T-AS-05、T-EX-07、T-UI-11(底部安全距离) |
+ | 7 项目管理 / 成员 / 拷贝 | T-PJ-11/12、T-AS-03/04/06/07、T-PM-01~05、T-CP-01~09、T-EX-06/08 |
| 8 三态 | T-UI-08/09/10、T-EX-02/10 |
diff --git a/docs/V0.2/任务模块/任务模块设计文档-框架设计.md b/docs/V0.2/任务模块/任务模块设计文档-框架设计.md
index 909ef08..0121204 100644
--- a/docs/V0.2/任务模块/任务模块设计文档-框架设计.md
+++ b/docs/V0.2/任务模块/任务模块设计文档-框架设计.md
@@ -36,15 +36,16 @@
```
┌──────────────────── 前端 (React + antd-mobile,打包进 Tauri / OTA) ────────────────────┐
│ App.jsx 路由 │
-│ └ /task ──► TaskPage ──┬── ProjectList (项目列表 / 个人默认项目) │
-│ └── /task/:pid ──► ProjectView │
-│ ├── ListView (列表 + 子任务缩进折叠) │
-│ ├── BoardView (看板 + 长按拖拽) │
-│ └── ColumnSettings / ProjectSettings │
-│ 组件: BoardColumn / TaskCard / SubtaskRow / StatusDot / ExtrasCard / UserPicker │
-│ 状态: TaskStore (项目内单例) / ReorderableList / useLongPressDrag │
-│ 工具: src/pages/task/date.js (与后端同口径的自然日边界) │
-│ src/pages/task/prefs.js (设备端视图记忆 / 上次项目) │
+│ └ /task ──► TaskPage (首屏: 上=RecentProjects 条, 下=当前项目 ProjectView) │
+│ /task/list ──► ProjectList (完整项目列表, 从「更多项目」进入) │
+│ /task/:pid ──► TaskPage (打开指定项目, 供深链) │
+│ ProjectView ──┬── ListView (列表 + 子任务缩进折叠) │
+│ ├── BoardView (看板 + 长按拖拽) │
+│ └── ProjectManage (项目管理: 成员 / 列设置 / 项目信息) │
+│ 组件: TaskCard / SubtaskRow / StatusDot / ExtrasCard / UserPicker / RecentProjects │
+│ 状态: TaskStore (项目内单例) / ReorderableList / useLongPressDrag │
+│ 工具: src/pages/task/date.js (与后端同口径的自然日边界) │
+│ src/pages/task/prefs.js (设备端: 视图记忆 / 上次项目 / 最近项目手动排序) │
└──────────────────────────────┬─────────────────────────────────────────────────────────┘
│ HTTPS + JWT (Authorization: Bearer)
┌──────────────────────────────▼─────────────────────────────────────────────────────────┐
@@ -98,18 +99,20 @@ src/
api/task.js # projectApi / columnApi / taskApi
pages/task/
date.js # 与后端同口径
- prefs.js # 设备端偏好(localStorage)
- TaskPage.jsx # 模块容器 + 路由
- ProjectList.jsx # 项目列表
+ prefs.js # 设备端偏好(视图记忆/上次项目/最近项目手动排序)
+ TaskPage.jsx # 首屏容器: RecentProjects 条 + 当前项目 ProjectView
+ ProjectList.jsx # 完整项目列表(/task/list, 从「更多项目」进入)
ProjectView.jsx # 列表/看板容器
ListView.jsx # 列表视图
BoardView.jsx # 看板视图
- ColumnSettings.jsx # 列管理 + 绑定 + 保留选项
- TaskDetail.jsx # 任务详情(含扩展卡片 / 子任务)
- ProjectMembers.jsx # 成员管理
+ ColumnSettings.jsx # 列管理 + 绑定 + 保留选项(由项目管理进入)
+ TaskDetail.jsx # 任务详情(含扩展卡片 / 子任务;底部安全距离)
+ ProjectMembers.jsx # 成员管理(由项目管理进入)
+ ProjectManage.jsx # 项目管理: 成员 / 列设置 / 项目信息 三区
components/task/
TaskCard.jsx SubtaskRow.jsx StatusDot.jsx
ExtrasCard.jsx UserPicker.jsx useLongPressDrag.js
+ RecentProjects.jsx # 首屏最近项目条(卡片可长按排序)
components/BottomNav.jsx # Tab 增「任务」,位于打卡之后
components/TabIcon.jsx # 增 task 图标
App.jsx # +/task、/task/:pid
diff --git a/docs/V0.2/任务模块/任务模块需求文档.md b/docs/V0.2/任务模块/任务模块需求文档.md
index c897ebd..1431084 100644
--- a/docs/V0.2/任务模块/任务模块需求文档.md
+++ b/docs/V0.2/任务模块/任务模块需求文档.md
@@ -37,7 +37,7 @@
### 1.3 完整功能
-1. **视图体系**:列表视图、看板视图,可随时互相切换,同源数据、实时双向同步;每个项目独立记忆上次使用的视图。列表只是不按列分组,**任务归属的看板列与看板视图完全一致**。
+1. **视图体系**:列表视图、看板视图,可随时互相切换,同源数据、实时双向同步;每个项目独立记忆上次使用的视图。列表只是不按列分组,**任务归属的看板列与看板视图完全一致**。**首屏**上部为「最近项目」(3 个卡片 + 更多入口,卡片可手动排序),下部直接展示**当前项目**的任务管理视图,不必先进项目列表。
2. **看板与任务状态的绑定配置**:
- 看板列**默认 4 列** `Plan → Doing → Action → Done`;列**可增删改、可调整顺序**,数量不固定。
- **每一列独立**选择是否绑定任务状态,并可选择绑定到 `未开始 / 执行中 / 完成` 中的任一个;**允许多列绑定同一状态**。
@@ -50,7 +50,7 @@
4. **任务字段体系**:基础字段(标题必填,负责人默认本人)+ 内置可折叠**扩展信息卡片**(默认收起、全非必填);负责人(单人)与任务成员(多人)均为信息标记。
5. **负责人 & 成员展示规则**:负责人大字号突出、成员小字号弱化并置于下方;二者只能从**当前项目成员**中选择;不改变项目统一读写权限。
6. **子任务体系**:仅一级、必须同项目、父子完成状态互不联动;列表视图支持缩进与折叠展开;**看板卡片内以简洁形式嵌套展示子任务并可直接勾选**,完整详情在卡片弹窗内编辑;支持**长按拖拽把任务降为另一个任务的一级子任务**。
-7. **项目体系**:个人默认项目(不可删、不可加成员、名称以用户名开头且可改名);普通协作项目(可增删成员);创建人可主动退出;成员清空后静默保留,交后台维护。
+7. **项目体系**:个人默认项目(不可删、不可加成员、名称以用户名开头且可改名);普通协作项目(可增删成员);创建人可主动退出;成员清空后静默保留,交后台维护。成员管理、列设置与项目信息(改名 / 归档 / 删除 / 退出)统一收进「**项目管理**」入口。
8. **项目权限**:以项目为隔离单位,只能看到自己是成员的项目;项目内成员权限均等、全读写;预留权限分级入口但不实现细分角色;任意成员均可归档项目。
9. **跨项目拷贝**:是**拷贝**不是移动;完整复制全部字段、扩展信息、子任务、负责人、成员、状态、看板列、标签;新任务创建人为执行拷贝者;拷贝后新旧任务完全独立。
10. **UI 交互规范**:参考 Todoist 的列表交互与子任务折叠风格;看板卡片轻量化;列表与看板同源、切换无刷新、数据一致;具备空状态 / 加载状态 / 数据异常状态。
@@ -82,8 +82,11 @@
**场景 G · 看板卡片内的子任务**
看板中某任务卡片下方以简洁样式嵌套显示其子任务 → 直接在卡片内勾选子任务完成,**父任务状态不受影响**,子任务状态也不受该列绑定规则约束;每个嵌套子任务带**自身状态小标记**,便于识别与父任务不一致的状态;需要编辑子任务标题或负责人时,点开卡片弹窗操作。
-**场景 H · 从底部 Tab 进入**
-底部导航顺序为「打卡 → **任务** → 宏观 → 监控 → 我」→ 点击「任务」即进入任务模块,落在上次访问的项目(记录在设备端;无记录则回到个人默认项目)。
+**场景 H · 从底部 Tab 进入(首屏)**
+底部导航顺序为「打卡 → **任务** → 宏观 → 监控 → 我」→ 点击「任务」进入首屏:**上部**是最近使用的 3 个项目卡片 + 「更多项目」入口(点卡片即切换当前项目,长按卡片可调整顺序);**下部**直接是当前项目(上次访问的,记录在设备端;无记录则回到个人默认项目)的任务管理页,无需先进项目列表。点「更多项目」才进入完整的项目列表页。
+
+**场景 I · 项目管理**
+在项目页头部点「项目管理」→ 弹出统一管理页,内含三个分区:**成员**(查看 / 添加 / 移除)、**列设置**(列管理 / 状态绑定 / 保留选项)、**项目信息**(改名 / 归档 / 删除 / 退出,个人默认项目不出现删除与退出)。
### 1.5 不在本期范围(明确排除)
@@ -156,6 +159,9 @@
| T-VW-05 | 从未选择过视图的新项目,**默认使用列表视图** | 新建项目首次进入为列表视图 | P1 |
| T-VW-06 | 视图记忆保存在**设备端**,不要求跨设备同步;本机跨会话保持 | 杀进程重进某项目,视图仍为上次选择;换设备登录不要求一致 | P0 |
| T-VW-07 | **上次访问的项目**记录在**设备端**;无记录时**一律回到个人默认项目** | 杀进程重进落在上次项目;首次使用或记录失效则进入个人默认项目 | P0 |
+| T-VW-08 | 任务**首屏上部**展示**最近使用的 3 个项目**卡片,末尾放「更多项目」入口;点击「更多项目」进入完整项目列表页 | 最近项目按访问时间倒序取前 3;点卡片切换当前项目并高亮;点「更多」进入项目列表 | P0 |
+| T-VW-09 | 任务**首屏下部**直接展示**当前选中项目**的任务管理视图(列表 / 看板,随该项目视图记忆);当前项目默认为上次访问的项目,无记录时为个人默认项目 | 进入任务 Tab 即见当前项目的任务,**无需先进项目列表**;切换首屏项目卡片后下部随之切换 | P0 |
+| T-VW-10 | 首屏**最近项目卡片支持手动排序**(长按拖动调整顺序),排序结果存**设备端**;未手动排序时按最近使用时间自动排列 | 拖动后顺序持久化,杀进程重进保持;新近访问但未手动排序的项目不出现在固定卡位之外 | P1 |
### 3.2 看板列管理(T-COL)
@@ -281,6 +287,8 @@
| T-PJ-08 | 项目**成员清空后静默保留**,交后台后续维护,前端不做特殊处理 | 成员全部退出后,前端不弹特殊提示、不出现错误;不可见 | P0 |
| T-PJ-09 | 项目可**归档**,归档项目与进行中项目区分展示 | 归档后从进行中列表移出,可在归档区找回 | P1 |
| T-PJ-10 | 项目列表按用户可见范围返回,仅含自己为成员的项目 | 非成员项目不出现在列表中 | P0 |
+| T-PJ-11 | 项目的**成员管理、成员新增、列设置**统一收进「**项目管理**」入口;项目页头部不再散置多个功能入口 | 项目管理内含 成员 / 列设置 / 项目信息 三个分区;头部仅保留「项目管理」一个入口 | P0 |
+| T-PJ-12 | 项目管理内提供**项目信息管理**:改名(T-PJ-04)、归档 / 取消归档(T-PJ-09)、删除项目(T-PJ-02,个人默认项目禁用)、退出项目(T-PJ-07,创建人可退) | 各操作在项目管理中可用且遵循既有约束;个人项目不出现删除与退出入口 | P0 |
### 3.9 项目权限(T-PM)
@@ -322,6 +330,7 @@
| T-UI-08 | 具备**空状态**展示(无项目 / 无任务 / 无子任务等) | 各层级无数据时展示空态与引导入口,不出现空白页 | P0 |
| T-UI-09 | 具备**加载状态**展示 | 首屏与切换视图时展示加载态,不出现闪烁或错乱 | P0 |
| T-UI-10 | 具备**数据异常状态**展示,可重试 | 接口失败时展示异常态与重试入口,重试成功后恢复 | P0 |
+| T-UI-11 | 弹层(任务详情等)**底部按钮与屏幕底边保持安全距离**,不被安全区 / 手势条遮挡 | 底部留白 ≥ 安全区高度 + 12px;小屏下弹层内容可滚动,删除等底部按钮始终完整可见可点 | P0 |
### 3.12 数据需求(T-DT)
@@ -375,8 +384,8 @@
## 4. 优先级汇总(本期交付范围)
-- **P0(必须)**:T-VW-01~04、T-VW-06、T-VW-07;T-COL-01~07、T-COL-09、T-COL-10;T-BD-01~10、13~16;T-FL-01~13、15;T-TK-01~11;T-AS-01~05、07;T-ST-01~09、11~14;T-PJ-01~08、10;T-PM-01、02、04~07;T-CP-01~06、08;T-UI-01~05、07~10;T-DT-01~08;T-NF-01~04;T-EX-01~05、07、09~12、15~17。
-- **P1(应该)**:T-VW-05;T-COL-08;T-BD-11、12;T-FL-14;T-TK-12、13;T-AS-06;T-ST-10、15;T-PJ-09;T-PM-03;T-CP-07、09;T-UI-06;T-DT-09;T-NF-05;T-EX-06、08、13、14、18、19。
+- **P0(必须)**:T-VW-01~04、T-VW-06~09;T-COL-01~07、T-COL-09、T-COL-10;T-BD-01~10、13~16;T-FL-01~13、15;T-TK-01~11;T-AS-01~05、07;T-ST-01~09、11~14;T-PJ-01~08、10~12;T-PM-01、02、04~07;T-CP-01~06、08;T-UI-01~05、07~11;T-DT-01~08;T-NF-01~04;T-EX-01~05、07、09~12、15~17。
+- **P1(应该)**:T-VW-05、10;T-COL-08;T-BD-11、12;T-FL-14;T-TK-12、13;T-AS-06;T-ST-10、15;T-PJ-09;T-PM-03;T-CP-07、09;T-UI-06;T-DT-09;T-NF-05;T-EX-06、08、13、14、18、19。
---
@@ -416,6 +425,11 @@
| 26 | 是否允许删除最后一个列? | **不允许**。删除唯一剩余列时拒绝并提示,项目至少保留 1 列(T-COL-06、T-EX-17) |
| 27 | 新增列的默认设置? | 按**默认值**:保留选项 `永久显示`、不绑定状态,**不继承**相邻列设置(T-COL-02、B-18) |
| 28 | 看板列数量上限? | 暂定**上限 10 列**(`待确认`,见下) |
+| 29 | 任务首屏结构? | **上部** = 最近使用的 3 个项目卡片 + 「更多项目」入口;**下部** = 当前选中项目的任务管理视图,默认上次访问的项目(T-VW-08/09) |
+| 30 | 首屏「任务卡片可以排序」的指代? | 指**最近项目卡片**可长按拖动排序,结果存设备端;任务卡片的列内排序维持既有口径(T-VW-10) |
+| 31 | 成员 / 列设置入口的位置? | 统一收进「**项目管理**」入口(含成员、列设置、项目信息三区),项目页头部不再散置(T-PJ-11) |
+| 32 | 项目信息管理包含什么? | 改名、归档 / 取消归档、删除(个人项目禁用)、退出(创建人可退),入口在项目管理内(T-PJ-12) |
+| 33 | 任务详情底部按钮遮挡? | 弹层底部预留**安全区 + 12px** 距离;小屏下弹层可滚动,底部按钮完整可见(T-UI-11) |
**仍待评审确认(影响实现口径):**
diff --git a/src/App.jsx b/src/App.jsx
index 3978886..acf78be 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -37,8 +37,9 @@ export default function App() {
{/* 打卡为默认首页 */}
} />
} />
- {/* 任务模块 (V0.2): 项目列表 / 项目内列表+看板 (T-PM-06) */}
+ {/* 任务模块 (V0.2): 首屏(最近项目+当前项目) / 项目列表 / 深链 (T-VW-08/09) */}
} />
+
} />
} />
{/* 宏观: 报警 / 新闻 / 油价 / 经济数据 / 策略研究 / 经济文章 (V0.2 原首页并入, 默认新闻) */}
} />
diff --git a/src/components/task/RecentProjects.jsx b/src/components/task/RecentProjects.jsx
new file mode 100644
index 0000000..3c86bf6
--- /dev/null
+++ b/src/components/task/RecentProjects.jsx
@@ -0,0 +1,136 @@
+import { useEffect, useMemo, useRef, useState } from 'react'
+import { getRecentIds, getRecentOrder, setRecentOrder } from '../../pages/task/prefs.js'
+
+// 首屏最近项目条 (T-VW-08/10)
+// - 展示最近使用的 3 个项目 + 「更多项目」入口
+// - 顺序: 手动排序(recentOrder)优先, 其次最近访问(recentIds), 兜底列表顺序
+// - 长按卡片 300ms 进入拖拽态, 水平拖动实时换位, 松手持久化(设备端)
+const COLORS = ['#1677ff', '#00b578', '#ff8f1f', '#a855f7', '#14b8a6', '#f43f9e']
+const colorOf = (id) => COLORS[Math.abs(id) % COLORS.length]
+const LONG_PRESS = 300
+const MOVE_TOLERANCE = 8
+
+export default function RecentProjects({ projects, currentId, onSelect, onMore }) {
+ const [ver, setVer] = useState(0) // 手动排序后强制重算
+ const [dragIdx, setDragIdx] = useState(-1) // 拖拽中的卡片下标
+ const barRef = useRef(null)
+ const timer = useRef(null)
+ const start = useRef(null)
+ const active = useRef(false)
+
+ // 展示顺序: 手动 > 自动 > 列表序, 只取未归档的前 3
+ const shown = useMemo(() => {
+ if (!projects) return []
+ const score = new Map()
+ getRecentOrder().forEach((id, i) => score.set(id, i))
+ let n = score.size
+ getRecentIds().forEach((id) => { if (!score.has(id)) score.set(id, n++) })
+ const fallback = n + 1000
+ return [...projects]
+ .filter((p) => !p.archived)
+ .sort((a, b) => (score.get(a.id) ?? fallback) - (score.get(b.id) ?? fallback))
+ .slice(0, 3)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [projects, ver])
+
+ useEffect(() => () => cleanup(), [])
+
+ function cleanup() {
+ if (timer.current) { clearTimeout(timer.current); timer.current = null }
+ active.current = false
+ start.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 }
+ }
+
+ // 按横坐标计算落点下标 (卡片宽 96 + 间距 8)
+ function indexAt(x) {
+ const rect = barRef.current?.getBoundingClientRect()
+ if (!rect) return dragIdx
+ const rel = x - rect.left + 4
+ return Math.max(0, Math.min(shown.length - 1, Math.floor(rel / 104)))
+ }
+
+ function onMove(ev) {
+ const p = pointOf(ev)
+ if (!active.current) {
+ // 未激活前: 移动超容差则取消长按 (用户想滚动)
+ if (start.current && (Math.abs(p.x - start.current.x) > MOVE_TOLERANCE || Math.abs(p.y - start.current.y) > MOVE_TOLERANCE)) {
+ if (timer.current) { clearTimeout(timer.current); timer.current = null }
+ }
+ return
+ }
+ if (ev.cancelable) ev.preventDefault()
+ const to = indexAt(p.x)
+ if (to !== dragIdx && to >= 0) {
+ // 实时换位: 重写设备端顺序, ver 触发重算 (shown 由 memo 派生)
+ const ids = shown.map((p2) => p2.id)
+ const [moved] = ids.splice(dragIdx, 1)
+ ids.splice(to, 0, moved)
+ setRecentOrder(ids)
+ setDragIdx(to)
+ setVer((v) => v + 1)
+ }
+ }
+
+ function onEnd() {
+ if (active.current) setVer((v) => v + 1)
+ cleanup()
+ setDragIdx(-1)
+ }
+
+ function beginPress(i, ev) {
+ if (ev.button && ev.button !== 0) return
+ const p = pointOf(ev)
+ start.current = p
+ active.current = false
+ cleanup()
+ timer.current = setTimeout(() => {
+ active.current = true
+ setDragIdx(i)
+ 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)
+ }
+
+ if (!projects) return null
+
+ return (
+ <>
+
+ {shown.map((p, i) => (
+
beginPress(i, ev)}
+ onMouseDown={(ev) => beginPress(i, ev)}
+ onClick={() => { if (!active.current && dragIdx < 0) onSelect?.(p.id) }}
+ >
+
+ {(p.name || '?').slice(0, 1)}
+
+
{p.name}
+
{p.taskCount ?? 0} 个任务
+
⠿
+
+ ))}
+
+
+
长按卡片可调整顺序(本机记忆)
+ >
+ )
+}
diff --git a/src/components/task/UserPicker.jsx b/src/components/task/UserPicker.jsx
index 8c85884..7eb789d 100644
--- a/src/components/task/UserPicker.jsx
+++ b/src/components/task/UserPicker.jsx
@@ -34,7 +34,7 @@ export default function UserPicker({
}
return (
-
+
{title}
仅可选择当前项目的成员
diff --git a/src/pages/task/ProjectManage.jsx b/src/pages/task/ProjectManage.jsx
new file mode 100644
index 0000000..ce3a219
--- /dev/null
+++ b/src/pages/task/ProjectManage.jsx
@@ -0,0 +1,172 @@
+import { useState } from 'react'
+import { Dialog, Input, Toast } from 'antd-mobile'
+import { projectApi } from '../../api/task.js'
+import { useAuth } from '../../auth/AuthContext.jsx'
+import ColumnSettings from './ColumnSettings.jsx'
+import ProjectMembers from './ProjectMembers.jsx'
+
+// 项目管理 (T-PJ-11/12): 成员 / 看板列 / 项目信息 三个分区的统一入口
+// onExited: 项目被删除/退出/归档后, 上层应离开该项目(回落个人默认项目)
+export default function ProjectManage({ project, onClose, onChanged, onExited }) {
+ const { user } = useAuth()
+ const [view, setView] = useState('main') // main | columns | members
+ const [renaming, setRenaming] = useState(false)
+ const [name, setName] = useState(project.name)
+
+ const members = project.members || []
+ const isOwner = user?.id === project.ownerId
+ const isPersonal = !!project.isPersonal
+
+ if (view === 'columns') {
+ return (
+ setView('main')}
+ onSaved={() => { setView('main'); onChanged?.() }}
+ />
+ )
+ }
+ if (view === 'members') {
+ return (
+ setView('main')}
+ onChanged={onChanged}
+ />
+ )
+ }
+
+ async function saveName() {
+ const t = name.trim()
+ if (!t) { Toast.show({ content: '项目名称不能为空' }); return }
+ try {
+ await projectApi.update(project.id, { name: t })
+ Toast.show({ content: '已改名' })
+ setRenaming(false)
+ onChanged?.()
+ } catch (e) {
+ Toast.show({ content: e.message })
+ }
+ }
+
+ async function toggleArchive() {
+ const toArchive = !project.archived
+ const ok = await Dialog.confirm({
+ content: toArchive ? '归档该项目?归档后从进行中列表移出,可在归档区找回。' : '取消归档该项目?',
+ confirmText: toArchive ? '归档' : '取消归档',
+ })
+ if (!ok) return
+ try {
+ await projectApi.update(project.id, { archived: toArchive })
+ Toast.show({ content: toArchive ? '已归档' : '已恢复' })
+ if (toArchive) onExited?.()
+ else onChanged?.()
+ } catch (e) {
+ Toast.show({ content: e.message })
+ }
+ }
+
+ async function remove() {
+ const ok = await Dialog.confirm({
+ content: `删除项目「${project.name}」?其中的任务、看板列将一并删除,不可恢复。`,
+ confirmText: '删除',
+ })
+ if (!ok) return
+ try {
+ await projectApi.remove(project.id)
+ Toast.show({ content: '已删除' })
+ onExited?.()
+ } catch (e) {
+ Toast.show({ content: e.message })
+ }
+ }
+
+ async function quit() {
+ const ok = await Dialog.confirm({
+ content: '退出该项目?退出后将不再可见。',
+ confirmText: '退出',
+ })
+ if (!ok) return
+ try {
+ await projectApi.removeMember(project.id, user.id)
+ Toast.show({ content: '已退出' })
+ onExited?.()
+ } catch (e) {
+ Toast.show({ content: e.message })
+ }
+ }
+
+ return (
+
+
+ ‹
+ 项目管理 · {project.name}
+
+
+
+
+ 项目内所有成员权限均等、全读写;以下配置对全体成员即时生效。
+
+
+
成员({members.length})
+
setView('members')}>
+
{(members[0]?.name || '?').slice(0, 1)}
+
+ {members.slice(0, 3).map((m) => m.name).join('、')}{members.length > 3 ? ` 等 ${members.length} 人` : ''}
+
+
管理 ›
+
+
+
看板列
+
setView('columns')}>
+
+ {(project.columns || []).map((c) => c.name).join(' · ')}
+
+ 列设置 ›
+
+
+
项目信息
+
{ setName(project.name); setRenaming(true) }}>
+ 项目名称
+ {project.name}
+
+
+ 归档项目
+ {project.archived ? '取消归档' : '归档'}
+
+ {!isPersonal && (
+
+ 删除项目
+ 删除
+
+ )}
+ {isOwner && !isPersonal && (
+
+ 退出项目
+ 退出
+
+ )}
+
+
+ 个人默认项目不可删除、不可退出(T-PJ-02);退出项目仅创建人可见(T-PJ-07)。
+
+
+
+
+ }
+ actions={[
+ { key: 'c', text: '取消', onClick: () => setRenaming(false) },
+ { key: 'ok', text: '保存', bold: true, onClick: saveName },
+ ]}
+ onClose={() => setRenaming(false)}
+ />
+
+ )
+}
diff --git a/src/pages/task/ProjectView.jsx b/src/pages/task/ProjectView.jsx
index e926674..6d8687b 100644
--- a/src/pages/task/ProjectView.jsx
+++ b/src/pages/task/ProjectView.jsx
@@ -5,20 +5,19 @@ 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'
+import ProjectManage from './ProjectManage.jsx'
// 项目视图容器: 列表 / 看板 切换 + 数据装载 + 乐观更新
-export default function ProjectView({ projectId, onBack }) {
+// 嵌在任务首屏下部 (T-VW-09); 头部仅保留「项目管理」与「+ 新增」(T-PJ-11)
+export default function ProjectView({ projectId, onProjectChanged, onProjectExited }) {
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 [showManage, setShowManage] = useState(false)
const [adding, setAdding] = useState(false)
const [tick, setTick] = useState(0) // 保留窗口重算触发
@@ -139,12 +138,8 @@ export default function ProjectView({ projectId, onBack }) {
if (err) {
return (
-