Browse Source

feat: register routes for MyPage, MenuManagement, RoleManagement, OrganizationManagement

master
dark 1 month ago
parent
commit
dea4ae80b6
  1. 12
      frontend/react-shadcn/pc/src/App.tsx
  2. 9
      frontend/react-shadcn/pc/src/components/layout/MainLayout.tsx
  3. 796
      frontend/react-shadcn/pc/src/pages/OrganizationManagementPage.tsx

12
frontend/react-shadcn/pc/src/App.tsx

@ -3,9 +3,15 @@ import { AuthProvider } from './contexts/AuthContext'
import { ProtectedRoute } from './components/layout/ProtectedRoute'
import { MainLayout } from './components/layout/MainLayout'
import { LoginPage } from './pages/LoginPage'
import { SSOCallbackPage } from './pages/SSOCallbackPage'
import { DashboardPage } from './pages/DashboardPage'
import { UserManagementPage } from './pages/UserManagementPage'
import { SettingsPage } from './pages/SettingsPage'
import { FileManagementPage } from './pages/FileManagementPage'
import { MyPage } from './pages/MyPage'
import { MenuManagementPage } from './pages/MenuManagementPage'
import { RoleManagementPage } from './pages/RoleManagementPage'
import { OrganizationManagementPage } from './pages/OrganizationManagementPage'
function App() {
return (
@ -13,6 +19,7 @@ function App() {
<BrowserRouter>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/sso/callback" element={<SSOCallbackPage />} />
<Route
path="/"
element={
@ -24,6 +31,11 @@ function App() {
<Route index element={<Navigate to="/dashboard" replace />} />
<Route path="dashboard" element={<DashboardPage />} />
<Route path="users" element={<UserManagementPage />} />
<Route path="files" element={<FileManagementPage />} />
<Route path="my" element={<MyPage />} />
<Route path="menus" element={<MenuManagementPage />} />
<Route path="roles" element={<RoleManagementPage />} />
<Route path="organizations" element={<OrganizationManagementPage />} />
<Route path="settings" element={<SettingsPage />} />
</Route>
</Routes>

9
frontend/react-shadcn/pc/src/components/layout/MainLayout.tsx

@ -1,15 +1,20 @@
import { Outlet } from 'react-router-dom'
import { Outlet, useLocation } from 'react-router-dom'
import { Sidebar } from './Sidebar'
import { Header } from './Header'
const pageTitles: Record<string, { title: string; subtitle?: string }> = {
'/my': { title: '我的', subtitle: '个人信息与机构' },
'/dashboard': { title: '仪表盘', subtitle: '系统概览与数据统计' },
'/users': { title: '用户管理', subtitle: '管理系统用户账号' },
'/files': { title: '文件管理', subtitle: '上传与管理系统文件' },
'/roles': { title: '角色管理', subtitle: '管理系统角色与权限' },
'/menus': { title: '菜单管理', subtitle: '配置系统导航菜单' },
'/organizations': { title: '机构管理', subtitle: '管理组织架构与成员' },
'/settings': { title: '系统设置', subtitle: '配置系统参数' },
}
export function MainLayout() {
const pathname = window.location.pathname
const { pathname } = useLocation()
const pageInfo = pageTitles[pathname] || { title: 'BASE', subtitle: '' }
return (

796
frontend/react-shadcn/pc/src/pages/OrganizationManagementPage.tsx

@ -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…
Cancel
Save