From 0cdbfb434fa39fe138589a28257efa66111ddee5 Mon Sep 17 00:00:00 2001 From: "jif2.zhang" Date: Sat, 24 Oct 2026 20:32:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(V0.2):=20=E4=BB=BB=E5=8A=A1=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E7=95=8C=E9=9D=A2=E8=B0=83=E6=95=B4=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E9=A6=96=E5=B1=8F/=E9=A1=B9=E7=9B=AE=E7=AE=A1?= =?UTF-8?q?=E7=90=86/=E5=BA=95=E9=83=A8=E5=AE=89=E5=85=A8=E8=B7=9D?= =?UTF-8?q?=E7=A6=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按 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 缺失 --- docs/V0.2/任务模块/任务模块功能设计.md | 22 +- docs/V0.2/任务模块/任务模块界面设计.html | 270 ++++++++++++++---- .../任务模块/任务模块设计文档-框架设计.md | 33 ++- docs/V0.2/任务模块/任务模块需求文档.md | 26 +- src/App.jsx | 3 +- src/components/task/RecentProjects.jsx | 136 +++++++++ src/components/task/UserPicker.jsx | 2 +- src/pages/task/ProjectManage.jsx | 172 +++++++++++ src/pages/task/ProjectView.jsx | 55 ++-- src/pages/task/TaskDetail.jsx | 4 +- src/pages/task/TaskPage.jsx | 135 ++++++--- src/pages/task/prefs.js | 57 +++- src/styles/globals.css | 35 +++ 13 files changed, 771 insertions(+), 179 deletions(-) create mode 100644 src/components/task/RecentProjects.jsx create mode 100644 src/pages/task/ProjectManage.jsx diff --git a/docs/V0.2/任务模块/任务模块功能设计.md b/docs/V0.2/任务模块/任务模块功能设计.md index 094332c..db505a2 100644 --- a/docs/V0.2/任务模块/任务模块功能设计.md +++ b/docs/V0.2/任务模块/任务模块功能设计.md @@ -4,14 +4,14 @@ | --- | --- | | 版本 | V0.2(对应分支 `dev-0.2`,应用版本 `0.2.0`) | | 文档状态 | **设计基线**(待评审) | -| 上游文档 | `任务模块需求文档.md`(**142 条需求点**,编号 `T-*`) | +| 上游文档 | `任务模块需求文档.md`(**148 条需求点**,编号 `T-*`) | | 配套文档 | `任务模块设计文档-框架设计.md`、`任务模块数据库设计文档.md`、`任务模块界面设计.html` | | 对应关系 | **本文与需求点一一对应**:每条需求点均给出「设计落点(功能/组件/接口/表字段)」与「实现要点」 | > **阅读方式** > 1. 本文按需求文档的模块码组织(`VW` / `COL` / `BD` / `FL` / `TK` / `AS` / `ST` / `PJ` / `PM` / `CP` / `UI` / `DT` / `NF` / `EX`)。 > 2. 每条需求点一行:**编号 → 设计落点 → 实现要点**。设计落点中的组件名、接口路径、表字段均与《任务模块数据库设计文档》《任务模块界面设计》一致。 -> 3. 文末 §15 给出**覆盖自检表**,确保 142 条无遗漏。 +> 3. 文末 §15 给出**覆盖自检表**,确保 148 条无遗漏。 --- @@ -62,6 +62,11 @@ | T-VW-05 | 默认值 `'list'` | 新项目首次进入为列表 | | T-VW-06 | `localStorage.iboard_task_prefs` | **不落库、不同步**;换设备不继承(与 T-FL-15 的项目级配置区分) | | T-VW-07 | `prefs.getLastProjectId()` / `setLastProjectId()` | 进入 `/task` 时读取;无记录或记录的项目已不可见 ⇒ 回落个人默认项目 | +| T-VW-08 | `TaskPage` 首屏上部 `RecentProjects` 条 | 取最近访问的 3 个项目(按设备端访问时间倒序),末尾「更多项目」入口 → `/task/list`(完整项目列表 `ProjectList`,从「更多」进入) | +| T-VW-09 | `TaskPage` 首屏下部直接内嵌 `ProjectView` | 当前项目 = 上次访问项目(无记录 ⇒ 个人默认项目);点首屏项目卡片切换当前项目,下部整体切换,不跳转页面 | +| T-VW-10 | `prefs.js` 增 `recentOrder[]`(设备端) | 长按卡片拖动排序并持久化;无手动记录时按最近访问时间自动排列;项目不可见时从记录剔除 | + +> **首屏结构调整(T-VW-08/09)**:`TaskPage`(`/task`)不再以项目列表为首屏,改为「最近项目条 + 当前项目 `ProjectView`」;完整项目列表迁移至 `/task/list`。旧路由 `/task/:pid` 保留(打开指定项目),供深链使用。 > **实现细节**:`lastProjectId` 指向的项目若已被退出/删除,视为"无记录",静默回落到个人默认项目,不弹错。 @@ -203,6 +208,8 @@ | T-PJ-08 | 删除最后成员不删项目 | 仅删 `ProjectMember`(B-11);见 T-EX-11 | | T-PJ-09 | `Project.archived` | 列表分「进行中 / 已归档」 | | T-PJ-10 | 列表查询按成员关系过滤 | `WHERE EXISTS ProjectMember(userId)` | +| T-PJ-11 | 新组件 `ProjectManage`(项目管理页) | 分区承载:成员(复用 `ProjectMembers` 逻辑)、列设置(复用 `ColumnSettings`)、项目信息;`ProjectView` 头部仅保留「项目管理」一个入口 | +| T-PJ-12 | `ProjectManage` 的「项目信息」分区 | 改名(`PUT /api/projects/:id {name}`)、归档/取消归档(`{archived}`)、删除(个人项目隐藏入口)、退出(仅创建人可见);删除与退出均有二次确认 | --- @@ -250,6 +257,7 @@ | T-UI-08 | `EmptyState` 组件 | 无项目 / 无任务 / 无子任务三档空态 + CTA | | T-UI-09 | `LoadingState` 骨架屏 | 首屏与切换视图 | | T-UI-10 | `ErrorState` 组件 | 展示错误 + 重试按钮 | +| T-UI-11 | 弹层容器统一 `padding-bottom: calc(env(safe-area-inset-bottom) + 20px)` | 底部按钮(删除等)与屏幕底边保持安全距离;小屏下弹层 `max-height` + 内部滚动,底部按钮始终完整可见 | --- @@ -311,20 +319,20 @@ | 模块 | 编号范围 | 条数 | 本文对应章节 | | --- | --- | --- | --- | -| 视图体系 | T-VW-01 ~ 07 | 7 | §1 | +| 视图体系 | T-VW-01 ~ 10 | 10 | §1 | | 看板列管理 | T-COL-01 ~ 10 | 10 | §2 | | 列-状态绑定 | T-BD-01 ~ 16 | 16 | §3 | | 已完成显示控制 | T-FL-01 ~ 15 | 15 | §4 | | 任务字段体系 | T-TK-01 ~ 13 | 13 | §5 | | 负责人 & 成员 | T-AS-01 ~ 07 | 7 | §6 | | 子任务体系 | T-ST-01 ~ 15 | 15 | §7 | -| 项目体系 | T-PJ-01 ~ 10 | 10 | §8 | +| 项目体系 | T-PJ-01 ~ 12 | 12 | §8 | | 项目权限与入口 | T-PM-01 ~ 07 | 7 | §9 | | 跨项目拷贝 | T-CP-01 ~ 09 | 9 | §10 | -| UI 交互规范 | T-UI-01 ~ 10 | 10 | §11 | +| UI 交互规范 | T-UI-01 ~ 11 | 11 | §11 | | 数据需求 | T-DT-01 ~ 09 | 9 | §12 | | 非功能需求 | T-NF-01 ~ 05 | 5 | §13 | | 异常与边界 | T-EX-01 ~ 19 | 19 | §14 | -| **合计** | — | **142** | — | +| **合计** | — | **148** | — | -> 需求文档共 **142 条**需求点,本文逐条给出设计落点,覆盖率 **142/142**。(其中 `T-BD-14~16`、`T-COL-*`、`T-EX-18~19` 为评审后新增,编号不连续,非笔误。) +> 需求文档共 **148 条**需求点,本文逐条给出设计落点,覆盖率 **148/148**。(其中 `T-BD-14~16`、`T-COL-*`、`T-EX-18~19`、`T-VW-08~10`、`T-PJ-11/12`、`T-UI-11` 为评审后新增,编号不连续,非笔误。) diff --git a/docs/V0.2/任务模块/任务模块界面设计.html b/docs/V0.2/任务模块/任务模块界面设计.html index 299ff74..5f4d15d 100644 --- a/docs/V0.2/任务模块/任务模块界面设计.html +++ b/docs/V0.2/任务模块/任务模块界面设计.html @@ -100,6 +100,28 @@ .sdot.pill{width:auto;height:auto;border-radius:999px;font-size:10px;padding:1px 6px;color:#fff;line-height:14px} .sdot.pill.todo{background:#a8b0ba} .sdot.pill.doing{background:var(--primary)} .sdot.pill.done{background:var(--success)} + /* ---------- 首屏 · 最近项目条 (T-VW-08/10) ---------- */ + .rp-bar { display: flex; gap: 8px; padding: 10px 12px 6px; overflow-x: auto; flex-shrink: 0; } + .rp-card { + flex: 0 0 96px; background: #fff; border-radius: 10px; padding: 9px 10px; + box-shadow: 0 1px 2px rgba(0,0,0,.04); cursor: pointer; position: relative; + border: 1.5px solid transparent; + } + .rp-card.on { border-color: var(--primary); background: #f5f9ff; } + .rp-card .rp-ico { + width: 26px; height: 26px; border-radius: 7px; color: #fff; font-size: 11px; + font-weight: 700; display: flex; align-items: center; justify-content: center; + } + .rp-card .rp-name { font-size: 12px; font-weight: 600; margin-top: 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .rp-card .rp-meta { font-size: 9.5px; color: var(--weak); margin-top: 1px; } + .rp-card .rp-grip { position: absolute; top: 5px; right: 6px; color: #d3d8de; font-size: 10px; letter-spacing: -1px; } + .rp-more { + flex: 0 0 auto; align-self: stretch; display: flex; flex-direction: column; align-items: center; + justify-content: center; gap: 3px; color: var(--muted); font-size: 10.5px; cursor: pointer; padding: 0 6px; + } + .rp-more .rp-more-ico { width: 30px; height: 30px; border-radius: 50%; background: #f2f3f5; color: #9aa1aa; display: flex; align-items: center; justify-content: center; font-size: 14px; } + .rp-drag-tip { font-size: 9.5px; color: var(--weak); padding: 0 14px 2px; } + /* ---------- 项目列表 ---------- */ .proj-head{padding:12px 14px 8px;display:flex;align-items:center} .proj-head b{font-size:18px;font-weight:700} @@ -186,7 +208,7 @@ .sw-toggle.on{background:var(--primary)} .sw-toggle.on::after{left:19px} .mask{position:absolute;inset:28px 0 0 0;background:rgba(0,0,0,.45);z-index:9} - .sheet{position:absolute;left:0;right:0;bottom:0;background:#fff;border-radius:12px 12px 0 0;padding:16px 16px 14px;z-index:10;max-height:640px;overflow:hidden} + .sheet{position:absolute;left:0;right:0;bottom:0;background:#fff;border-radius:12px 12px 0 0;padding:16px 16px calc(env(safe-area-inset-bottom, 0px) + 20px);z-index:10;max-height:640px;overflow:hidden} .sheet-title{font-size:16px;font-weight:700;margin-bottom:4px} .sheet-sub{font-size:11.5px;color:var(--weak);margin-bottom:14px} .field{margin-bottom:13px} @@ -284,23 +306,119 @@ -

2 项目列表 入口 · 个人默认项目 · 归档分组

-

顶级 Tab「任务」的第一屏。个人默认项目置顶且不可删除、不可加成员(T-PJ-01~04);普通协作项目可增删成员(T-PJ-05/06)。

+

2 任务首屏 最近项目条(可排序)+ 当前项目任务管理 · 更多进项目列表

+

首屏上部为最近使用的 3 个项目卡片 + 「更多项目」入口(T-VW-08),卡片可长按拖动排序(T-VW-10,设备端记忆);下部直接是当前选中项目的任务管理视图(T-VW-09,默认上次访问的项目),不必先进项目列表。完整项目列表从「更多项目」进入。

-
-
- 我的项目 -
-
+ 归档
-
+ 新建
+
+ +
+
+
张
+
张三的任务
+
12 个任务
+
⠿
+
+
+
APP
+
APP 改版
+
28 个任务
+
⠿
+
+
+
二
+
APP 二期
+
9 个任务
+
⠿
+
+
+
▤
+ 更多项目
+
长按卡片可调整顺序(本机记忆)
-
+ + +
+
列表
看板
+
过滤
+
+
+
+
+
+
登录接口返回 500
+
+ 执行中 + Doing + 张三 + + 李四、王五 +
+
+
›
+
+
+
+
+
首页改版视觉走查
+
+ 执行中 + Doing + 李四 +
+
+
›
+
+
+
+
+
需求评审
+
+ 完成 + Done + 王五 +
+
+
›
+
+
+
+
+
打卡
+
任务
+
宏观
+
监控
+
我
+
+
+
+ 任务首屏(T-VW-08/09) +

上:最近 3 个项目卡片,当前项目主色描边高亮;长按「⠿」拖动排序(T-VW-10,设备端)。下:当前项目的任务管理,默认上次访问的项目。项目页头部仅保留「项目管理」+「+ 新增」两个入口(T-PJ-11)。

+
+
+ +
+
+ + +
+
张
张三的任务 个人
@@ -354,32 +472,8 @@
- 说明 -

个人默认项目带「个人」徽标,无删除入口;归档项目降透明并单独分组。Tab 共 5 项,任务位于打卡之后(T-PM-06),默认首页仍为打卡。

-
-
- -
-
- -
-
-
▤
-

还没有协作项目

-
新建项目
-
-
-
-
打卡
-
任务
-
宏观
-
监控
-
我
-
-
-
- 空状态(T-UI-08) -

无协作项目时展示空态与 CTA;个人默认项目始终存在,故不会出现完全空白。

+ 我的项目列表(从「更多项目」进入) +

完整项目列表:个人默认项目带「个人」徽标、无删除入口;归档项目降透明分组。选择项目后返回首屏并切换当前项目(T-VW-09)。

@@ -395,14 +489,14 @@ ‹ APP 改版
-
⋯
+
项目管理
+ 新增
列表
看板
-
过滤
成员
+
过滤
Doing 2
@@ -496,12 +590,12 @@
列表
看板
-
过滤
成员
+
过滤
Done 3
@@ -563,12 +657,12 @@
列表
看板
-
列设置
+
过滤
@@ -668,12 +762,12 @@
列表
看板
-
列设置
+
过滤
@@ -838,12 +932,12 @@
列表
看板
-
列设置
+
过滤
@@ -895,7 +989,7 @@
@@ -958,7 +1052,7 @@
详情(扩展信息收起) -

负责人默认填入创建者本人,可清空为「未指派」(T-TK-09/10)。扩展信息卡片默认收起,右侧标出条目数。

+

负责人默认填入创建者本人,可清空为「未指派」(T-TK-09/10)。扩展信息卡片默认收起,右侧标出条目数。弹层底部预留 安全区 + 20px(T-UI-11):删除按钮不被手势条遮挡,小屏下弹层内部滚动、底部按钮始终完整可见。

@@ -968,7 +1062,7 @@
@@ -1043,10 +1137,67 @@
-

7 成员与跨项目拷贝 项目成员唯一选人源 · 拷贝非移动

-

负责人与任务成员只能从当前项目成员中选择(T-AS-03/04);跨项目操作是拷贝(T-CP-01),新任务创建人为操作者(T-CP-04)。

+

7 项目管理 · 成员与跨项目拷贝 统一入口:成员 / 列设置 / 项目信息

+

项目页头部的「项目管理」进入统一管理页(T-PJ-11),含成员、列设置、项目信息三个分区;负责人与任务成员只能从当前项目成员中选择(T-AS-03/04);跨项目操作是拷贝(T-CP-01)。

+
+
+ + +
+
+ 项目内所有成员权限均等、全读写(T-PM-02);以下配置对全体成员即时生效。 +
+ +
成员(4)
+
+
张
+ 张三 创建人 + 管理 › +
+ +
看板列
+
+ Plan · Doing · Action · Done + 列设置 › +
+ +
项目信息
+
+ 项目名称 + APP 改版 +
+
+ 归档项目 + 归档 +
+
+ 删除项目 + 删除 +
+ +
+ 个人默认项目不出现「删除项目」;退出项目仅创建人可见(T-PJ-07)。删除与退出均有二次确认。 +
+
+
+
打卡
+
任务
+
宏观
+
监控
+
我
+
+
+
+ 项目管理(T-PJ-11/12) +

三分区:成员(增删)、列设置(进入第 5 节的列设置页)、项目信息(改名 / 归档 / 删除 / 退出)。项目页头部不再散置成员与列设置入口。

+
+
+
@@ -1105,7 +1256,7 @@
@@ -1173,7 +1324,7 @@
列表
看板
@@ -1198,7 +1349,7 @@
列表
看板
@@ -1224,7 +1375,7 @@
列表
看板
@@ -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 ( -
-
- ‹ - 任务 -
-
+
+
!

{err}

重试
@@ -155,24 +150,21 @@ export default function ProjectView({ projectId, onBack }) { if (!project || !tasks) { return ( -
-
- ‹ - 任务 +
+
+
··

加载中…

-
··

加载中…

) } return ( -
-
- ‹ +
+
{project.name}
- setShowMembers(true)}>成员 - setAdding((v) => !v)}>+ 新增 + setShowManage(true)}>项目管理 + setAdding((v) => !v)}>+ 新增
@@ -182,9 +174,6 @@ export default function ProjectView({ projectId, onBack }) {
switchMode('list')}>列表
switchMode('board')}>看板
-
- setShowColumns(true)}>列设置 -
{adding && ( @@ -234,20 +223,12 @@ export default function ProjectView({ projectId, onBack }) { /> )} - {showColumns && ( - setShowColumns(false)} - onSaved={() => { setShowColumns(false); load() }} - /> - )} - - {showMembers && ( - setShowMembers(false)} - onChanged={load} + onClose={() => setShowManage(false)} + onChanged={() => { load(); onProjectChanged?.() }} + onExited={() => { setShowManage(false); onProjectExited?.() }} /> )}
diff --git a/src/pages/task/TaskDetail.jsx b/src/pages/task/TaskDetail.jsx index 14be143..b5fc59a 100644 --- a/src/pages/task/TaskDetail.jsx +++ b/src/pages/task/TaskDetail.jsx @@ -133,7 +133,7 @@ export default function TaskDetail({ task, columns, members, onClose, onSaved, o visible onMaskClick={onClose} position="bottom" - bodyStyle={{ borderTopLeftRadius: 12, borderTopRightRadius: 12, maxHeight: '88vh', overflowY: 'auto', padding: '16px 16px 14px' }} + bodyStyle={{ borderTopLeftRadius: 12, borderTopRightRadius: 12, maxHeight: '88vh', overflowY: 'auto', padding: '16px 16px calc(env(safe-area-inset-bottom, 0px) + 20px)' }} >
{task.title}
@@ -253,7 +253,7 @@ export default function TaskDetail({ task, columns, members, onClose, onSaved, o setCopies(null)} - bodyStyle={{ borderTopLeftRadius: 12, borderTopRightRadius: 12, padding: '16px 16px 14px' }} + bodyStyle={{ borderTopLeftRadius: 12, borderTopRightRadius: 12, padding: '16px 16px calc(env(safe-area-inset-bottom, 0px) + 20px)' }} >
跨项目拷贝
源任务保持不变;新任务的创建人是你
diff --git a/src/pages/task/TaskPage.jsx b/src/pages/task/TaskPage.jsx index 0ccfd52..50c2097 100644 --- a/src/pages/task/TaskPage.jsx +++ b/src/pages/task/TaskPage.jsx @@ -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 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 ( +
+
+ navigate('/task')}>‹ + 我的项目 +
+
+ { selectProject(id); navigate('/task') }} + onChanged={load} + /> +
+
+ ) } return (
-
- { - setLastProjectId(id) - navigate(`/task/${id}`) - }} - onChanged={load} - /> +
+ {err && !projects ? ( +
+
!
+

{err}

+
重试
+
+ ) : !projects ? ( +
+
··
+

加载中…

+ +
+ ) : ( + <> + navigate('/task/list')} + /> + {currentId != null && ( + + )} + + )}
) } +// 完整项目列表 (从「更多项目」进入) 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 ( -
··

