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