feat(V0.2): 任务模块 —— 项目 / 列表与看板 / 列-状态绑定 / 一级子任务
Docker Build and Push / build-image (push) Failing after 3m51s
Docker Build and Push / build-image (push) Failing after 3m51s
需求 142 条 (T-*), 设计见 docs/V0.2/任务模块/ 数据层: - 新增 6 张表: Project / ProjectMember / BoardColumn / Task / TaskMember / TaskTag - 看板列是实体(可增删改排序), Task.columnId 外键 RESTRICT 防误删列丢任务 - Task.status(todo/doing/done) 与 columnId 双落库, 二者正交 领域层 server/task-domain/ (唯一口径, 前后端不重复实现): - resolveStatus: 列决定状态(只看目标列, 无拖出回退); 状态与列不匹配时按列序取第一个绑定该状态的列自动归位, 无匹配列则不移动; 子任务完全豁免 - retention: 逐列 6 档保留窗口(永不显示/当天/3天/本周/本月/永久显示), 以 doneAt 判定 - copyTask: 跨项目深拷贝, 人员按目标项目成员过滤并回报丢弃名单 接口: /api/projects, /api/columns, /api/tasks (均校验项目成员) 前端: - 底部 Tab 插入「任务」, 位于「打卡」之后; 默认首页仍为打卡 - 列表视图(子任务缩进+折叠) / 看板视图(长按拖拽换列、拖到卡片降为子任务) - 列设置(列管理+状态绑定+保留选项) / 任务详情(扩展信息卡片默认收起) / 成员管理 - 视图记忆与上次项目存设备端 已验证: 95 文件语法 0 错误、相对导入 0 缺失、领域逻辑用例全部通过
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
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 14px' }}>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user