加载中…

- ) + return
··

加载中…

} 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 ( <> -
- 我的项目 -
-
setCreating(true)}>+ 新建
-
-
- +
{active.map((p) => (
onOpen(p.id)}>
@@ -150,17 +207,14 @@ function ProjectList({ projects, err, onRetry, onOpen, onChanged }) { )} +
+ - +
} actions={[ @@ -172,6 +226,3 @@ function ProjectList({ projects, err, onRetry, onOpen, onChanged }) { ) } - -// 避免未使用告警 -void Empty diff --git a/src/pages/task/prefs.js b/src/pages/task/prefs.js index 409e8a0..959864a 100644 --- a/src/pages/task/prefs.js +++ b/src/pages/task/prefs.js @@ -1,19 +1,27 @@ // 任务模块: 设备端偏好 (不落库, 不跨设备) // lastProjectId 上次访问的项目 (T-VW-07) // viewMode 按项目记忆的视图 list | board (T-VW-03/06) +// recentIds 自动按最近访问排序的项目 id (最前 = 最近), 上限 10 (T-VW-08) +// recentOrder 首屏最近项目卡片的手动排序结果, 优先于 recentIds (T-VW-10) const KEY = 'iboard_task_prefs' +function blank() { + return { lastProjectId: null, viewMode: {}, recentIds: [], recentOrder: [] } +} + function read() { try { const raw = localStorage.getItem(KEY) - if (!raw) return { lastProjectId: null, viewMode: {} } + if (!raw) return blank() const obj = JSON.parse(raw) return { lastProjectId: obj.lastProjectId ?? null, viewMode: obj.viewMode && typeof obj.viewMode === 'object' ? obj.viewMode : {}, + recentIds: Array.isArray(obj.recentIds) ? obj.recentIds : [], + recentOrder: Array.isArray(obj.recentOrder) ? obj.recentOrder : [], } } catch { - return { lastProjectId: null, viewMode: {} } + return blank() } } @@ -46,13 +54,44 @@ export function setViewMode(projectId, mode) { write(cur) } -/** 清理已不可见的项目记录 */ -export function prunePrefs(visibleIds) { - const set = new Set((visibleIds || []).map(String)) +/** 最近访问记录: 把 id 提到队首, 上限 10 (T-VW-08) */ +export function getRecentIds() { + return read().recentIds +} + +export function setRecentIds(id) { + if (!id) return const cur = read() - if (cur.lastProjectId && !set.has(String(cur.lastProjectId))) cur.lastProjectId = null - for (const k of Object.keys(cur.viewMode)) { - if (!set.has(k)) delete cur.viewMode[k] - } + cur.recentIds = [id, ...cur.recentIds.filter((x) => x !== id)].slice(0, 10) write(cur) } + +/** 首屏卡片手动排序 (T-VW-10): 完整顺序, 排在前面的项目优先展示 */ +export function getRecentOrder() { + return read().recentOrder +} + +export function setRecentOrder(ids) { + const cur = read() + cur.recentOrder = (Array.isArray(ids) ? ids : []).map(Number).filter(Boolean) + write(cur) +} + +/** 清理已不可见的项目记录 */ +export function prunePrefs(visibleIds) { + const set = new Set((visibleIds || []).map(Number)) + const cur = read() + let dirty = false + if (cur.lastProjectId && !set.has(cur.lastProjectId)) { cur.lastProjectId = null; dirty = true } + for (const k of Object.keys(cur.viewMode)) { + if (!set.has(Number(k))) { delete cur.viewMode[k]; dirty = true } + } + const ri = cur.recentIds.filter((id) => set.has(id)) + const ro = cur.recentOrder.filter((id) => set.has(id)) + if (ri.length !== cur.recentIds.length || ro.length !== cur.recentOrder.length) dirty = true + if (dirty) { + cur.recentIds = ri + cur.recentOrder = ro + write(cur) + } +} diff --git a/src/styles/globals.css b/src/styles/globals.css index dc47a74..2b3f3c4 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -1096,3 +1096,38 @@ textarea.inp { resize: vertical; } } .empty .ico.err { background: #fff1f0; color: #ff7875; } .empty p { font-size: 13px; margin-bottom: 12px; } + +/* ---- 任务首屏 (T-VW-08/09/10): 最近项目条 + 当前项目 ---- */ +.task-home { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; } +.rp-bar { display: flex; gap: 8px; padding: 10px 12px 6px; overflow-x: auto; flex-shrink: 0; } +.rp-card { + flex: 0 0 96px; background: #fff; border-radius: 10px; padding: 9px 10px; + box-shadow: 0 1px 2px rgba(0,0,0,.04); cursor: pointer; position: relative; + border: 1.5px solid transparent; +} +.rp-card.on { border-color: var(--color-primary); background: #f5f9ff; } +.rp-card.dragging { opacity: .6; transform: scale(.96); box-shadow: 0 8px 18px rgba(15,23,42,.18); z-index: 5; } +.rp-card .rp-ico { + width: 26px; height: 26px; border-radius: 7px; color: #fff; font-size: 11px; + font-weight: 700; display: flex; align-items: center; justify-content: center; +} +.rp-card .rp-name { font-size: 12px; font-weight: 600; margin-top: 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.rp-card .rp-meta { font-size: 9.5px; color: #999; margin-top: 1px; } +.rp-card .rp-grip { position: absolute; top: 5px; right: 6px; color: #d3d8de; font-size: 10px; letter-spacing: -1px; } +.rp-more { + flex: 0 0 auto; align-self: stretch; display: flex; flex-direction: column; align-items: center; + justify-content: center; gap: 3px; color: #666; font-size: 10.5px; cursor: pointer; padding: 0 6px; +} +.rp-more .rp-more-ico { width: 30px; height: 30px; border-radius: 50%; background: #f2f3f5; color: #9aa1aa; display: flex; align-items: center; justify-content: center; font-size: 14px; } +.rp-drag-tip { font-size: 9.5px; color: #b8bcc4; padding: 0 14px 2px; flex-shrink: 0; } + +/* 首屏内嵌的项目视图 (T-VW-09) */ +.pv-embed { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; } +.task-projbar { + display: flex; align-items: center; gap: 10px; padding: 9px 14px; + background: #fff; border-bottom: 1px solid var(--color-border); flex-shrink: 0; margin-top: 4px; +} +.task-projbar .back { font-size: 20px; color: #666; line-height: 1; cursor: pointer; } +.task-projbar .title { font-size: 15px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.task-projbar .right { margin-left: auto; display: flex; gap: 6px; flex-shrink: 0; } +.pv-embed .task-content { flex: 1 1 auto; }