@@ -3,14 +3,14 @@
// 门控集中在 src/rdmManagement.ts;分区可见性由 AdminZone/ProjectSettingsModal
// 门控集中在 src/rdmManagement.ts;分区可见性由 AdminZone/ProjectSettingsModal
// 按 store 的 rdm*Available 门控(命令面不齐不渲染,普通基座零暴露)。样式只复用
// 按 store 的 rdm*Available 门控(命令面不齐不渲染,普通基座零暴露)。样式只复用
// 既有 ad-*/lg-*/settings-* 类(design-guard 棘轮:不新增裸色值/字号)。
// 既有 ad-*/lg-*/settings-* 类(design-guard 棘轮:不新增裸色值/字号)。
import { useEffect , useState } from 'react'
import { useEffect , useState , useRef , useCallback } from 'react'
import { api } from '../api'
import { api } from '../api'
import { useStore } from '../store'
import { useStore } from '../store'
import { Btn , EmptyState , Icon } from '@ui'
import { Btn , EmptyState , Icon , Picker } from '@ui'
import {
import {
ROLE_ACTIONS , parseReviewers ,
ROLE_ACTIONS , parseReviewers ,
roleCreatePayload , roleActionPayload , roleAssignPayload , roleDisablePayload ,
roleCreatePayload , roleActionPayload , roleAssignPayload , roleDisablePayload ,
memberAddPayload , memberRemovePayload ,
memberAddPayload , memberRemovePayload , memberSetRolePayload ,
assignDocDutiesPayload , docSubmitPayload , docApprovePayload , docRejectPayload ,
assignDocDutiesPayload , docSubmitPayload , docApprovePayload , docRejectPayload ,
docFinalPayload , docStatusPayload , permCheckPayload , describeSource ,
docFinalPayload , docStatusPayload , permCheckPayload , describeSource ,
rolesListPlaceholder , describeRoleListError ,
rolesListPlaceholder , describeRoleListError ,
@@ -144,17 +144,43 @@ export function RdmRolesPanel() {
)
)
}
}
// ---- 项目设置:成员 ----
// ---- 项目设置:成员(P7:可搜索人员选择器 + 专业角色/项目身份两列 + 显式身份调整) ----
interface MemberRow { owner : string ; role : string ; addedBy : string ; addedAt : string }
interface MemberRow {
owner : string ; role : string ; addedBy : string ; addedAt : string
profRole ? : { roleId : string ; name : string } | null
}
export function RdmMembersSection ( { projectId } : { projectId : string } ) {
export function RdmMembersSection ( { projectId } : { projectId : string } ) {
const [ rows , setRows ] = useState < MemberRow [ ] | null > ( null )
const [ rows , setRows ] = useState < MemberRow [ ] | null > ( null )
const [ err , setErr ] = useState ( '' )
const [ err , setErr ] = useState ( '' )
const [ notice , setNotice ] = useState ( '' )
const [ notice , setNotice ] = useState ( '' )
const [ busy , setBusy ] = useState ( false )
const [ busy , setBusy ] = useState ( false )
const [ member , setMember ] = useState ( '' )
const [ picked , setPicked ] = useState < string [ ] > ( [ ] )
const [ role , setRole ] = useState < 'member' | 'owner' > ( 'member' )
const [ role , setRole ] = useState < 'member' | 'owner' > ( 'member' )
// 成员表经 ref 参与取数(已加入标记),保持 fetchPage 身份稳定(Picker 依赖)。
const rowsRef = useRef < MemberRow [ ] | null > ( null )
rowsRef . current = rows
/** 人员选择器取数适配(P7-A):rdm.account.list 目录(member.manage 生效者)。
* query 透传后端(基座升级 AccountSearcher 后全量生效);旧基座忽略 query 时
* 对首页做本地包含匹配降级(翻页为浏览模式,不过滤)。 */
const fetchPage = useCallback ( async ( query : string , cursor : string , limit : number ) = > {
const out = await api . rdmCommand < { accounts ? : { owner : string ; name? : string ; valid : boolean } [ ] ; next? : string } > (
'rdm.account.list' , { projectId , cursor , limit , query } )
const joined = new Set ( ( rowsRef . current ? ? [ ] ) . map ( m = > m . owner ) )
let items = ( out . accounts ? ? [ ] ) . filter ( a = > a . valid ) . map ( a = > ( {
id : a.owner ,
title : a.name || a . owner ,
subtitle : a.owner ,
badge : joined.has ( a . owner ) ? '已加入' : undefined ,
} ) )
if ( query !== '' && cursor === '' ) {
const q = query . toLowerCase ( )
items = items . filter ( it = > it . id . toLowerCase ( ) . includes ( q ) || it . title . toLowerCase ( ) . includes ( q ) )
}
return { items , nextCursor : out.next || undefined }
} , [ projectId ] )
const refresh = async ( ) = > {
const refresh = async ( ) = > {
try {
try {
@@ -162,7 +188,7 @@ export function RdmMembersSection({ projectId }: { projectId: string }) {
setRows ( out . members ? ? [ ] )
setRows ( out . members ? ? [ ] )
} catch ( e ) { setErr ( String ( e ) ) }
} catch ( e ) { setErr ( String ( e ) ) }
}
}
useEffect ( ( ) = > { refresh ( ) } , [ projectId ] )
useEffect ( ( ) = > { setPicked ( [ ] ) ; refresh ( ) } , [ projectId ] )
const run = async ( label : string , fn : ( ) = > Promise < unknown > ) = > {
const run = async ( label : string , fn : ( ) = > Promise < unknown > ) = > {
setErr ( '' ) ; setNotice ( '' ) ; setBusy ( true )
setErr ( '' ) ; setNotice ( '' ) ; setBusy ( true )
@@ -170,40 +196,56 @@ export function RdmMembersSection({ projectId }: { projectId: string }) {
catch ( e ) { setErr ( String ( e ) ) } finally { setBusy ( false ) }
catch ( e ) { setErr ( String ( e ) ) } finally { setBusy ( false ) }
}
}
const pickedId = picked [ 0 ] ? ? ''
return (
return (
< div >
< div >
< div className = "ad-create" >
< div className = "ad-create" >
< div className = "ad-row" >
< div className = "ad-row" >
< input className = "lg-input lg-mono" placeholder = "成员账号(owner) " value = { member }
< Picker
onChange = { e = > setMember ( e . target . value ) } / >
aria-label = "选择人员"
selected = { picked }
onSelect = { setPicked }
fetchPage = { fetchPage }
placeholder = "输入账号或姓名,或浏览候选…"
/ >
< select className = "lg-input" value = { role } onChange = { e = > setRole ( e . target . value as 'member' | 'owner' ) } >
< select className = "lg-input" value = { role } onChange = { e = > setRole ( e . target . value as 'member' | 'owner' ) } >
< option value = "member" > member ( 成 员 ) < / option >
< option value = "member" > 项 目 成 员 < / option >
< option value = "owner" > owner ( 负 责 人 ) < / option >
< option value = "owner" > 项 目 负 责 人 < / option >
< / select >
< / select >
< button className = "lg-submit ad-create-btn" disabled = { busy || ! member . trim ( ) }
< button className = "lg-submit ad-create-btn" disabled = { busy || ! pickedId }
onClick = { ( ) = > run ( ` 已添加 ${ member . trim ( ) } ` , async ( ) = > {
onClick = { ( ) = > run ( ` 已添加 ${ pickedId } ` , async ( ) = > {
await api . rdmCommand ( 'rdm.project.member.add' , memberAddPayload ( projectId , member . trim ( ) , role ) )
await api . rdmCommand ( 'rdm.project.member.add' , memberAddPayload ( projectId , pickedId , role ) )
setMember ( '' )
setPicked ( [ ] )
} ) } > 添 加 成 员 < / button >
} ) } > 添 加 成 员 < / button >
< / div >
< / div >
< p className = "ad-hint" > 加 入 后 成 员 即 可 见 本 项 目 ; 移 除 即 时 生 效 。 最 后 一 位 负 责 人 不 可 移 除 / 降 级 。 < / p >
< p className = "ad-hint" > 候 选 来 自 系 统 账 号 目 录 ( 姓 名 为 空 时 显 示 账 号 ) ; 「 已 加 入 」 标 记 避 免 重 复 添 加 造 成 隐 式 变 更 — — 调 整 身 份 请 在 成 员 表 中 操 作 。 最 后 一 位 负 责 人 不 可 移 除 / 降 级 。 < / p >
< / div >
< / div >
< table className = "ad-table" >
< table className = "ad-table" >
< thead > < tr > < th > 账 号 < / th > < th > 项 目 角 色 < / th > < th > 添 加 人 < / th > < th > 添 加 时 间 < / th > < th > < / th > < / tr > < / thead >
< thead > < tr > < th > 成 员 < / th > < th > 专 业 角 色 < / th > < th > 项 目 身 份 < / th > < th > 添 加 人 < / th > < th > 添 加 时 间 < / th > < th > < / th > < / tr > < / thead >
< tbody >
< tbody >
{ ( rows ? ? [ ] ) . map ( m = > (
{ ( rows ? ? [ ] ) . map ( m = > (
< tr key = { m . owner } >
< tr key = { m . owner } >
< td className = "ad-mono" > { m . owner } < / td >
< td > < span className = "ad-mono" > { m . owner } < / span > { m . profRole ? . name ? < span className = "ad-dim" > · { m . profRole . name } < / span > : null } < / td >
< td > { m . role === 'owner' ? < span className = "ad-chip ad-chip-admin" > OWNER < / span > : < span className = "ad-chip" > MEMBER < / span > } < / td >
< td className = "ad-dim" > { m . profRole ? . name || '—' } < / td >
< td > { m . role === 'owner' ? < span className = "ad-chip ad-chip-admin" > 项 目 负 责 人 < / span > : < span className = "ad-chip" > 项 目 成 员 < / span > } < / td >
< td className = "ad-dim" > { m . addedBy || '—' } < / td >
< td className = "ad-dim" > { m . addedBy || '—' } < / td >
< td className = "ad-dim" > { m . addedAt ? new Date ( m . addedAt ) . toLocaleString ( ) : '—' } < / td >
< td className = "ad-dim" > { m . addedAt ? new Date ( m . addedAt ) . toLocaleString ( ) : '—' } < / td >
< td > < button className = "ad-mini" disabled = { busy }
< td >
onClick = { ( ) = > run ( ` 已移除 ${ m . owner } ` ,
< button className = "ad-mini" disabled = { busy }
( ) = > api . rdmCommand ( 'rdm.project.member.remove' , memberRemovePayload ( projectId , m . owner ) ) ) } > 移 除 < / button > < / td >
title = { m . role === 'owner' ? '降为项目成员(setRole) ' : '升为项目负责人(setRole) ' }
onClick = { ( ) = > run ( ` 已调整 ${ m . owner } 的项目身份 ` , ( ) = >
api . rdmCommand ( 'rdm.project.member.setRole' , memberSetRolePayload ( projectId , m . owner , m . role === 'owner' ? 'member' : 'owner' ) ) ) } >
{ m . role === 'owner' ? '降为成员' : '升为负责人' }
< / button > { ' ' }
< button className = "ad-mini" disabled = { busy }
onClick = { ( ) = > run ( ` 已移除 ${ m . owner } ` ,
( ) = > api . rdmCommand ( 'rdm.project.member.remove' , memberRemovePayload ( projectId , m . owner ) ) ) } > 移 除 < / button >
< / td >
< / tr >
< / tr >
) ) }
) ) }
{ rows !== null && rows . length === 0 && < tr > < td colSpan = { 5 } className = "ad-dim" > 暂 无 成 员 < / td > < / tr > }
{ rows !== null && rows . length === 0 && < tr > < td colSpan = { 6 } className = "ad-dim" > 暂 无 成 员 < / td > < / tr > }
{ rows === null && < tr > < td colSpan = { 5 } className = "ad-dim" > 加 载 中 … < / td > < / tr > }
{ rows === null && < tr > < td colSpan = { 6 } className = "ad-dim" > 加 载 中 … < / td > < / tr > }
< / tbody >
< / tbody >
< / table >
< / table >
{ notice && < div className = "ad-notice" > < Icon name = "check" size = { 14 } / > { notice } < / div > }
{ notice && < div className = "ad-notice" > < Icon name = "check" size = { 14 } / > { notice } < / div > }