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
67 lines
2.3 KiB
React
67 lines
2.3 KiB
React
import { useEffect, useState } from 'react'
|
|
import { Popup, Toast } from 'antd-mobile'
|
|
|
|
// 人员选择 (T-AS-03/04): 只能从"当前项目成员"中选
|
|
export default function UserPicker({
|
|
visible, members = [], value, multiple = false, title = '选择负责人',
|
|
allowEmpty = true, onClose, onConfirm,
|
|
}) {
|
|
const [sel, setSel] = useState(multiple ? [] : null)
|
|
|
|
useEffect(() => {
|
|
if (!visible) return
|
|
setSel(multiple ? (value || []) : (value ?? null))
|
|
}, [visible, value, multiple])
|
|
|
|
const toggle = (uid) => {
|
|
if (multiple) {
|
|
setSel((prev) => {
|
|
const arr = Array.isArray(prev) ? prev : []
|
|
return arr.includes(uid) ? arr.filter((x) => x !== uid) : [...arr, uid]
|
|
})
|
|
} else {
|
|
setSel((prev) => (prev === uid ? null : uid))
|
|
}
|
|
}
|
|
|
|
const confirm = () => {
|
|
if (!multiple && sel === null && !allowEmpty) {
|
|
Toast.show({ content: '请选择负责人' })
|
|
return
|
|
}
|
|
onConfirm?.(sel)
|
|
onClose?.()
|
|
}
|
|
|
|
return (
|
|
<Popup visible={visible} onMaskClick={onClose} bodyStyle={{ borderTopLeftRadius: 12, borderTopRightRadius: 12, padding: '16px 16px calc(env(safe-area-inset-bottom, 0px) + 20px)' }}>
|
|
<div className="sheet-title">{title}</div>
|
|
<div className="sheet-sub">仅可选择当前项目的成员</div>
|
|
|
|
{members.length === 0 && <div className="extras-empty">项目暂无成员</div>}
|
|
|
|
{members.map((m) => {
|
|
const on = multiple ? (Array.isArray(sel) && sel.includes(m.userId)) : sel === m.userId
|
|
return (
|
|
<div className="pick-row" key={m.userId} onClick={() => toggle(m.userId)}>
|
|
<div className="avatar">{(m.name || '?').slice(0, 1)}</div>
|
|
<span className="pn">{m.name}</span>
|
|
<span className={`ck${on ? ' on' : ''}`} />
|
|
</div>
|
|
)
|
|
})}
|
|
|
|
{allowEmpty && !multiple && (
|
|
<div className="pick-row" onClick={() => setSel(null)}>
|
|
<div className="avatar" style={{ background: '#f2f3f5', color: '#b6bcc4' }}>—</div>
|
|
<span className="pn">清除负责人</span>
|
|
<span className={`ck${sel === null ? ' on' : ''}`} />
|
|
</div>
|
|
)}
|
|
|
|
<div className="btn-primary" onClick={confirm}>确定</div>
|
|
<div className="btn-ghost" onClick={onClose}>取消</div>
|
|
</Popup>
|
|
)
|
|
}
|