From d6ca7573f37231686523e16267b57ce9a7f3a673 Mon Sep 17 00:00:00 2001 From: "jif2.zhang" Date: Sat, 24 Oct 2026 23:24:31 +0800 Subject: [PATCH 01/29] =?UTF-8?q?fix(V0.2):=20=E4=B8=89=E8=BD=AE=E5=8F=8D?= =?UTF-8?q?=E9=A6=88=205=20=E9=A1=B9=20=E2=80=94=E2=80=94=20=E5=9B=9E?= =?UTF-8?q?=E6=BB=9A=E9=94=AE=E7=9B=98=E6=96=B9=E6=A1=88=20/=20=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=8D=A1=E7=89=87=E6=8B=96=E5=8A=A8=20/=20=E7=9C=8B?= =?UTF-8?q?=E6=9D=BF=E9=98=B2=E8=AF=AF=E8=A7=A6=20/=20=E6=A8=AA=E6=BB=91?= =?UTF-8?q?=E6=95=B4=E5=B1=8F=20/=20=E8=AF=A6=E6=83=85=E9=A1=B5=E6=81=A2?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 回滚键盘遮挡统一方案 (真机测试不通过, 需求搁置): 恢复 TaskDetail/ UserPicker/CopyTaskPopup/ProjectManage 与 useKeyboardInset 到 9fea1a5^, 删除 useKeyboardInsetWatcher / .kb-safe / interactive-widget=resizes-content 2. 任务详情页样式随 1 一并恢复提交前状态 (含底部拷贝/删除按钮) 3. 列表视图换列改为卡片拖动: 新 useRowDrag (长按 400ms 行抬起, 左右 80px/列步进, 行尾徽标显示目标列, 松手落列), 移除长按 ActionSheet —— 旧长按与复选框/行点击互相误触, 抬手合成 click 落在控件上触发状态切换 4. 看板拖动防误触: 任务区禁长按文字选择与系统菜单 (输入框除外); pointercancel 只放弃不提交; 拖动结束 350ms 内抑制 click 误开详情; 状态点/⋯/子任务行 pointerdown 不冒泡, 不发起拖动 5. 看板横滑扩展到整屏: .page-transition 补 height:100% 打通 app-main → page-with-navbar 百分比链, board-wrap 撑满剩余高度 已验证: kb-safe/Watcher/onLongPress 残留引用清零, 逐文件 diff 审阅通过; 本机无 Node 运行时, 未跑 vite build --- index.html | 2 +- src/App.jsx | 3 - src/components/task/CopyTaskPopup.jsx | 5 +- src/components/task/TaskCard.jsx | 3 + src/components/task/TaskRow.jsx | 51 +++++--------- src/components/task/UserPicker.jsx | 2 +- src/components/task/useColumnDrag.js | 61 ++++++++++------- src/components/task/useRowDrag.js | 98 +++++++++++++++++++++++++++ src/hooks/useKeyboardInset.js | 27 ++++++++ src/hooks/useKeyboardInsetWatcher.js | 35 ---------- src/pages/task/BoardView.jsx | 2 +- src/pages/task/ListView.jsx | 15 ++-- src/pages/task/ProjectManage.jsx | 12 +++- src/pages/task/ProjectView.jsx | 17 +---- src/pages/task/TaskDetail.jsx | 50 ++++++++++++-- src/styles/globals.css | 50 ++++++++------ 16 files changed, 285 insertions(+), 148 deletions(-) create mode 100644 src/components/task/useRowDrag.js create mode 100644 src/hooks/useKeyboardInset.js delete mode 100644 src/hooks/useKeyboardInsetWatcher.js diff --git a/index.html b/index.html index fbabbb2..9d8210e 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - + iBoard - 经济数据与策略 diff --git a/src/App.jsx b/src/App.jsx index 33466da..acf78be 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -8,7 +8,6 @@ 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' @@ -31,8 +30,6 @@ 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 93ff7ef..a3ea884 100644 --- a/src/components/task/CopyTaskPopup.jsx +++ b/src/components/task/CopyTaskPopup.jsx @@ -38,7 +38,10 @@ export default function CopyTaskPopup({ task, visible, onClose, onDone }) {
跨项目拷贝
「{task.title}」→ 选择目标项目;源任务保持不变
diff --git a/src/components/task/TaskCard.jsx b/src/components/task/TaskCard.jsx index 23a9ecb..c7ed88a 100644 --- a/src/components/task/TaskCard.jsx +++ b/src/components/task/TaskCard.jsx @@ -25,6 +25,7 @@ export default function TaskCard({ { e.stopPropagation(); onToggle?.(task) }} + onPointerDown={(e) => e.stopPropagation()} title="点按切换完成" > @@ -42,6 +43,7 @@ export default function TaskCard({ { e.stopPropagation(); onMore(task) }} + onPointerDown={(e) => e.stopPropagation()} >⋯ )} @@ -51,6 +53,7 @@ export default function TaskCard({ key={c.id} className={`bsub${c.status === 'done' ? ' done' : ''}`} onClick={(e) => { e.stopPropagation(); onToggleSub?.(c) }} + onPointerDown={(e) => e.stopPropagation()} > {c.title} diff --git a/src/components/task/TaskRow.jsx b/src/components/task/TaskRow.jsx index 48dc3ba..c115a83 100644 --- a/src/components/task/TaskRow.jsx +++ b/src/components/task/TaskRow.jsx @@ -1,49 +1,25 @@ -import { useEffect, useRef } from 'react' import StatusDot from './StatusDot.jsx' // 列表视图的一行 (Todoist 风格, T-UI-01) -// 长按 500ms → onLongPress (移动到列菜单); 点击其他区域 → onClick (编辑) -const LONG_PRESS = 500 - +// 点击行体 → 打开详情; 长按 400ms 后左右拖动换列 (bindDrag 来自 useRowDrag) +// 复选框 / ⋯ 是独立控件: pointerdown 不冒泡, 不发起拖动, +// 避免长按行后抬手时合成的 click 落在控件上误触状态切换 export default function TaskRow({ - task, onToggle, onClick, onMore, onLongPress, indent = false, children, + task, onToggle, onClick, onMore, bindDrag, dragTarget, 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} + onClick={onClick} + {...(!indent && bindDrag ? bindDrag(task) : {})} >
{ e.stopPropagation(); onToggle?.(task) }} + onPointerDown={(e) => e.stopPropagation()} > @@ -72,7 +48,16 @@ export default function TaskRow({
- { e.stopPropagation(); onMore?.(task) }}>⋯ + {dragTarget && ( + + → {dragTarget.name} + + )} + { e.stopPropagation(); onMore?.(task) }} + onPointerDown={(e) => e.stopPropagation()} + >⋯
)} diff --git a/src/components/task/UserPicker.jsx b/src/components/task/UserPicker.jsx index 0ef053d..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/components/task/useColumnDrag.js b/src/components/task/useColumnDrag.js index 7403804..320df9b 100644 --- a/src/components/task/useColumnDrag.js +++ b/src/components/task/useColumnDrag.js @@ -1,18 +1,21 @@ import { useCallback, useEffect, useRef, useState } from 'react' -// 看板卡片列拖动 (重点设计 §7.1, 2026-10-24 重做) -// 方案要点 (替代旧版"手指跟随浮层 + 落点即子任务"的不可靠实现): +// 看板卡片列拖动 (重点设计 §7.1) +// 方案要点: // 1. Pointer Events + setPointerCapture —— 触摸/鼠标统一, 捕获后不丢事件 -// 2. 长按 400ms 激活 (先给滚动让路: 激活前移动 >10px 视为滚动并取消) -// 3. 激活后卡片原位抬起 (不跟随手指, 无浮层/无任务名), touch-action 由捕获保证 +// 2. 长按 400ms 激活 (激活前移动 >10px 视为滚动, 取消本次拖动) +// 3. 激活后卡片原位抬起 (不跟随手指, 无浮层/无任务名) // 4. 拖动中持续 elementFromPoint 命中 [data-column-id] 高亮目标列; -// 指针靠近容器左右边缘时自动横向滚动, 可拖到屏幕外的列 -// 5. 松手/取消(pointercancel 也要处理) → 落到目标列(≠原列)才提交; -// 拖到卡片上不再转子任务 —— 统一走详情页「转为子任务」 +// 指针靠近容器左右边缘时 rAF 自动横滚, 可拖到屏幕外的列 +// 5. 松手落到目标列 (≠原列) 才提交; pointercancel (浏览器接管手势, +// 如触发纵向滚动) 只放弃不提交 +// 6. 防误触: 卡片禁长按系统菜单(contextmenu); 拖动结束后 350ms 内的 +// click 视为拖动余波, 不打开详情 (justEnded) 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 lastEnd = useRef(0) // 拖动结束时刻 const stopAutoScroll = () => { st.current.autoScroll = 0 @@ -29,6 +32,9 @@ export default function useColumnDrag({ onDrop, scrollSelector = '.board-wrap' } useEffect(() => () => cleanup(), [cleanup]) + /** 拖动刚结束 (350ms 内): 抑制松手合成的 click 误开详情页 */ + const justEnded = useCallback(() => Date.now() - lastEnd.current < 350, []) + const hitColumn = (x, y) => { const el = document.elementFromPoint(x, y) const colEl = el?.closest?.('[data-column-id]') @@ -54,17 +60,6 @@ export default function useColumnDrag({ onDrop, scrollSelector = '.board-wrap' } 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) { @@ -82,6 +77,7 @@ export default function useColumnDrag({ onDrop, scrollSelector = '.board-wrap' } const onPointerUp = (ev) => { const s = st.current if (s.active) { + lastEnd.current = Date.now() const col = hitColumn(ev.clientX, ev.clientY) if (col && s.task && col !== s.task.columnId) onDrop?.(s.task, col) } @@ -91,24 +87,37 @@ export default function useColumnDrag({ onDrop, scrollSelector = '.board-wrap' } /** 绑定到卡片: 需要卡片元素设置 touch-action: pan-y */ const bind = useCallback((task) => ({ onPointerDown: (ev) => { - // 捕获前先让事件正常冒泡(点击等); 只做长按计时 - onPointerDown(task)(ev) - // 捕获: 后续 move/up 都派发给该元素, 不丢 + 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) + // 捕获: 后续 move/up/cancel 都派发给该元素, 不丢 try { ev.currentTarget.setPointerCapture(ev.pointerId) } catch { /* ignore */ } const el = ev.currentTarget const move = (e2) => onPointerMove(e2) - const up = (e2) => { - onPointerUp(e2) + const up = (e2) => { onPointerUp(e2); detach() } + const cancel = () => { + if (st.current.active) lastEnd.current = Date.now() + cleanup() + detach() + } + const detach = () => { el.removeEventListener('pointermove', move) el.removeEventListener('pointerup', up) - el.removeEventListener('pointercancel', up) + el.removeEventListener('pointercancel', cancel) } el.addEventListener('pointermove', move) el.addEventListener('pointerup', up) - el.addEventListener('pointercancel', up) + el.addEventListener('pointercancel', cancel) }, + // 长按不弹系统菜单 (配合 CSS user-select:none, 防长按变文字选择) + onContextMenu: (ev) => ev.preventDefault(), // eslint-disable-next-line react-hooks/exhaustive-deps }), []) - return { dragTask, overCol, bind, cancel: cleanup } + return { dragTask, overCol, bind, cancel: cleanup, justEnded } } diff --git a/src/components/task/useRowDrag.js b/src/components/task/useRowDrag.js new file mode 100644 index 0000000..3cad767 --- /dev/null +++ b/src/components/task/useRowDrag.js @@ -0,0 +1,98 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' + +// 列表视图卡片拖动换列 (2026-10-24, 替代「长按 → ActionSheet」: +// 旧方案长按与复选框/行点击互相误触, 且菜单弹出时手指还按在屏上, 体验割裂) +// 手势与看板一致: 长按 400ms 行原位抬起(震动), 之后左右拖动; +// 水平位移按 80px/列 步进切换目标列 (顺序同列表分组), 行尾实时显示 +// 目标列名徽标, 松手落到目标列 (≠当前列才提交)。 +// touch-action: pan-y —— 横向位移归拖动, 纵向仍归列表滚动; +// 激活后浏览器若接管纵向滚动 (pointercancel) 只放弃不提交。 +// 注: 列表只展示有任务的列, 空列拖不进去 (用看板视图可拖空列)。 +const STEP = 80 + +export default function useRowDrag({ columns, onDrop }) { + // { task, fromIdx, toIdx }: 拖动中的行 + 当前目标列下标 (徽标渲染) + const [drag, setDrag] = useState(null) + const st = useRef({ timer: null, active: false, startX: 0, startY: 0, task: null, fromIdx: 0, toIdx: 0 }) + const lastEnd = useRef(0) // 拖动结束时刻 + + // 目标列顺序 = 列表分组顺序 (sortOrder) + const ordered = useMemo( + () => [...(columns || [])].sort((a, b) => a.sortOrder - b.sortOrder), + [columns], + ) + + const cleanup = useCallback(() => { + if (st.current.timer) { clearTimeout(st.current.timer); st.current.timer = null } + st.current.active = false + setDrag(null) + }, []) + + useEffect(() => () => cleanup(), [cleanup]) + + /** 拖动刚结束 (350ms 内): 抑制松手合成的 click 误开详情页 */ + const justEnded = useCallback(() => Date.now() - lastEnd.current < 350, []) + + const onMove = (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 to = Math.max(0, Math.min(ordered.length - 1, s.fromIdx + Math.round((ev.clientX - s.startX) / STEP))) + if (to !== s.toIdx) { + s.toIdx = to + setDrag({ task: s.task, fromIdx: s.fromIdx, toIdx: to }) + } + } + + const onUp = () => { + const s = st.current + if (s.active && s.task) { + lastEnd.current = Date.now() + const target = ordered[s.toIdx] + if (target && target.id !== s.task.columnId) onDrop?.(s.task, target.id) + } + cleanup() + } + + /** 绑定到列表行: 行元素需设置 touch-action: pan-y */ + const bind = useCallback((task) => ({ + onPointerDown: (ev) => { + if (ev.button !== undefined && ev.button !== 0) return + const fromIdx = ordered.findIndex((c) => c.id === task.columnId) + if (fromIdx < 0) return // 不在任何列 (如「其他」组) 的任务不支持拖动 + st.current = { ...st.current, startX: ev.clientX, startY: ev.clientY, task, fromIdx, toIdx: fromIdx, active: false } + st.current.timer = setTimeout(() => { + st.current.active = true + if (navigator.vibrate) navigator.vibrate(15) + setDrag({ task, fromIdx, toIdx: fromIdx }) + }, 400) + try { ev.currentTarget.setPointerCapture(ev.pointerId) } catch { /* ignore */ } + const el = ev.currentTarget + const move = (e2) => onMove(e2) + const up = () => { onUp(); detach() } + const cancel = () => { + if (st.current.active) lastEnd.current = Date.now() + cleanup() + detach() + } + const detach = () => { + el.removeEventListener('pointermove', move) + el.removeEventListener('pointerup', up) + el.removeEventListener('pointercancel', cancel) + } + el.addEventListener('pointermove', move) + el.addEventListener('pointerup', up) + el.addEventListener('pointercancel', cancel) + }, + // 长按不弹系统菜单 (配合 CSS user-select:none, 防长按变文字选择) + onContextMenu: (ev) => ev.preventDefault(), + // eslint-disable-next-line react-hooks/exhaustive-deps + }), [ordered, onDrop]) + + return { drag, bind, cancel: cleanup, justEnded } +} diff --git a/src/hooks/useKeyboardInset.js b/src/hooks/useKeyboardInset.js new file mode 100644 index 0000000..096bc21 --- /dev/null +++ b/src/hooks/useKeyboardInset.js @@ -0,0 +1,27 @@ +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 deleted file mode 100644 index 71bf9a3..0000000 --- a/src/hooks/useKeyboardInsetWatcher.js +++ /dev/null @@ -1,35 +0,0 @@ -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 2506fc1..5ca3b3d 100644 --- a/src/pages/task/BoardView.jsx +++ b/src/pages/task/BoardView.jsx @@ -40,7 +40,7 @@ export default function BoardView({ task={t} dragging={drag.dragTask?.id === t.id} bindDrag={drag.bind} - onClick={() => { if (!drag.dragTask) onOpen?.(t) }} + onClick={() => { if (!drag.dragTask && !drag.justEnded()) onOpen?.(t) }} onToggleSub={onToggleSub} onToggle={onToggle} onMore={onMore} diff --git a/src/pages/task/ListView.jsx b/src/pages/task/ListView.jsx index 5851043..4b3c4d3 100644 --- a/src/pages/task/ListView.jsx +++ b/src/pages/task/ListView.jsx @@ -1,13 +1,16 @@ import { useState } from 'react' import TaskRow from '../../components/task/TaskRow.jsx' +import useRowDrag from '../../components/task/useRowDrag.js' // 列表视图 (T-VW-01, T-ST-04/05) // 按看板列分组展示; 子任务缩进 + 折叠展开 -// 子任务的添加只在任务详情页完成 (或把别的任务转为子任务), 列表页不放入口 +// 换列手势与看板一致: 长按 400ms 行抬起, 左右拖动选列, 松手落列 +// (列表只展示有任务的列, 空列需到看板视图拖入) export default function ListView({ - tasks, columns, onToggle, onOpen, onMore, onLongPress, + tasks, columns, onToggle, onOpen, onMore, onMoveColumn, }) { const [collapsed, setCollapsed] = useState({}) + const drag = useRowDrag({ columns, onDrop: onMoveColumn }) if (!tasks || tasks.length === 0) { return ( @@ -41,19 +44,21 @@ export default function ListView({ {g.list.map((t) => { const kids = t.children || [] const isCollapsed = !!collapsed[t.id] + const dragging = drag.drag?.task?.id === t.id ? drag.drag : null return (
onOpen(t)} + onClick={() => { if (!drag.justEnded()) onOpen(t) }} onMore={onMore} - onLongPress={onLongPress} + bindDrag={drag.bind} + dragTarget={dragging ? orderedCols[dragging.toIdx] : null} /> {kids.length > 0 && ( <> {!isCollapsed && kids.map((c) => ( - onOpen(c)} onMore={onMore} onLongPress={onLongPress} /> + onOpen(c)} onMore={onMore} /> ))}
{ if (view !== 'main') setView('main'); else onClose() }) + // 键盘弹出插入量: 信息块编辑弹层用 + const kb = useKeyboardInset() if (view === 'columns') { return ( @@ -230,7 +233,13 @@ export default function ProjectManage({ project, onClose, onChanged, onExited }) visible={!!noteEdit} onMaskClick={() => setNoteEdit(null)} position="bottom" - className="kb-safe" + bodyStyle={{ + borderTopLeftRadius: 12, + borderTopRightRadius: 12, + maxHeight: '86vh', + overflowY: 'auto', + padding: `16px 16px calc(env(safe-area-inset-bottom, 0px) + 20px + ${kb}px)`, + }} >
{noteEdit?.index >= 0 ? '编辑信息块' : '添加信息块'}
用于记录 Readme 等项目相关长文本,每块独立编辑
@@ -249,7 +258,6 @@ export default function ProjectManage({ project, onClose, onChanged, onExited })