fix(V0.2): 任务模块 4 个 bug —— 覆盖层安全区/返回键/项目列表排序/键盘遮挡
Docker Build and Push / build-image (push) Successful in 4m17s

1. 项目管理等 cs-page 覆盖层与系统状态栏重合:
   .cs-page 补 padding-top: var(--safe-top) (复用 main.jsx 实测校准的变量)

2. 项目管理按返回键跑到上一个 Tab:
   新增 useOverlayBack —— 覆盖层打开时 pushState 哨兵, 返回键先关覆盖层
   应用于 项目管理(内层视图先回主区) 与 任务详情弹层

3. 更多项目页:
   - 头部补「+ 新建」入口
   - 项目列表支持长按拖动排序(纵向), 顺序存设备端, 与首屏最近项目共用 recentOrder
   - 顺带修复首屏 RecentProjects 拖动排序的闭包缺陷(监听器读到旧下标, 首次移动
     会 splice(-1)); 两处均改为 ref 取当前下标

4. 任务详情输入子任务被键盘遮挡:
   新增 useKeyboardInset (visualViewport 差值), 键盘弹出时抬升弹层内容并收缩
   maxHeight; 输入框聚焦后再 scrollIntoView 到可视区中部 (双保险)

已验证: 99 文件语法 0 错误、相对导入 0 缺失
This commit is contained in:
jif2.zhang
2026-10-24 21:09:19 +08:00
parent 0cdbfb434f
commit 1fd7522fa6
7 changed files with 215 additions and 20 deletions
+27
View File
@@ -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
}
+24
View File
@@ -0,0 +1,24 @@
import { useEffect } from 'react'
// 弹层 / 全屏覆盖层打开时拦截返回键: 先关弹层, 而不是离开当前路由 (bug: 返回跑到上一个 Tab)
// 原理: 打开时 pushState 一条哨兵记录 (URL 不变), 返回键先消费它;
// popstate => 视为"关闭弹层"。正常关闭 (点 X / 遮罩) 时在 cleanup 里 history.back() 撤掉哨兵。
// 注意: close 必须把 open 置 false, 否则 cleanup 不会执行、哨兵残留。
export default function useOverlayBack(open, close) {
useEffect(() => {
if (!open) return
let closedByBack = false
window.history.pushState({ __overlay: true }, '')
const onPop = () => {
closedByBack = true
close()
}
window.addEventListener('popstate', onPop)
return () => {
window.removeEventListener('popstate', onPop)
// 正常关闭: 撤销哨兵 (不产生可见的后退, 因为 URL 未变)
if (!closedByBack) window.history.back()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open])
}