fix(V0.2): 三轮反馈 5 项 —— 回滚键盘方案 / 列表卡片拖动 / 看板防误触 / 横滑整屏 / 详情页恢复
Docker Build and Push / build-image (push) Successful in 4m9s

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
This commit is contained in:
jif2.zhang
2026-10-24 23:24:31 +08:00
parent 9fea1a5a3a
commit d6ca7573f3
16 changed files with 285 additions and 148 deletions
+1 -1
View File
@@ -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}
+10 -5
View File
@@ -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 (
<div key={t.id}>
<TaskRow
task={t}
onToggle={onToggle}
onClick={() => 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) => (
<TaskRow key={c.id} task={c} indent onToggle={onToggle} onClick={() => onOpen(c)} onMore={onMore} onLongPress={onLongPress} />
<TaskRow key={c.id} task={c} indent onToggle={onToggle} onClick={() => onOpen(c)} onMore={onMore} />
))}
<div
className="collapse"
+10 -2
View File
@@ -2,6 +2,7 @@ import { useState } from 'react'
import { Dialog, Input, Popup, Toast } from 'antd-mobile'
import { projectApi } from '../../api/task.js'
import { useAuth } from '../../auth/AuthContext.jsx'
import useKeyboardInset from '../../hooks/useKeyboardInset.js'
import useOverlayBack from '../../hooks/useOverlayBack.js'
import ColumnSettings from './ColumnSettings.jsx'
import ProjectMembers from './ProjectMembers.jsx'
@@ -23,6 +24,8 @@ export default function ProjectManage({ project, onClose, onChanged, onExited })
// 返回键: 内层视图(列设置/成员)先回主区, 主区再关闭整个项目管理
useOverlayBack(true, () => { 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)`,
}}
>
<div className="sheet-title">{noteEdit?.index >= 0 ? '编辑信息块' : '添加信息块'}</div>
<div className="sheet-sub">用于记录 Readme 等项目相关长文本,每块独立编辑</div>
@@ -249,7 +258,6 @@ export default function ProjectManage({ project, onClose, onChanged, onExited })
<textarea
className="inp"
rows={10}
onFocus={(e) => { setTimeout(() => e.target.scrollIntoView({ block: 'center', behavior: 'smooth' }), 300) }}
placeholder="支持多行长文本…"
value={noteEdit?.content || ''}
onChange={(e) => setNoteEdit((p) => ({ ...(p || {}), content: e.target.value }))}
+1 -16
View File
@@ -127,21 +127,6 @@ export default function ProjectView({ projectId, onProjectChanged, onProjectExit
}
}
/** 长按任务 → 移动到列 (重点设计 §7.1: 列表页不做自由拖动, 菜单可靠) */
function openMoveMenu(task) {
ActionSheet.show({
actions: (columns || []).map((c) => ({
text: c.name + (c.id === task.columnId ? ' ✓' : ''),
key: String(c.id),
})),
cancelText: '取消',
onAction: (item) => {
const columnId = Number(item.key)
if (columnId !== task.columnId) moveToColumn(task, columnId)
},
})
}
/** 任务 ⋯ 菜单: 编辑 / 拷贝到其他项目 / 删除 */
function openTaskMenu(task) {
ActionSheet.show({
@@ -236,7 +221,7 @@ export default function ProjectView({ projectId, onProjectChanged, onProjectExit
onToggle={toggleDone}
onOpen={(t) => setOpenTask(t)}
onMore={openTaskMenu}
onLongPress={openMoveMenu}
onMoveColumn={moveToColumn}
/>
) : (
<BoardView
+46 -4
View File
@@ -1,9 +1,11 @@
import { useEffect, useState } from 'react'
import { ActionSheet, Popup, Toast } from 'antd-mobile'
import { ActionSheet, Dialog, Popup, Toast } from 'antd-mobile'
import { taskApi } from '../../api/task.js'
import { STATUS_OPTIONS, STATUS_LABEL } from './date.js'
import UserPicker from '../../components/task/UserPicker.jsx'
import StatusDot from '../../components/task/StatusDot.jsx'
import CopyTaskPopup from '../../components/task/CopyTaskPopup.jsx'
import useKeyboardInset from '../../hooks/useKeyboardInset.js'
import useOverlayBack from '../../hooks/useOverlayBack.js'
// 任务详情 (T-TK-*, T-ST-03, T-AS-05)
@@ -23,8 +25,11 @@ export default function TaskDetail({ task, columns, members, tasks, onClose, onS
const [newSub, setNewSub] = useState('')
const [saving, setSaving] = useState(false)
const [picker, setPicker] = useState(null) // 'assignee' | 'members'
const [copyOpen, setCopyOpen] = useState(false)
const [parentPick, setParentPick] = useState(false) // 转为子任务: 选择父任务
// 键盘弹出插入量: 抬升弹层内容, 输入框不被遮挡
const kb = useKeyboardInset()
// 返回键: 先关详情弹层, 不离开任务页
useOverlayBack(true, onClose)
@@ -127,12 +132,35 @@ export default function TaskDetail({ task, columns, members, tasks, onClose, onS
}
}
async function removeTask() {
const ok = await Dialog.confirm({
content: subs.length
? `删除该任务?将一并删除 ${subs.length} 条子任务。`
: '删除该任务?',
confirmText: '删除',
})
if (!ok) return
try {
await taskApi.remove(task.id)
Toast.show({ content: '已删除' })
onSaved?.()
} catch (e) {
Toast.show({ content: e.message })
}
}
return (
<Popup
visible
onMaskClick={onClose}
position="bottom"
className="kb-safe"
bodyStyle={{
borderTopLeftRadius: 12,
borderTopRightRadius: 12,
maxHeight: `calc(88vh - ${kb}px)`,
overflowY: 'auto',
padding: `16px 16px calc(env(safe-area-inset-bottom, 0px) + 20px + ${kb}px)`,
}}
>
<div className="sheet-title">{task.title}</div>
<div className="sheet-sub">
@@ -250,6 +278,10 @@ export default function TaskDetail({ task, columns, members, tasks, onClose, onS
</div>
<div className="btn-primary" onClick={save}>{saving ? '保存中…' : '保存'}</div>
<div className="btn-ghost" onClick={() => setCopyOpen(true)}>拷贝到其他项目</div>
<div className="btn-danger" onClick={removeTask}>
删除任务{subs.length > 0 && <span className="del-hint">(将一并删除 {subs.length} 条子任务)</span>}
</div>
<UserPicker
visible={picker === 'assignee'}
@@ -269,12 +301,22 @@ export default function TaskDetail({ task, columns, members, tasks, onClose, onS
onConfirm={(ids) => setForm((p) => ({ ...p, memberIds: Array.isArray(ids) ? ids : [] }))}
/>
<CopyTaskPopup
task={task}
visible={copyOpen}
onClose={() => setCopyOpen(false)}
onDone={onChanged}
/>
{/* 转为子任务: 从项目内顶层任务中选父任务 */}
<Popup
visible={parentPick}
onMaskClick={() => setParentPick(false)}
className="kb-safe"
bodyStyle={{ maxHeight: '70vh' }}
bodyStyle={{
borderTopLeftRadius: 12, borderTopRightRadius: 12,
maxHeight: '70vh', overflowY: 'auto',
padding: '16px 16px calc(env(safe-area-inset-bottom, 0px) + 20px)',
}}
>
<div className="sheet-title">转为子任务</div>
<div className="sheet-sub">选择父任务(仅支持一级;已有子任务的任务不能作为父任务)</div>