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 缺失、领域逻辑用例全部通过
112 lines
3.8 KiB
React
112 lines
3.8 KiB
React
import { useState } from 'react'
|
||
import { Dialog, Toast } from 'antd-mobile'
|
||
import { projectApi } from '../../api/task.js'
|
||
import { useAuth } from '../../auth/AuthContext.jsx'
|
||
|
||
// 项目成员 (T-PJ-06/07, T-AS-06, T-PM-02)
|
||
export default function ProjectMembers({ project, onClose, onChanged }) {
|
||
const { user } = useAuth()
|
||
const meId = user?.id
|
||
const [members, setMembers] = useState(project.members || [])
|
||
const [adding, setAdding] = useState(false)
|
||
const [email, setEmail] = useState('')
|
||
|
||
async function add() {
|
||
const e = email.trim()
|
||
if (!e) return
|
||
try {
|
||
await projectApi.addMember(project.id, { email: e })
|
||
setEmail('')
|
||
setAdding(false)
|
||
Toast.show({ content: '已添加' })
|
||
onChanged?.()
|
||
onClose?.()
|
||
} catch (err) {
|
||
Toast.show({ content: err.message })
|
||
}
|
||
}
|
||
|
||
async function remove(m, isSelf) {
|
||
const ok = await Dialog.confirm({
|
||
content: isSelf ? '退出该项目?退出后将不再可见。' : `移除成员「${m.name}」?`,
|
||
confirmText: isSelf ? '退出' : '移除',
|
||
})
|
||
if (!ok) return
|
||
try {
|
||
await projectApi.removeMember(project.id, m.userId)
|
||
setMembers((prev) => prev.filter((x) => x.userId !== m.userId))
|
||
Toast.show({ content: isSelf ? '已退出' : '已移除' })
|
||
onChanged?.()
|
||
if (isSelf) onClose?.()
|
||
} catch (err) {
|
||
Toast.show({ content: err.message })
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="cs-page">
|
||
<div className="navbar-sub">
|
||
<span className="back" onClick={onClose}>‹</span>
|
||
<span className="title">{project.name}</span>
|
||
<div className="right">
|
||
{!project.isPersonal && (
|
||
<span className="mini-btn solid" onClick={() => setAdding(true)}>+ 添加</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="cs-body">
|
||
<div className="set-hint">
|
||
{project.isPersonal
|
||
? '个人默认项目不可添加成员。'
|
||
: '项目内所有成员权限均等、全读写;负责人与成员只是信息标记,不影响权限。'}
|
||
</div>
|
||
|
||
{members.map((m) => {
|
||
const isOwner = m.userId === project.ownerId
|
||
const isMe = m.userId === meId
|
||
return (
|
||
<div className="set-row" key={m.userId} style={{ padding: '12px 14px' }}>
|
||
<div className="avatar">{(m.name || '?').slice(0, 1)}</div>
|
||
<span className="pn" style={{ flex: 1, fontSize: 13.5 }}>
|
||
{m.name}
|
||
{isMe && <span className="tag grey" style={{ marginLeft: 6 }}>我</span>}
|
||
{isOwner && <span className="tag grey" style={{ marginLeft: 6 }}>创建人</span>}
|
||
</span>
|
||
{isMe && !project.isPersonal && (
|
||
<span className="link danger" onClick={() => remove(m, true)}>退出</span>
|
||
)}
|
||
{!isMe && isOwner && <span className="link" style={{ color: '#999' }}>—</span>}
|
||
{!isMe && !isOwner && !project.isPersonal && (
|
||
<span className="link danger" onClick={() => remove(m, false)}>移除</span>
|
||
)}
|
||
</div>
|
||
)
|
||
})}
|
||
|
||
<div className="set-hint">
|
||
成员被移出项目后,其担任的负责人 / 成员标记会保留历史并显示为「已退出」,不清空、不报错。
|
||
</div>
|
||
</div>
|
||
|
||
<Dialog
|
||
visible={adding}
|
||
title="添加成员"
|
||
content={
|
||
<input
|
||
className="inp"
|
||
placeholder="输入成员邮箱"
|
||
value={email}
|
||
onChange={(e) => setEmail(e.target.value)}
|
||
/>
|
||
}
|
||
actions={[
|
||
{ key: 'c', text: '取消', onClick: () => setAdding(false) },
|
||
{ key: 'ok', text: '添加', bold: true, onClick: add },
|
||
]}
|
||
onClose={() => setAdding(false)}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|