Files
iboard/src/pages/task/ProjectMembers.jsx
T
jif2.zhang 6c9cd3fd99
Docker Build and Push / build-image (push) Failing after 3m51s
feat(V0.2): 任务模块 —— 项目 / 列表与看板 / 列-状态绑定 / 一级子任务
需求 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 缺失、领域逻辑用例全部通过
2026-10-24 11:21:16 +08:00

112 lines
3.8 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}