From 9fea1a5a3a87a5d699f9d8736a69cf00a0fda010 Mon Sep 17 00:00:00 2001 From: "jif2.zhang" Date: Sat, 24 Oct 2026 22:15:52 +0800 Subject: [PATCH] =?UTF-8?q?fix(V0.2):=20=E4=BA=8C=E8=BD=AE=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=206=20=E9=A1=B9=20=E2=80=94=E2=80=94=20BUILD=5FID=20?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=20/=20=E8=8F=9C=E5=8D=95=E4=B8=8E=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E7=B2=BE=E7=AE=80=20/=20=E6=8B=96=E5=8A=A8=E9=87=8D?= =?UTF-8?q?=E5=81=9A=20/=20=E9=94=AE=E7=9B=98=E7=BB=9F=E4=B8=80=E6=96=B9?= =?UTF-8?q?=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. BUILD_ID 改为 yyyyMMddHHmmss (去掉中间点), 仍 UTC+8 2. 任务 ⋯ 菜单去掉顶部任务名标题 3. 任务详情去掉底部「拷贝到其他项目」「删除任务」按钮 (⋯ 菜单已有) 4+6. 键盘遮挡统一方案 (重点设计 §7.2): - index.html 加 interactive-widget=resizes-content - App 级 useKeyboardInsetWatcher 写 --kb-inset (visualViewport+window 双源) - 底部弹层统一 .kb-safe 类 (安全区+20px+键盘插入量, dvh 收缩+内部滚动) - TaskDetail/UserPicker/CopyTaskPopup/信息块弹层全部迁移, 删除 per-component hook - 信息块 textarea 补 focus scrollIntoView 兜底 5. 拖动方案重做 (重点设计 §7.1): - 新 useColumnDrag: Pointer Events + setPointerCapture, 长按400ms原位抬起, elementFromPoint 高亮目标列, 边缘 rAF 自动横滚; 移除手指跟随浮层(任务名) - 列表行长按 500ms → 「移动到列」ActionSheet (不做自由拖动) - 移除「拖到卡片转子任务」误触路径, 统一走详情页层级区 - 删除旧 useLongPressDrag 已验证: 99 文件语法 0 错误、导入 0 缺失 --- .../任务模块/任务模块设计文档-框架设计.md | 26 ++++ index.html | 26 ++-- src/App.jsx | 3 + src/components/task/CopyTaskPopup.jsx | 5 +- src/components/task/TaskRow.jsx | 33 ++++- src/components/task/UserPicker.jsx | 2 +- src/components/task/useColumnDrag.js | 114 ++++++++++++++++++ src/components/task/useLongPressDrag.js | 99 --------------- src/hooks/useKeyboardInset.js | 27 ----- src/hooks/useKeyboardInsetWatcher.js | 35 ++++++ src/pages/task/BoardView.jsx | 111 +++++++---------- src/pages/task/ListView.jsx | 5 +- src/pages/task/ProjectManage.jsx | 12 +- src/pages/task/ProjectView.jsx | 33 +++-- src/pages/task/TaskDetail.jsx | 50 +------- src/styles/globals.css | 17 +++ vite.config.js | 4 +- 17 files changed, 313 insertions(+), 289 deletions(-) create mode 100644 src/components/task/useColumnDrag.js delete mode 100644 src/components/task/useLongPressDrag.js delete mode 100644 src/hooks/useKeyboardInset.js create mode 100644 src/hooks/useKeyboardInsetWatcher.js diff --git a/docs/V0.2/任务模块/任务模块设计文档-框架设计.md b/docs/V0.2/任务模块/任务模块设计文档-框架设计.md index 0121204..543e3aa 100644 --- a/docs/V0.2/任务模块/任务模块设计文档-框架设计.md +++ b/docs/V0.2/任务模块/任务模块设计文档-框架设计.md @@ -423,3 +423,29 @@ POST /api/tasks/:id/copy { projectId: 目标项目 } | **交互验证** | 拖拽换列 / 降为子任务 / 拒绝场景 Toast;保留档位切换任务当场消失并重排 | | **兼容性** | Android WebView + 移动浏览器(T-NF-02);375 / 390 / 430 三档视口 | | **两视图一致** | 列表与看板可见条数一致(T-FL-11、T-FL-14);任一处修改另一处即时可见(T-UI-03) | + +--- + +## 7. 重点设计(2026-10-24 二轮评审) + +### 7.1 任务拖动方案(重做) + +旧版(手指跟随浮层 + 落点命中即转子任务)在真机上不可靠:浮层遮挡视线、touch 事件与滚动冲突、拖到卡片上误触转子任务。重做后的方案收敛为「**一个可靠手势 + 一个可靠菜单**」: + +| 场景 | 交互 | 实现 | +| --- | --- | --- | +| 看板换列 | 长按 400ms 激活(激活前移动 >10px 视为滚动,自动取消)→ 卡片**原位抬起**(不跟随手指、无浮层、无任务名)→ 指针划过列高亮 → 松手落列 | `useColumnDrag`:Pointer Events + `setPointerCapture`(触摸/鼠标统一、捕获后不丢事件、天然吞掉 pointercancel);`elementFromPoint` 命中 `[data-column-id]`;指针靠近容器边缘时 rAF 自动横滚,可拖到屏幕外的列 | +| 列表移动 | 长按 500ms → 弹「移动到列」ActionSheet(当前列打勾) | `TaskRow` 长按计时 → `ProjectView.openMoveMenu`;列表是纵向滚动流,不做自由拖动 | +| 转为子任务 | 不走拖拽(已移除「拖到卡片上」路径,误触率高) | 详情页「层级」区:选父任务转为子任务 / 一键转为顶层 | + +关键细节:卡片 `touch-action: pan-y`(纵向滚动留给浏览器,水平位移给拖动);激活与提交都以同一元素的 pointer 事件为准;`pointercancel` 与 `pointerup` 同样清理状态。换列后的状态仍由服务端按目标列绑定统一裁决(§2.2 不变)。 + +### 7.2 键盘遮挡统一方案 + +多个底部弹层(任务详情、信息块编辑、人员选择、拷贝)都出现过"输入框被输入法遮挡"。逐个组件接键盘逻辑不可维护,统一为**一个全局信号 + 一个 CSS 类**: + +1. **信号源**:`App` 级唯一监听器 `useKeyboardInsetWatcher`,同时监听 `visualViewport` 的 resize/scroll 与 `window.resize`(配合 `index.html` 的 `interactive-widget=resizes-content`,Chromium 系 WebView 键盘弹出会整体收缩布局视口);以"见过的最大视口高"为基线计算键盘插入量,写入 CSS 变量 **`--kb-inset`**(>90px 才视为键盘,过滤地址栏抖动)。 +2. **消费端**:底部弹层统一挂 **`.kb-safe`** 类 —— `padding-bottom: calc(安全区 + 20px + var(--kb-inset))`、`max-height` 用 `dvh` 随视口收缩并内部滚动。组件不再各自 import 键盘 hook、不再写内联 padding。 +3. **兜底**:长输入区(子任务输入、信息块 textarea)聚焦 300ms 后 `scrollIntoView({block:'center'})`,双保险。 + +新增弹层只要挂 `.kb-safe` 即自动获得键盘避让,无需再处理。 diff --git a/index.html b/index.html index 8faf2fb..fbabbb2 100644 --- a/index.html +++ b/index.html @@ -1,14 +1,14 @@ - - - - - - - - iBoard - 经济数据与策略 - - -
- - + + + + + + + + iBoard - 经济数据与策略 + + +
+ + \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index acf78be..33466da 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -8,6 +8,7 @@ import MePage from './pages/MePage.jsx' import CheckinPage from './pages/checkin/CheckinPage.jsx' import TaskPage from './pages/task/TaskPage.jsx' import { useAuth } from './auth/AuthContext.jsx' +import useKeyboardInsetWatcher from './hooks/useKeyboardInsetWatcher.js' // 默认落地页: 打卡 (V0.2 起打卡是第一个 Tab 且为默认选中) const HOME_PATH = '/checkin' @@ -30,6 +31,8 @@ function RequireAuth({ children }) { } export default function App() { + // 键盘插入量全局监听: 写入 --kb-inset, 弹层用 .kb-safe 类即可避让键盘 (重点设计 §7.2) + useKeyboardInsetWatcher() return ( } /> diff --git a/src/components/task/CopyTaskPopup.jsx b/src/components/task/CopyTaskPopup.jsx index a3ea884..93ff7ef 100644 --- a/src/components/task/CopyTaskPopup.jsx +++ b/src/components/task/CopyTaskPopup.jsx @@ -38,10 +38,7 @@ export default function CopyTaskPopup({ task, visible, onClose, onDone }) {
跨项目拷贝
「{task.title}」→ 选择目标项目;源任务保持不变
diff --git a/src/components/task/TaskRow.jsx b/src/components/task/TaskRow.jsx index 5458b25..48dc3ba 100644 --- a/src/components/task/TaskRow.jsx +++ b/src/components/task/TaskRow.jsx @@ -1,16 +1,45 @@ +import { useEffect, useRef } from 'react' import StatusDot from './StatusDot.jsx' // 列表视图的一行 (Todoist 风格, T-UI-01) +// 长按 500ms → onLongPress (移动到列菜单); 点击其他区域 → onClick (编辑) +const LONG_PRESS = 500 + export default function TaskRow({ - task, onToggle, onClick, onMore, indent = false, children, + task, onToggle, onClick, onMore, onLongPress, indent = false, children, }) { + const timer = useRef(null) + const moved = useRef(false) + + const clear = () => { if (timer.current) { clearTimeout(timer.current); timer.current = null } } + useEffect(() => () => clear(), []) + + const startPress = () => { + moved.current = false + clear() + if (!onLongPress) return + timer.current = setTimeout(() => { + if (!moved.current) { + if (navigator.vibrate) navigator.vibrate(15) + onLongPress(task) + } + }, LONG_PRESS) + } + const cancelPress = () => clear() + const markMoved = () => { moved.current = true; clear() } + const done = task.status === 'done' return ( <>
{ if (!moved.current) onClick?.(task) }} + onTouchStart={startPress} + onTouchMove={markMoved} + onTouchEnd={cancelPress} + onMouseDown={startPress} + onMouseUp={cancelPress} >
+
{title}
仅可选择当前项目的成员
diff --git a/src/components/task/useColumnDrag.js b/src/components/task/useColumnDrag.js new file mode 100644 index 0000000..7403804 --- /dev/null +++ b/src/components/task/useColumnDrag.js @@ -0,0 +1,114 @@ +import { useCallback, useEffect, useRef, useState } from 'react' + +// 看板卡片列拖动 (重点设计 §7.1, 2026-10-24 重做) +// 方案要点 (替代旧版"手指跟随浮层 + 落点即子任务"的不可靠实现): +// 1. Pointer Events + setPointerCapture —— 触摸/鼠标统一, 捕获后不丢事件 +// 2. 长按 400ms 激活 (先给滚动让路: 激活前移动 >10px 视为滚动并取消) +// 3. 激活后卡片原位抬起 (不跟随手指, 无浮层/无任务名), touch-action 由捕获保证 +// 4. 拖动中持续 elementFromPoint 命中 [data-column-id] 高亮目标列; +// 指针靠近容器左右边缘时自动横向滚动, 可拖到屏幕外的列 +// 5. 松手/取消(pointercancel 也要处理) → 落到目标列(≠原列)才提交; +// 拖到卡片上不再转子任务 —— 统一走详情页「转为子任务」 +export default function useColumnDrag({ onDrop, scrollSelector = '.board-wrap' } = {}) { + const [dragTask, setDragTask] = useState(null) // 拖动中的任务 (视觉态) + const [overCol, setOverCol] = useState(null) // 高亮的目标列 id + const st = useRef({ timer: null, active: false, startX: 0, startY: 0, task: null, autoScroll: 0, raf: 0 }) + + const stopAutoScroll = () => { + st.current.autoScroll = 0 + if (st.current.raf) { cancelAnimationFrame(st.current.raf); st.current.raf = 0 } + } + + const cleanup = useCallback(() => { + if (st.current.timer) { clearTimeout(st.current.timer); st.current.timer = null } + st.current.active = false + stopAutoScroll() + setDragTask(null) + setOverCol(null) + }, []) + + useEffect(() => () => cleanup(), [cleanup]) + + const hitColumn = (x, y) => { + const el = document.elementFromPoint(x, y) + const colEl = el?.closest?.('[data-column-id]') + return colEl ? Number(colEl.dataset.columnId) : null + } + + // 边缘自动滚动 (±36px 触发, 每帧 10px) + const autoScroll = (container, clientX) => { + if (!container) { stopAutoScroll(); return } + const r = container.getBoundingClientRect() + const edge = 36 + let dir = 0 + if (clientX < r.left + edge) dir = -1 + else if (clientX > r.right - edge) dir = 1 + st.current.autoScroll = dir + if (!dir) { stopAutoScroll(); return } + if (st.current.raf) return + const step = () => { + if (!st.current.autoScroll) return + container.scrollLeft += st.current.autoScroll * 10 + st.current.raf = requestAnimationFrame(step) + } + st.current.raf = requestAnimationFrame(step) + } + + const onPointerDown = (task) => (ev) => { + if (ev.button !== undefined && ev.button !== 0) return + st.current = { ...st.current, startX: ev.clientX, startY: ev.clientY, task, active: false } + // 长按计时: 期间移动超容差(用户在滚动)则取消 + st.current.timer = setTimeout(() => { + st.current.active = true + setDragTask(task) + if (navigator.vibrate) navigator.vibrate(15) + }, 400) + } + + const onPointerMove = (ev) => { + const s = st.current + if (!s.active) { + if (s.timer && (Math.abs(ev.clientX - s.startX) > 10 || Math.abs(ev.clientY - s.startY) > 10)) { + clearTimeout(s.timer); s.timer = null + } + return + } + ev.preventDefault() + const col = hitColumn(ev.clientX, ev.clientY) + setOverCol(col) + autoScroll(document.querySelector(scrollSelector), ev.clientX) + } + + const onPointerUp = (ev) => { + const s = st.current + if (s.active) { + const col = hitColumn(ev.clientX, ev.clientY) + if (col && s.task && col !== s.task.columnId) onDrop?.(s.task, col) + } + cleanup() + } + + /** 绑定到卡片: 需要卡片元素设置 touch-action: pan-y */ + const bind = useCallback((task) => ({ + onPointerDown: (ev) => { + // 捕获前先让事件正常冒泡(点击等); 只做长按计时 + onPointerDown(task)(ev) + // 捕获: 后续 move/up 都派发给该元素, 不丢 + try { ev.currentTarget.setPointerCapture(ev.pointerId) } catch { /* ignore */ } + const el = ev.currentTarget + const move = (e2) => onPointerMove(e2) + const up = (e2) => { + onPointerUp(e2) + el.removeEventListener('pointermove', move) + el.removeEventListener('pointerup', up) + el.removeEventListener('pointercancel', up) + } + el.addEventListener('pointermove', move) + el.addEventListener('pointerup', up) + el.addEventListener('pointercancel', up) + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + }), []) + + return { dragTask, overCol, bind, cancel: cleanup } +} diff --git a/src/components/task/useLongPressDrag.js b/src/components/task/useLongPressDrag.js deleted file mode 100644 index 4863473..0000000 --- a/src/components/task/useLongPressDrag.js +++ /dev/null @@ -1,99 +0,0 @@ -import { useCallback, useRef, useState } from 'react' - -// 长按拖拽 (T-UI-07, T-ST-12) -// 用法: const drag = useLongPressDrag({ onDrop }) -// - 长按 300ms 后进入拖拽态, 产生浮层数据 -// - 松手时用 elementFromPoint 找落点: [data-column-id] / [data-task-id] -// - 返回 { dragging, ghost, bind(task) } -const LONG_PRESS = 300 -const MOVE_TOLERANCE = 8 - -export default function useLongPressDrag({ onDropColumn, onDropTask, onReject } = {}) { - const [dragging, setDragging] = useState(null) // { task, x, y } - const timer = useRef(null) - const start = useRef(null) - const active = useRef(false) - - const cleanup = useCallback(() => { - if (timer.current) { clearTimeout(timer.current); timer.current = null } - start.current = null - active.current = false - document.removeEventListener('touchmove', onMove, { passive: false }) - document.removeEventListener('touchend', onEnd) - document.removeEventListener('mousemove', onMove) - document.removeEventListener('mouseup', onEnd) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - const pointOf = (ev) => { - const t = ev.touches?.[0] || ev.changedTouches?.[0] || ev - return { x: t.clientX, y: t.clientY } - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - function onMove(ev) { - if (!active.current) { - // 未进入拖拽前, 若移动超过容差则取消长按(用户想滚动) - if (start.current) { - const p = pointOf(ev) - if (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 p = pointOf(ev) - setDragging((d) => (d ? { ...d, x: p.x, y: p.y } : d)) - } - - function onEnd(ev) { - if (!active.current) { cleanup(); return } - const p = pointOf(ev) - const el = document.elementFromPoint(p.x, p.y) - const taskEl = el?.closest?.('[data-task-id]') - const colEl = el?.closest?.('[data-column-id]') - const task = dragging?.task - cleanup() - setDragging(null) - if (!task) return - - if (taskEl) { - const targetId = Number(taskEl.dataset.taskId) - if (targetId && targetId !== task.id) onDropTask?.(task, targetId) - return - } - if (colEl) { - const columnId = Number(colEl.dataset.columnId) - if (columnId && columnId !== task.columnId) onDropColumn?.(task, columnId) - return - } - onReject?.('未放到有效位置') - } - - const bind = useCallback((task) => { - const onStart = (ev) => { - if (ev.button && ev.button !== 0) return - const p = pointOf(ev) - start.current = p - setDragging(null) - active.current = false - timer.current = setTimeout(() => { - active.current = true - setDragging({ task, x: p.x, y: p.y }) - 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) - } - return { - onTouchStart: onStart, - onMouseDown: onStart, - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - return { dragging, bind, cancel: () => { cleanup(); setDragging(null) } } -} diff --git a/src/hooks/useKeyboardInset.js b/src/hooks/useKeyboardInset.js deleted file mode 100644 index 096bc21..0000000 --- a/src/hooks/useKeyboardInset.js +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect, useState } from 'react' - -// 键盘弹出时的"插入量": window.innerHeight 与 visualViewport.height 的差 -// Android WebView 键盘弹出时 visualViewport 变小; 用它把底部弹层的内容抬到键盘上方, -// 避免输入框(如任务详情里的"添加子任务")被键盘遮挡。 -// 阈值 90px 过滤掉地址栏收起之类的轻微抖动; 无 visualViewport 时恒为 0 (iOS 原生键盘通常整体缩放, 无需处理)。 -export default function useKeyboardInset() { - const [inset, setInset] = useState(0) - - useEffect(() => { - const vv = window.visualViewport - if (!vv) return - const onChange = () => { - const diff = window.innerHeight - vv.height - vv.offsetTop - setInset(diff > 90 ? Math.round(diff) : 0) - } - vv.addEventListener('resize', onChange) - vv.addEventListener('scroll', onChange) - onChange() - return () => { - vv.removeEventListener('resize', onChange) - vv.removeEventListener('scroll', onChange) - } - }, []) - - return inset -} diff --git a/src/hooks/useKeyboardInsetWatcher.js b/src/hooks/useKeyboardInsetWatcher.js new file mode 100644 index 0000000..71bf9a3 --- /dev/null +++ b/src/hooks/useKeyboardInsetWatcher.js @@ -0,0 +1,35 @@ +import { useEffect } from 'react' + +// 键盘遮挡统一方案 (重点设计 §7.2): +// 全局唯一监听器, 把键盘插入量写入 CSS 变量 --kb-inset (0 或 px 字符串), +// 任何底部弹层只要挂 .kb-safe 类即可被抬到键盘上方, 组件不再各自接键盘逻辑。 +// 信号源 (双保险): +// 1) visualViewport 的 resize/scroll —— Android WebView 键盘弹出时它先变小 +// 2) window resize —— 配合 index.html 的 interactive-widget=resizes-content, +// Chromium 系 WebView 会整体收缩布局视口 +// 基线: 记录见过的最大视口高 (键盘收起时), 避免"弹出键盘后基线被拉低"的漂移。 +export default function useKeyboardInsetWatcher() { + useEffect(() => { + const root = document.documentElement + const vv = window.visualViewport + let maxH = window.innerHeight + + const calc = () => { + const h = vv ? vv.height : window.innerHeight + if (h > maxH) maxH = h + const inset = Math.max(0, Math.round(maxH - h - (vv ? vv.offsetTop : 0))) + root.style.setProperty('--kb-inset', inset > 90 ? inset + 'px' : '0px') + } + + calc() + window.addEventListener('resize', calc) + vv?.addEventListener('resize', calc) + vv?.addEventListener('scroll', calc) + return () => { + window.removeEventListener('resize', calc) + vv?.removeEventListener('resize', calc) + vv?.removeEventListener('scroll', calc) + root.style.setProperty('--kb-inset', '0px') + } + }, []) +} diff --git a/src/pages/task/BoardView.jsx b/src/pages/task/BoardView.jsx index 0f31c09..2506fc1 100644 --- a/src/pages/task/BoardView.jsx +++ b/src/pages/task/BoardView.jsx @@ -1,78 +1,57 @@ -import { useState } from 'react' -import { Toast } from 'antd-mobile' import TaskCard from '../../components/task/TaskCard.jsx' -import useLongPressDrag from '../../components/task/useLongPressDrag.js' +import useColumnDrag from '../../components/task/useColumnDrag.js' -// 看板视图 (T-BD-01, T-UI-07, T-ST-06/12) -// 列横向滚动; 长按卡片拖拽换列 / 降为子任务 +// 看板视图 (T-BD-01, T-UI-07) +// 拖动 = 长按 400ms 激活, 卡片原位抬起, 指针划过列高亮, 松手落列 (重点设计 §7.1) +// 不做"拖到卡片上转子任务" —— 统一走详情页「转为子任务」 export default function BoardView({ - tasks, columns, onMoveColumn, onMakeSubtask, onOpen, onToggleSub, onToggle, onMore, onQuickAdd, + tasks, columns, onMoveColumn, onOpen, onToggleSub, onToggle, onMore, onQuickAdd, }) { - const [overCol, setOverCol] = useState(null) - - const drag = useLongPressDrag({ - onDropColumn: (task, columnId) => { setOverCol(null); onMoveColumn?.(task, columnId) }, - onDropTask: (task, parentId) => { setOverCol(null); onMakeSubtask?.(task, parentId) }, - onReject: (msg) => { setOverCol(null); Toast.show({ content: msg }) }, - }) + const drag = useColumnDrag({ onDrop: onMoveColumn }) const ordered = [...(columns || [])].sort((a, b) => a.sortOrder - b.sortOrder) return ( - <> -
- {ordered.map((col) => { - const list = (tasks || []).filter((t) => t.columnId === col.id) - return ( -
setOverCol(col.id)} - onDragLeave={() => setOverCol((c) => (c === col.id ? null : c))} - > -
- {col.name} - {list.length} - {col.boundStatus ? ( - - ↔ {col.boundStatus === 'done' ? '完成' : col.boundStatus === 'doing' ? '执行中' : '未开始'} - - ) : ( - 未绑定 - )} -
- -
- {list.map((t) => ( - { if (!drag.dragging) onOpen?.(t) }} - onToggleSub={onToggleSub} - onToggle={onToggle} - onMore={onMore} - /> - ))} - {list.length === 0 &&
暂无任务
} -
onQuickAdd?.(col.id)}>+ 添加任务
-
+
+ {ordered.map((col) => { + const list = (tasks || []).filter((t) => t.columnId === col.id) + return ( +
+
+ {col.name} + {list.length} + {col.boundStatus ? ( + + ↔ {col.boundStatus === 'done' ? '完成' : col.boundStatus === 'doing' ? '执行中' : '未开始'} + + ) : ( + 未绑定 + )}
- ) - })} -
- {drag.dragging && ( -
- {drag.dragging.task.title} -
- )} - +
+ {list.map((t) => ( + { if (!drag.dragTask) onOpen?.(t) }} + onToggleSub={onToggleSub} + onToggle={onToggle} + onMore={onMore} + /> + ))} + {list.length === 0 &&
暂无任务
} +
onQuickAdd?.(col.id)}>+ 添加任务
+
+
+ ) + })} +
) } diff --git a/src/pages/task/ListView.jsx b/src/pages/task/ListView.jsx index 48db7c3..5851043 100644 --- a/src/pages/task/ListView.jsx +++ b/src/pages/task/ListView.jsx @@ -5,7 +5,7 @@ import TaskRow from '../../components/task/TaskRow.jsx' // 按看板列分组展示; 子任务缩进 + 折叠展开 // 子任务的添加只在任务详情页完成 (或把别的任务转为子任务), 列表页不放入口 export default function ListView({ - tasks, columns, onToggle, onOpen, onMore, + tasks, columns, onToggle, onOpen, onMore, onLongPress, }) { const [collapsed, setCollapsed] = useState({}) @@ -48,11 +48,12 @@ export default function ListView({ onToggle={onToggle} onClick={() => onOpen(t)} onMore={onMore} + onLongPress={onLongPress} /> {kids.length > 0 && ( <> {!isCollapsed && kids.map((c) => ( - onOpen(c)} onMore={onMore} /> + onOpen(c)} onMore={onMore} onLongPress={onLongPress} /> ))}
{ if (view !== 'main') setView('main'); else onClose() }) - // 键盘弹出插入量: 信息块编辑弹层用 - const kb = useKeyboardInset() if (view === 'columns') { return ( @@ -233,13 +230,7 @@ export default function ProjectManage({ project, onClose, onChanged, onExited }) visible={!!noteEdit} onMaskClick={() => setNoteEdit(null)} position="bottom" - bodyStyle={{ - borderTopLeftRadius: 12, - borderTopRightRadius: 12, - maxHeight: '86vh', - overflowY: 'auto', - padding: `16px 16px calc(env(safe-area-inset-bottom, 0px) + 20px + ${kb}px)`, - }} + className="kb-safe" >
{noteEdit?.index >= 0 ? '编辑信息块' : '添加信息块'}
用于记录 Readme 等项目相关长文本,每块独立编辑
@@ -258,6 +249,7 @@ export default function ProjectManage({ project, onClose, onChanged, onExited })