3 changed files with 815 additions and 2 deletions
@ -0,0 +1,796 @@ |
|||
import { useState, useEffect, useCallback } from 'react' |
|||
import { Plus, Search, Edit2, Trash2, Users, ChevronRight, ChevronDown } from 'lucide-react' |
|||
import { Button } from '@/components/ui/Button' |
|||
import { Input } from '@/components/ui/Input' |
|||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card' |
|||
import { Modal } from '@/components/ui/Modal' |
|||
import { |
|||
Table, |
|||
TableHeader, |
|||
TableBody, |
|||
TableRow, |
|||
TableHead, |
|||
TableCell, |
|||
} from '@/components/ui/Table' |
|||
import type { OrgInfo, CreateOrgRequest, UpdateOrgRequest, OrgMember, RoleInfo } from '@/types' |
|||
import { apiClient } from '@/services/api' |
|||
|
|||
// ---------------------------------------------------------------------------
|
|||
// Helpers
|
|||
// ---------------------------------------------------------------------------
|
|||
|
|||
type FlatOrg = OrgInfo & { depth: number } |
|||
|
|||
function flattenOrgTree(items: OrgInfo[], depth = 0): FlatOrg[] { |
|||
const result: FlatOrg[] = [] |
|||
for (const item of items) { |
|||
result.push({ ...item, depth }) |
|||
if (item.children && item.children.length > 0) { |
|||
result.push(...flattenOrgTree(item.children, depth + 1)) |
|||
} |
|||
} |
|||
return result |
|||
} |
|||
|
|||
function collectOrgOptions(items: OrgInfo[], depth = 0): { id: number; name: string; depth: number }[] { |
|||
const result: { id: number; name: string; depth: number }[] = [] |
|||
for (const item of items) { |
|||
result.push({ id: item.id, name: item.name, depth }) |
|||
if (item.children && item.children.length > 0) { |
|||
result.push(...collectOrgOptions(item.children, depth + 1)) |
|||
} |
|||
} |
|||
return result |
|||
} |
|||
|
|||
const STATUS_LABELS: Record<number, { text: string; className: string }> = { |
|||
1: { text: '正常', className: 'bg-green-500/20 text-green-400 border border-green-500/30' }, |
|||
0: { text: '停用', className: 'bg-red-500/20 text-red-400 border border-red-500/30' }, |
|||
} |
|||
|
|||
// ---------------------------------------------------------------------------
|
|||
// Component
|
|||
// ---------------------------------------------------------------------------
|
|||
|
|||
export function OrganizationManagementPage() { |
|||
// --- Org tree state ---
|
|||
const [orgTree, setOrgTree] = useState<OrgInfo[]>([]) |
|||
const [isLoading, setIsLoading] = useState(true) |
|||
const [error, setError] = useState<string | null>(null) |
|||
const [searchQuery, setSearchQuery] = useState('') |
|||
const [expandedIds, setExpandedIds] = useState<Set<number>>(new Set()) |
|||
|
|||
// --- Create / Edit modal ---
|
|||
const [isModalOpen, setIsModalOpen] = useState(false) |
|||
const [editingOrg, setEditingOrg] = useState<OrgInfo | null>(null) |
|||
const [formData, setFormData] = useState<Partial<CreateOrgRequest>>({ |
|||
parentId: 0, |
|||
name: '', |
|||
code: '', |
|||
leader: '', |
|||
phone: '', |
|||
email: '', |
|||
sortOrder: 0, |
|||
}) |
|||
|
|||
// --- Delete confirmation ---
|
|||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false) |
|||
const [orgToDelete, setOrgToDelete] = useState<FlatOrg | null>(null) |
|||
const [isDeleting, setIsDeleting] = useState(false) |
|||
const [deleteError, setDeleteError] = useState<string | null>(null) |
|||
|
|||
// --- Member management modal ---
|
|||
const [memberModalOpen, setMemberModalOpen] = useState(false) |
|||
const [memberOrg, setMemberOrg] = useState<FlatOrg | null>(null) |
|||
const [members, setMembers] = useState<OrgMember[]>([]) |
|||
const [isMembersLoading, setIsMembersLoading] = useState(false) |
|||
const [roles, setRoles] = useState<RoleInfo[]>([]) |
|||
const [newMemberUserId, setNewMemberUserId] = useState('') |
|||
const [newMemberRoleId, setNewMemberRoleId] = useState<number>(0) |
|||
const [editingMemberUserId, setEditingMemberUserId] = useState<number | null>(null) |
|||
const [editingMemberRoleId, setEditingMemberRoleId] = useState<number>(0) |
|||
|
|||
// ---------------------------------------------------------------------------
|
|||
// Data fetching
|
|||
// ---------------------------------------------------------------------------
|
|||
|
|||
const fetchOrganizations = useCallback(async () => { |
|||
try { |
|||
setIsLoading(true) |
|||
setError(null) |
|||
const response = await apiClient.getOrganizations() |
|||
setOrgTree(response.list || []) |
|||
// Expand all by default on first load
|
|||
const allIds = flattenOrgTree(response.list || []).map((o) => o.id) |
|||
setExpandedIds(new Set(allIds)) |
|||
} catch (err) { |
|||
console.error('Failed to fetch organizations:', err) |
|||
setError('获取组织列表失败,请稍后重试') |
|||
setOrgTree([]) |
|||
} finally { |
|||
setIsLoading(false) |
|||
} |
|||
}, []) |
|||
|
|||
useEffect(() => { |
|||
fetchOrganizations() |
|||
}, [fetchOrganizations]) |
|||
|
|||
const fetchMembers = async (orgId: number) => { |
|||
try { |
|||
setIsMembersLoading(true) |
|||
const response = await apiClient.getOrgMembers(orgId) |
|||
setMembers(response.list || []) |
|||
} catch (err) { |
|||
console.error('Failed to fetch members:', err) |
|||
setMembers([]) |
|||
} finally { |
|||
setIsMembersLoading(false) |
|||
} |
|||
} |
|||
|
|||
const fetchRoles = async () => { |
|||
try { |
|||
const response = await apiClient.getRoles() |
|||
setRoles(response.list || []) |
|||
} catch (err) { |
|||
console.error('Failed to fetch roles:', err) |
|||
setRoles([]) |
|||
} |
|||
} |
|||
|
|||
// ---------------------------------------------------------------------------
|
|||
// Org CRUD handlers
|
|||
// ---------------------------------------------------------------------------
|
|||
|
|||
const resetForm = () => { |
|||
setFormData({ parentId: 0, name: '', code: '', leader: '', phone: '', email: '', sortOrder: 0 }) |
|||
setEditingOrg(null) |
|||
} |
|||
|
|||
const openModal = (org?: FlatOrg) => { |
|||
if (org) { |
|||
setEditingOrg(org) |
|||
setFormData({ |
|||
parentId: org.parentId, |
|||
name: org.name, |
|||
code: org.code, |
|||
leader: org.leader, |
|||
phone: org.phone, |
|||
email: org.email, |
|||
sortOrder: org.sortOrder, |
|||
}) |
|||
} else { |
|||
resetForm() |
|||
} |
|||
setIsModalOpen(true) |
|||
} |
|||
|
|||
const handleCreateOrg = async () => { |
|||
try { |
|||
await apiClient.createOrganization(formData as CreateOrgRequest) |
|||
await fetchOrganizations() |
|||
setIsModalOpen(false) |
|||
resetForm() |
|||
} catch (err) { |
|||
console.error('Failed to create organization:', err) |
|||
alert('创建组织失败') |
|||
} |
|||
} |
|||
|
|||
const handleUpdateOrg = async () => { |
|||
if (!editingOrg) return |
|||
try { |
|||
const data: UpdateOrgRequest = { ...formData } |
|||
await apiClient.updateOrganization(editingOrg.id, data) |
|||
await fetchOrganizations() |
|||
setIsModalOpen(false) |
|||
resetForm() |
|||
} catch (err) { |
|||
console.error('Failed to update organization:', err) |
|||
alert('更新组织失败') |
|||
} |
|||
} |
|||
|
|||
const openDeleteConfirm = (org: FlatOrg) => { |
|||
setDeleteError(null) |
|||
|
|||
// Reject if has children
|
|||
if (org.children && org.children.length > 0) { |
|||
setDeleteError('该组织包含子组织,无法删除。请先删除或转移子组织。') |
|||
setOrgToDelete(org) |
|||
setDeleteConfirmOpen(true) |
|||
return |
|||
} |
|||
|
|||
// Reject if has members
|
|||
if (org.memberCount > 0) { |
|||
setDeleteError('该组织包含成员,无法删除。请先移除所有成员。') |
|||
setOrgToDelete(org) |
|||
setDeleteConfirmOpen(true) |
|||
return |
|||
} |
|||
|
|||
setOrgToDelete(org) |
|||
setDeleteConfirmOpen(true) |
|||
} |
|||
|
|||
const handleDeleteOrg = async () => { |
|||
if (!orgToDelete || deleteError) return |
|||
try { |
|||
setIsDeleting(true) |
|||
await apiClient.deleteOrganization(orgToDelete.id) |
|||
setDeleteConfirmOpen(false) |
|||
setOrgToDelete(null) |
|||
await fetchOrganizations() |
|||
} catch (err) { |
|||
console.error('Failed to delete organization:', err) |
|||
alert('删除组织失败') |
|||
} finally { |
|||
setIsDeleting(false) |
|||
} |
|||
} |
|||
|
|||
// ---------------------------------------------------------------------------
|
|||
// Member management handlers
|
|||
// ---------------------------------------------------------------------------
|
|||
|
|||
const openMemberModal = async (org: FlatOrg) => { |
|||
setMemberOrg(org) |
|||
setMemberModalOpen(true) |
|||
setNewMemberUserId('') |
|||
setNewMemberRoleId(0) |
|||
setEditingMemberUserId(null) |
|||
await Promise.all([fetchMembers(org.id), fetchRoles()]) |
|||
} |
|||
|
|||
const handleAddMember = async () => { |
|||
if (!memberOrg || !newMemberUserId || !newMemberRoleId) return |
|||
try { |
|||
await apiClient.addOrgMember(memberOrg.id, Number(newMemberUserId), newMemberRoleId) |
|||
setNewMemberUserId('') |
|||
setNewMemberRoleId(0) |
|||
await fetchMembers(memberOrg.id) |
|||
await fetchOrganizations() |
|||
} catch (err) { |
|||
console.error('Failed to add member:', err) |
|||
alert('添加成员失败') |
|||
} |
|||
} |
|||
|
|||
const handleUpdateMemberRole = async (userId: number) => { |
|||
if (!memberOrg || !editingMemberRoleId) return |
|||
try { |
|||
await apiClient.updateOrgMember(memberOrg.id, userId, editingMemberRoleId) |
|||
setEditingMemberUserId(null) |
|||
await fetchMembers(memberOrg.id) |
|||
} catch (err) { |
|||
console.error('Failed to update member role:', err) |
|||
alert('更新成员角色失败') |
|||
} |
|||
} |
|||
|
|||
const handleRemoveMember = async (userId: number) => { |
|||
if (!memberOrg) return |
|||
try { |
|||
await apiClient.removeOrgMember(memberOrg.id, userId) |
|||
await fetchMembers(memberOrg.id) |
|||
await fetchOrganizations() |
|||
} catch (err) { |
|||
console.error('Failed to remove member:', err) |
|||
alert('移除成员失败') |
|||
} |
|||
} |
|||
|
|||
// ---------------------------------------------------------------------------
|
|||
// Tree expand / collapse
|
|||
// ---------------------------------------------------------------------------
|
|||
|
|||
const toggleExpand = (id: number) => { |
|||
setExpandedIds((prev) => { |
|||
const next = new Set(prev) |
|||
if (next.has(id)) { |
|||
next.delete(id) |
|||
} else { |
|||
next.add(id) |
|||
} |
|||
return next |
|||
}) |
|||
} |
|||
|
|||
// ---------------------------------------------------------------------------
|
|||
// Derived data
|
|||
// ---------------------------------------------------------------------------
|
|||
|
|||
const flatOrgs = flattenOrgTree(orgTree) |
|||
const orgOptions = collectOrgOptions(orgTree) |
|||
|
|||
// Filter: show rows whose name or code matches, plus their ancestors
|
|||
const filteredOrgs = searchQuery |
|||
? flatOrgs.filter( |
|||
(o) => |
|||
o.name.toLowerCase().includes(searchQuery.toLowerCase()) || |
|||
o.code.toLowerCase().includes(searchQuery.toLowerCase()) || |
|||
o.leader.toLowerCase().includes(searchQuery.toLowerCase()) |
|||
) |
|||
: flatOrgs.filter((_) => { |
|||
return true |
|||
}) |
|||
|
|||
// For collapse filtering: determine visible rows considering expanded state
|
|||
const visibleOrgs = (() => { |
|||
if (searchQuery) return filteredOrgs |
|||
|
|||
const visible: FlatOrg[] = [] |
|||
const depthStack: number[] = [] // tracks the depth of hidden subtrees
|
|||
|
|||
for (const org of flatOrgs) { |
|||
// If this org is at a depth deeper than a collapsed parent, skip it
|
|||
if (depthStack.length > 0 && org.depth > depthStack[depthStack.length - 1]) { |
|||
continue |
|||
} |
|||
|
|||
// Clean up the depth stack
|
|||
while (depthStack.length > 0 && org.depth <= depthStack[depthStack.length - 1]) { |
|||
depthStack.pop() |
|||
} |
|||
|
|||
visible.push(org) |
|||
|
|||
// If this org has children and is not expanded, push its depth to mark subtree as hidden
|
|||
if (org.children && org.children.length > 0 && !expandedIds.has(org.id)) { |
|||
depthStack.push(org.depth) |
|||
} |
|||
} |
|||
|
|||
return visible |
|||
})() |
|||
|
|||
// ---------------------------------------------------------------------------
|
|||
// Render helpers
|
|||
// ---------------------------------------------------------------------------
|
|||
|
|||
const renderStatusBadge = (status: number) => { |
|||
const config = STATUS_LABELS[status] || { text: '未知', className: 'bg-gray-500/20 text-gray-400' } |
|||
return ( |
|||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${config.className}`}> |
|||
{config.text} |
|||
</span> |
|||
) |
|||
} |
|||
|
|||
// ---------------------------------------------------------------------------
|
|||
// JSX
|
|||
// ---------------------------------------------------------------------------
|
|||
|
|||
return ( |
|||
<div className="space-y-6 animate-fade-in"> |
|||
{/* Header */} |
|||
<Card> |
|||
<CardContent className="p-6"> |
|||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4"> |
|||
<div className="flex-1 w-full sm:max-w-md"> |
|||
<Input |
|||
placeholder="搜索组织名称、编码、负责人..." |
|||
value={searchQuery} |
|||
onChange={(e) => setSearchQuery(e.target.value)} |
|||
leftIcon={<Search className="h-4 w-4" />} |
|||
/> |
|||
</div> |
|||
<Button onClick={() => openModal()} className="whitespace-nowrap"> |
|||
<Plus className="h-4 w-4" /> |
|||
新增组织 |
|||
</Button> |
|||
</div> |
|||
</CardContent> |
|||
</Card> |
|||
|
|||
{/* Error */} |
|||
{error && ( |
|||
<div className="p-4 bg-red-500/10 text-red-400 rounded-lg flex justify-between items-center"> |
|||
<span>{error}</span> |
|||
<button onClick={fetchOrganizations} className="underline hover:text-red-300"> |
|||
重试 |
|||
</button> |
|||
</div> |
|||
)} |
|||
|
|||
{/* Organizations Table */} |
|||
<Card> |
|||
<CardHeader> |
|||
<CardTitle>组织列表 ({flatOrgs.length})</CardTitle> |
|||
</CardHeader> |
|||
<CardContent> |
|||
<div className="overflow-x-auto"> |
|||
<Table> |
|||
<TableHeader> |
|||
<TableRow> |
|||
<TableHead>名称</TableHead> |
|||
<TableHead>编码</TableHead> |
|||
<TableHead>负责人</TableHead> |
|||
<TableHead>联系电话</TableHead> |
|||
<TableHead>成员数</TableHead> |
|||
<TableHead>状态</TableHead> |
|||
<TableHead className="text-right">操作</TableHead> |
|||
</TableRow> |
|||
</TableHeader> |
|||
<TableBody> |
|||
{isLoading ? ( |
|||
<TableRow> |
|||
<TableCell>加载中...</TableCell> |
|||
</TableRow> |
|||
) : visibleOrgs.length === 0 ? ( |
|||
<TableRow> |
|||
<TableCell>暂无数据</TableCell> |
|||
</TableRow> |
|||
) : ( |
|||
visibleOrgs.map((org) => { |
|||
const hasChildren = org.children && org.children.length > 0 |
|||
const isExpanded = expandedIds.has(org.id) |
|||
|
|||
return ( |
|||
<TableRow key={org.id}> |
|||
<TableCell> |
|||
<div className="flex items-center" style={{ paddingLeft: `${org.depth * 24}px` }}> |
|||
{hasChildren ? ( |
|||
<button |
|||
onClick={() => toggleExpand(org.id)} |
|||
className="mr-2 p-0.5 rounded hover:bg-gray-700/50 text-gray-400 hover:text-gray-200 transition-colors" |
|||
> |
|||
{isExpanded ? ( |
|||
<ChevronDown className="h-4 w-4" /> |
|||
) : ( |
|||
<ChevronRight className="h-4 w-4" /> |
|||
)} |
|||
</button> |
|||
) : ( |
|||
<span className="mr-2 w-5 inline-block" /> |
|||
)} |
|||
<span className="font-medium text-white">{org.name}</span> |
|||
</div> |
|||
</TableCell> |
|||
<TableCell className="text-gray-400">{org.code}</TableCell> |
|||
<TableCell className="text-gray-300">{org.leader || '-'}</TableCell> |
|||
<TableCell className="text-gray-400">{org.phone || '-'}</TableCell> |
|||
<TableCell> |
|||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-sky-500/20 text-sky-400 border border-sky-500/30"> |
|||
{org.memberCount} |
|||
</span> |
|||
</TableCell> |
|||
<TableCell>{renderStatusBadge(org.status)}</TableCell> |
|||
<TableCell className="text-right"> |
|||
<div className="flex items-center justify-end gap-1"> |
|||
<Button |
|||
variant="ghost" |
|||
size="sm" |
|||
onClick={() => openMemberModal(org)} |
|||
title="成员管理" |
|||
> |
|||
<Users className="h-4 w-4" /> |
|||
</Button> |
|||
<Button |
|||
variant="ghost" |
|||
size="sm" |
|||
onClick={() => openModal(org)} |
|||
title="编辑" |
|||
> |
|||
<Edit2 className="h-4 w-4" /> |
|||
</Button> |
|||
<Button |
|||
variant="ghost" |
|||
size="sm" |
|||
onClick={() => openDeleteConfirm(org)} |
|||
className="text-red-400 hover:text-red-300" |
|||
title="删除" |
|||
> |
|||
<Trash2 className="h-4 w-4" /> |
|||
</Button> |
|||
</div> |
|||
</TableCell> |
|||
</TableRow> |
|||
) |
|||
}) |
|||
)} |
|||
</TableBody> |
|||
</Table> |
|||
</div> |
|||
</CardContent> |
|||
</Card> |
|||
|
|||
{/* Create / Edit Modal */} |
|||
<Modal |
|||
isOpen={isModalOpen} |
|||
onClose={() => { |
|||
setIsModalOpen(false) |
|||
resetForm() |
|||
}} |
|||
title={editingOrg ? '编辑组织' : '新增组织'} |
|||
size="md" |
|||
footer={ |
|||
<> |
|||
<Button |
|||
variant="outline" |
|||
onClick={() => { |
|||
setIsModalOpen(false) |
|||
resetForm() |
|||
}} |
|||
> |
|||
取消 |
|||
</Button> |
|||
<Button onClick={editingOrg ? handleUpdateOrg : handleCreateOrg}> |
|||
{editingOrg ? '保存' : '创建'} |
|||
</Button> |
|||
</> |
|||
} |
|||
> |
|||
<div className="space-y-4"> |
|||
{/* Parent Org */} |
|||
<div> |
|||
<label className="block text-sm font-medium text-gray-300 mb-1.5">上级组织</label> |
|||
<select |
|||
className="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-sky-500/50" |
|||
value={formData.parentId || 0} |
|||
onChange={(e) => setFormData({ ...formData, parentId: Number(e.target.value) })} |
|||
> |
|||
<option value={0} className="bg-gray-800">无(顶级组织)</option> |
|||
{orgOptions |
|||
.filter((o) => !editingOrg || o.id !== editingOrg.id) |
|||
.map((o) => ( |
|||
<option key={o.id} value={o.id} className="bg-gray-800"> |
|||
{' '.repeat(o.depth)}{o.name} |
|||
</option> |
|||
))} |
|||
</select> |
|||
</div> |
|||
|
|||
<Input |
|||
label="组织名称" |
|||
placeholder="请输入组织名称" |
|||
value={formData.name || ''} |
|||
onChange={(e) => setFormData({ ...formData, name: e.target.value })} |
|||
/> |
|||
<Input |
|||
label="组织编码" |
|||
placeholder="请输入组织编码" |
|||
value={formData.code || ''} |
|||
onChange={(e) => setFormData({ ...formData, code: e.target.value })} |
|||
/> |
|||
<Input |
|||
label="负责人" |
|||
placeholder="请输入负责人" |
|||
value={formData.leader || ''} |
|||
onChange={(e) => setFormData({ ...formData, leader: e.target.value })} |
|||
/> |
|||
<Input |
|||
label="联系电话" |
|||
placeholder="请输入联系电话" |
|||
value={formData.phone || ''} |
|||
onChange={(e) => setFormData({ ...formData, phone: e.target.value })} |
|||
/> |
|||
<Input |
|||
label="邮箱" |
|||
type="email" |
|||
placeholder="请输入邮箱" |
|||
value={formData.email || ''} |
|||
onChange={(e) => setFormData({ ...formData, email: e.target.value })} |
|||
/> |
|||
<Input |
|||
label="排序" |
|||
type="number" |
|||
placeholder="排序号(数字越小越靠前)" |
|||
value={formData.sortOrder?.toString() || '0'} |
|||
onChange={(e) => setFormData({ ...formData, sortOrder: Number(e.target.value) })} |
|||
/> |
|||
</div> |
|||
</Modal> |
|||
|
|||
{/* Delete Confirmation Modal */} |
|||
<Modal |
|||
isOpen={deleteConfirmOpen} |
|||
onClose={() => { |
|||
setDeleteConfirmOpen(false) |
|||
setOrgToDelete(null) |
|||
setDeleteError(null) |
|||
}} |
|||
title="确认删除" |
|||
size="sm" |
|||
footer={ |
|||
<> |
|||
<Button |
|||
variant="outline" |
|||
onClick={() => { |
|||
setDeleteConfirmOpen(false) |
|||
setOrgToDelete(null) |
|||
setDeleteError(null) |
|||
}} |
|||
disabled={isDeleting} |
|||
> |
|||
取消 |
|||
</Button> |
|||
{!deleteError && ( |
|||
<Button |
|||
variant="destructive" |
|||
onClick={handleDeleteOrg} |
|||
disabled={isDeleting} |
|||
> |
|||
{isDeleting ? '删除中...' : '确认删除'} |
|||
</Button> |
|||
)} |
|||
</> |
|||
} |
|||
> |
|||
<div className="py-4"> |
|||
{deleteError ? ( |
|||
<div className="p-3 bg-red-500/10 border border-red-500/30 rounded-lg"> |
|||
<p className="text-red-400 text-sm">{deleteError}</p> |
|||
</div> |
|||
) : ( |
|||
<> |
|||
<p className="text-gray-300"> |
|||
确定要删除组织 <span className="font-medium text-white">{orgToDelete?.name}</span> 吗? |
|||
</p> |
|||
<p className="text-sm text-gray-500 mt-2">此操作不可恢复,请谨慎操作。</p> |
|||
</> |
|||
)} |
|||
</div> |
|||
</Modal> |
|||
|
|||
{/* Member Management Modal */} |
|||
<Modal |
|||
isOpen={memberModalOpen} |
|||
onClose={() => { |
|||
setMemberModalOpen(false) |
|||
setMemberOrg(null) |
|||
setMembers([]) |
|||
setEditingMemberUserId(null) |
|||
}} |
|||
title={`成员管理 — ${memberOrg?.name || ''}`} |
|||
size="lg" |
|||
> |
|||
<div className="space-y-6"> |
|||
{/* Add Member Section */} |
|||
<div className="p-4 rounded-lg border border-gray-800 bg-gray-800/30"> |
|||
<h4 className="text-sm font-medium text-gray-300 mb-3">添加成员</h4> |
|||
<div className="flex items-end gap-3"> |
|||
<div className="flex-1"> |
|||
<Input |
|||
label="用户 ID" |
|||
placeholder="请输入用户 ID" |
|||
value={newMemberUserId} |
|||
onChange={(e) => setNewMemberUserId(e.target.value)} |
|||
/> |
|||
</div> |
|||
<div className="flex-1"> |
|||
<label className="block text-sm font-medium text-gray-300 mb-1.5">角色</label> |
|||
<select |
|||
className="w-full h-11 rounded-lg border border-white/10 bg-gray-900/80 px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-sky-500/50" |
|||
value={newMemberRoleId} |
|||
onChange={(e) => setNewMemberRoleId(Number(e.target.value))} |
|||
> |
|||
<option value={0} className="bg-gray-800">请选择角色</option> |
|||
{roles.map((role) => ( |
|||
<option key={role.id} value={role.id} className="bg-gray-800"> |
|||
{role.name} |
|||
</option> |
|||
))} |
|||
</select> |
|||
</div> |
|||
<Button |
|||
onClick={handleAddMember} |
|||
disabled={!newMemberUserId || !newMemberRoleId} |
|||
className="shrink-0" |
|||
> |
|||
<Plus className="h-4 w-4" /> |
|||
添加 |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* Members List */} |
|||
<div> |
|||
<h4 className="text-sm font-medium text-gray-300 mb-3"> |
|||
当前成员 ({members.length}) |
|||
</h4> |
|||
<div className="overflow-x-auto"> |
|||
<Table> |
|||
<TableHeader> |
|||
<TableRow> |
|||
<TableHead>用户名</TableHead> |
|||
<TableHead>角色</TableHead> |
|||
<TableHead>加入时间</TableHead> |
|||
<TableHead className="text-right">操作</TableHead> |
|||
</TableRow> |
|||
</TableHeader> |
|||
<TableBody> |
|||
{isMembersLoading ? ( |
|||
<TableRow> |
|||
<TableCell>加载中...</TableCell> |
|||
</TableRow> |
|||
) : members.length === 0 ? ( |
|||
<TableRow> |
|||
<TableCell>暂无成员</TableCell> |
|||
</TableRow> |
|||
) : ( |
|||
members.map((member) => ( |
|||
<TableRow key={member.userId}> |
|||
<TableCell className="font-medium text-white">{member.username}</TableCell> |
|||
<TableCell> |
|||
{editingMemberUserId === member.userId ? ( |
|||
<select |
|||
className="rounded-lg border border-white/10 bg-white/5 px-2 py-1 text-sm text-white focus:outline-none focus:ring-2 focus:ring-sky-500/50" |
|||
value={editingMemberRoleId} |
|||
onChange={(e) => setEditingMemberRoleId(Number(e.target.value))} |
|||
> |
|||
{roles.map((role) => ( |
|||
<option key={role.id} value={role.id} className="bg-gray-800"> |
|||
{role.name} |
|||
</option> |
|||
))} |
|||
</select> |
|||
) : ( |
|||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-500/20 text-blue-400 border border-blue-500/30"> |
|||
{member.roleName} |
|||
</span> |
|||
)} |
|||
</TableCell> |
|||
<TableCell className="text-gray-400"> |
|||
{new Date(member.createdAt).toLocaleDateString('zh-CN')} |
|||
</TableCell> |
|||
<TableCell className="text-right"> |
|||
<div className="flex items-center justify-end gap-2"> |
|||
{editingMemberUserId === member.userId ? ( |
|||
<> |
|||
<Button |
|||
variant="ghost" |
|||
size="sm" |
|||
onClick={() => handleUpdateMemberRole(member.userId)} |
|||
className="text-green-400 hover:text-green-300" |
|||
> |
|||
保存 |
|||
</Button> |
|||
<Button |
|||
variant="ghost" |
|||
size="sm" |
|||
onClick={() => setEditingMemberUserId(null)} |
|||
> |
|||
取消 |
|||
</Button> |
|||
</> |
|||
) : ( |
|||
<> |
|||
<Button |
|||
variant="ghost" |
|||
size="sm" |
|||
onClick={() => { |
|||
setEditingMemberUserId(member.userId) |
|||
setEditingMemberRoleId(member.roleId) |
|||
}} |
|||
title="修改角色" |
|||
> |
|||
<Edit2 className="h-4 w-4" /> |
|||
</Button> |
|||
<Button |
|||
variant="ghost" |
|||
size="sm" |
|||
onClick={() => handleRemoveMember(member.userId)} |
|||
className="text-red-400 hover:text-red-300" |
|||
title="移除成员" |
|||
> |
|||
<Trash2 className="h-4 w-4" /> |
|||
</Button> |
|||
</> |
|||
)} |
|||
</div> |
|||
</TableCell> |
|||
</TableRow> |
|||
)) |
|||
)} |
|||
</TableBody> |
|||
</Table> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</Modal> |
|||
</div> |
|||
) |
|||
} |
|||
Loading…
Reference in new issue