Docker Build and Push / build-image (push) Failing after 1m48s
现象: 多端同时打开同一项目时, 一端改完, 另一端必须切到别的页面再切回来才更新。 根因: 不是同步没到, 而是 UI 从未订阅本地库变化 —— PowerSync 的下行推送 (PG 逻辑复制 → sync service → 客户端本地 SQLite) 本身正常, 但任务模块的读全是命令式 db().getAll() (api/task.js), 读完不再看; lib/powersync/react.jsx 虽然导出了 useQuery/useStatus, 全项目一次都没用过; TaskPage/ProjectView 只在 useEffect 里 load() 一次, 连 useRefreshOnVisible 都没接 (只有打卡与宏观页面在用)。「切面板能刷新」纯粹是因为组件重新挂载又 load 了一次。 实现 (docs/V0.2/离线编辑模块/即时同步方案.md, 方案 C): - 新增 lib/powersync/useDbSignal.js: 用 db.watch() 的表级过滤 (tables) + 节流 (throttleMs) + AbortSignal, 订阅本地库变更; 跳过订阅时的首次通知, 避免与 挂载时的 load() 重复。 (初版设计是手写 count/sum/max(updatedAt) 的「信号 SQL」, 实施时发现 SDK 原生 支持 tables 过滤, 更可靠, 已改为表过滤并在文档中记录变更。) - api/task.js 新增 TASK_TABLES / PROJECT_TABLES 常量 (表名须与客户端 schema 一致)。 - ProjectView: 订阅任务域 (Task/BoardColumn/TaskMember/TaskTag) → 重算; TaskPage: 订阅项目域 (Project/ProjectMember) → 重算。 - 不打断操作: BoardView/ListView 上报 onDragActiveChange, 拖拽期间收到变更只记 pending, 松手后补一次刷新, 避免卡片在手指下跳位。 - 回前台兜底: 两页都接 useRefreshOnVisible (WebView 后台可能节流订阅回调)。 验证 (真实 Windows 外壳 + CDP, 服务端直接改库模拟另一端): - 服务端改任务标题后, 页面在**无任何操作**的情况下自动更新 (三次复测均成功); - 改回原标题后同样自动还原; 测试数据已恢复。 已知瓶颈 (不在 UI 层, 另见文档 §7.0): - 实测端到端延迟约 13~14s, 未达 ≤1s 目标; - CDP Network 观察客户端: 无 WebSocket, 仅 /sync/stream 长连接; - NPM (baishe nginx-app-1, proxy_host/5.conf) 未关闭 proxy_buffering, 流式响应被 nginx 缓冲后按批下发, 与观测到的固定批次延迟吻合; - 修复属运维配置 (proxy_buffering off 等), 需单独评估后执行。
94 lines
3.7 KiB
React
94 lines
3.7 KiB
React
import { useEffect, useRef, useState } from 'react'
|
|
import TaskCard from '../../components/task/TaskCard.jsx'
|
|
import useColumnDrag from '../../components/task/useColumnDrag.js'
|
|
|
|
// 列体 + 右缘滚动条: 滚动时出现细滚动条, 停止约 700ms 后淡出
|
|
// (移动端 WebView 原生滚动条是即隐式的, 看板列窄、需要明确的滚动指示)
|
|
function ScrollBody({ children }) {
|
|
const ref = useRef(null)
|
|
const fade = useRef(null)
|
|
const [bar, setBar] = useState(null) // { h, y } 滚动条几何
|
|
|
|
useEffect(() => () => clearTimeout(fade.current), [])
|
|
|
|
function onScroll() {
|
|
const el = ref.current
|
|
if (!el) return
|
|
const max = el.scrollHeight - el.clientHeight
|
|
if (max <= 2) { setBar(null); return }
|
|
const h = Math.max(28, Math.round(el.clientHeight * (el.clientHeight / el.scrollHeight)))
|
|
const y = Math.round((el.scrollTop / max) * (el.clientHeight - h))
|
|
setBar({ h, y })
|
|
clearTimeout(fade.current)
|
|
fade.current = setTimeout(() => setBar(null), 700)
|
|
}
|
|
|
|
return (
|
|
<div className="bcol-main">
|
|
<div className="bcol-body" ref={ref} onScroll={onScroll}>{children}</div>
|
|
<div className={'bcol-bar' + (bar ? ' on' : '')} style={bar ? { top: bar.y, height: bar.h } : undefined} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// 看板视图 (T-BD-01, T-UI-07)
|
|
// 拖动 = 长按 400ms 激活, 卡片原位抬起, 指针划过列高亮, 松手落列 (重点设计 §7.1)
|
|
// 横向滚列; 列内独立纵向滚动, 列头固定, 滚动时右缘显示滚动条
|
|
// 不做"拖到卡片上转子任务" —— 统一走详情页「转为子任务」
|
|
export default function BoardView({
|
|
tasks, columns, onMoveColumn, onOpen, onToggleSub, onToggle, onMore, onQuickAdd, onDragActiveChange,
|
|
}) {
|
|
const drag = useColumnDrag({ onDrop: onMoveColumn })
|
|
|
|
// 把"是否正在拖拽"上报给容器: 拖拽期间收到远端变更时, 容器会延后刷新,
|
|
// 避免卡片在手中跳位 (见 docs/V0.2/离线编辑模块/即时同步方案.md §4.4)
|
|
const dragging = !!drag.dragTask
|
|
useEffect(() => { onDragActiveChange?.(dragging) }, [dragging, onDragActiveChange])
|
|
|
|
const ordered = [...(columns || [])].sort((a, b) => a.sortOrder - b.sortOrder)
|
|
|
|
return (
|
|
<div className="board-wrap">
|
|
{ordered.map((col) => {
|
|
const list = (tasks || []).filter((t) => t.columnId === col.id)
|
|
return (
|
|
<div
|
|
className={`bcol${drag.overCol === col.id ? ' dragover' : ''}`}
|
|
key={col.id}
|
|
data-column-id={col.id}
|
|
>
|
|
<div className="bcol-head">
|
|
<span className="bcol-name">{col.name}</span>
|
|
<span className="bcol-cnt">{list.length}</span>
|
|
{col.boundStatus ? (
|
|
<span className="bcol-bind">
|
|
↔ {col.boundStatus === 'done' ? '完成' : col.boundStatus === 'doing' ? '执行中' : '未开始'}
|
|
</span>
|
|
) : (
|
|
<span className="bcol-bind off">未绑定</span>
|
|
)}
|
|
</div>
|
|
|
|
<ScrollBody>
|
|
{list.map((t) => (
|
|
<TaskCard
|
|
key={t.id}
|
|
task={t}
|
|
dragging={drag.dragTask?.id === t.id}
|
|
bindDrag={drag.bind}
|
|
onClick={() => { if (!drag.dragTask && !drag.justEnded()) onOpen?.(t) }}
|
|
onToggleSub={onToggleSub}
|
|
onToggle={onToggle}
|
|
onMore={onMore}
|
|
/>
|
|
))}
|
|
{list.length === 0 && <div className="bcol-empty">暂无任务</div>}
|
|
<div className="badd" onClick={() => onQuickAdd?.(col.id)}>+ 添加任务</div>
|
|
</ScrollBody>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|