feat(settings): add team management functionality with user invite and team operations
This commit is contained in:
@@ -127,3 +127,125 @@ export function useSystemStatus() {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 团队设置
|
||||
*/
|
||||
export function useTeamSetting() {
|
||||
const { userInfo, tenantInfo, tenantList, refreshUserData } = useUserData();
|
||||
const [tenantUsers, setTenantUsers] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 获取租户用户列表
|
||||
const fetchTenantUsers = useCallback(async () => {
|
||||
if (!tenantInfo?.tenant_id) return;
|
||||
|
||||
try {
|
||||
const response = await userService.listTenantUser(tenantInfo.tenant_id);
|
||||
if (response.data.code === 0) {
|
||||
setTenantUsers(response.data.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('获取租户用户失败:', error);
|
||||
}
|
||||
}, [tenantInfo?.tenant_id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tenantInfo?.tenant_id) {
|
||||
fetchTenantUsers();
|
||||
}
|
||||
}, [fetchTenantUsers]);
|
||||
|
||||
const inviteUser = async (email: string) => {
|
||||
if (!email.trim() || !tenantInfo?.tenant_id) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await userService.addTenantUser(tenantInfo.tenant_id, email.trim());
|
||||
if (response.data.code === 0) {
|
||||
await fetchTenantUsers();
|
||||
await refreshUserData();
|
||||
return { success: true };
|
||||
} else {
|
||||
const errorMsg = response.data.message || '邀请失败';
|
||||
setError(errorMsg);
|
||||
return { success: false, error: errorMsg };
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMsg = error.message || '邀请失败';
|
||||
setError(errorMsg);
|
||||
return { success: false, error: errorMsg };
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteUser = async (userId: string) => {
|
||||
if (!tenantInfo?.tenant_id) return;
|
||||
|
||||
try {
|
||||
const response = await userService.deleteTenantUser({
|
||||
tenantId: tenantInfo.tenant_id,
|
||||
userId
|
||||
});
|
||||
if (response.data.code === 0) {
|
||||
await fetchTenantUsers();
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false };
|
||||
} catch (error) {
|
||||
logger.error('删除用户失败:', error);
|
||||
return { success: false };
|
||||
}
|
||||
};
|
||||
|
||||
const agreeTenant = async (tenantId: string) => {
|
||||
try {
|
||||
const response = await userService.agreeTenant(tenantId);
|
||||
if (response.data.code === 0) {
|
||||
await refreshUserData();
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false };
|
||||
} catch (error) {
|
||||
logger.error('同意加入失败:', error);
|
||||
return { success: false };
|
||||
}
|
||||
};
|
||||
|
||||
const quitTenant = async (tenantId: string) => {
|
||||
if (!userInfo?.id) return;
|
||||
|
||||
try {
|
||||
const response = await userService.deleteTenantUser({
|
||||
tenantId,
|
||||
userId: userInfo.id
|
||||
});
|
||||
if (response.data.code === 0) {
|
||||
await refreshUserData();
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false };
|
||||
} catch (error) {
|
||||
logger.error('退出租户失败:', error);
|
||||
return { success: false };
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
tenantInfo,
|
||||
tenantList,
|
||||
tenantUsers,
|
||||
loading,
|
||||
error,
|
||||
inviteUser,
|
||||
deleteUser,
|
||||
agreeTenant,
|
||||
quitTenant,
|
||||
refreshUserData,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,292 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
Button,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
Chip,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
Alert,
|
||||
Divider,
|
||||
Stack
|
||||
} from '@mui/material';
|
||||
import {
|
||||
PersonAdd as PersonAddIcon,
|
||||
Person as PersonIcon,
|
||||
Group as GroupIcon,
|
||||
Delete as DeleteIcon,
|
||||
Check as CheckIcon,
|
||||
Close as CloseIcon,
|
||||
ExitToApp as ExitToAppIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useTeamSetting } from '@/hooks/setting-hooks';
|
||||
|
||||
interface TenantUser {
|
||||
id: string;
|
||||
nickname: string;
|
||||
email: string;
|
||||
role: string;
|
||||
update_date: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface JoinedTenant {
|
||||
tenant_id: string;
|
||||
nickname: string;
|
||||
email: string;
|
||||
role: string;
|
||||
update_date: string;
|
||||
}
|
||||
|
||||
const TENANT_ROLES = {
|
||||
Owner: 'owner',
|
||||
Invite: 'invite',
|
||||
Normal: 'normal',
|
||||
} as const
|
||||
|
||||
type TenantRole = (typeof TENANT_ROLES)[keyof typeof TENANT_ROLES]
|
||||
|
||||
function TeamsSetting() {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
userInfo,
|
||||
tenantInfo,
|
||||
tenantList,
|
||||
tenantUsers,
|
||||
loading,
|
||||
error,
|
||||
inviteUser,
|
||||
deleteUser,
|
||||
agreeTenant,
|
||||
quitTenant
|
||||
} = useTeamSetting();
|
||||
|
||||
const [inviteDialogOpen, setInviteDialogOpen] = useState(false);
|
||||
const [inviteEmail, setInviteEmail] = useState('');
|
||||
|
||||
const handleInviteUser = async () => {
|
||||
if (!inviteEmail.trim()) return;
|
||||
|
||||
const result = await inviteUser(inviteEmail);
|
||||
if (result?.success) {
|
||||
setInviteDialogOpen(false);
|
||||
setInviteEmail('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteUser = async (userId: string) => {
|
||||
await deleteUser(userId);
|
||||
};
|
||||
|
||||
const handleAgreeTenant = async (tenantId: string) => {
|
||||
await agreeTenant(tenantId);
|
||||
};
|
||||
|
||||
const handleQuitTenant = async (tenantId: string) => {
|
||||
await quitTenant(tenantId);
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleString('zh-CN');
|
||||
};
|
||||
|
||||
const getRoleColor = (role: TenantRole) => {
|
||||
if (!role) return 'primary';
|
||||
// "error" | "primary" | "success" | "info" | "warning" | "secondary" | "default"
|
||||
if (role === 'owner') return 'primary';
|
||||
if (role === 'normal') return 'success';
|
||||
if (role === 'invite') return 'warning';
|
||||
return 'primary';
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Teams Setting</h1>
|
||||
</div>
|
||||
<Box sx={{ p: 3, maxWidth: 1200, mx: 'auto' }}>
|
||||
{/* 工作空间卡片 */}
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h6" component="div">
|
||||
{userInfo?.nickname} {t('setting.workspace')}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<PersonAddIcon />}
|
||||
onClick={() => setInviteDialogOpen(true)}
|
||||
>
|
||||
{t('setting.invite')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 团队成员表格 */}
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 2 }}>
|
||||
<PersonIcon color="primary" />
|
||||
<Typography variant="h6">{t('setting.teamMembers')}</Typography>
|
||||
</Stack>
|
||||
|
||||
<TableContainer component={Paper} variant="outlined">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>{t('common.name')}</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>{t('setting.email')}</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>{t('setting.role')}</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>{t('setting.updateDate')}</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>{t('common.action')}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{tenantUsers.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>{user.nickname}</TableCell>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={user.role}
|
||||
color={getRoleColor(user.role)}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{formatDate(user.update_date)}</TableCell>
|
||||
<TableCell>
|
||||
{user.id !== userInfo?.id && (
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => handleDeleteUser(user.id)}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 加入的团队表格 */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 2 }}>
|
||||
<GroupIcon color="primary" />
|
||||
<Typography variant="h6">{t('setting.joinedTeams')}</Typography>
|
||||
</Stack>
|
||||
|
||||
<TableContainer component={Paper} variant="outlined">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>{t('common.name')}</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>{t('setting.email')}</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>{t('setting.updateDate')}</TableCell>
|
||||
<TableCell sx={{ fontWeight: 'bold' }}>{t('common.action')}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{tenantList.map((tenant) => (
|
||||
<TableRow key={tenant.tenant_id}>
|
||||
<TableCell>{tenant.nickname}</TableCell>
|
||||
<TableCell>{tenant.email}</TableCell>
|
||||
<TableCell>{formatDate(tenant.update_date)}</TableCell>
|
||||
<TableCell>
|
||||
{tenant.role === TENANT_ROLES.Invite ? (
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
color="success"
|
||||
startIcon={<CheckIcon />}
|
||||
onClick={() => handleAgreeTenant(tenant.tenant_id)}
|
||||
>
|
||||
{t('setting.agree')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="error"
|
||||
startIcon={<CloseIcon />}
|
||||
onClick={() => handleQuitTenant(tenant.tenant_id)}
|
||||
>
|
||||
{t('setting.refuse')}
|
||||
</Button>
|
||||
</Stack>
|
||||
) : (
|
||||
tenant.role !== TENANT_ROLES.Owner && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="warning"
|
||||
startIcon={<ExitToAppIcon />}
|
||||
onClick={() => handleQuitTenant(tenant.tenant_id)}
|
||||
>
|
||||
{t('setting.quit')}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 邀请用户对话框 */}
|
||||
<Dialog open={inviteDialogOpen} onClose={() => setInviteDialogOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{t('setting.invite')}</DialogTitle>
|
||||
<DialogContent>
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
label={t('setting.email')}
|
||||
type="email"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={inviteEmail}
|
||||
onChange={(e) => setInviteEmail(e.target.value)}
|
||||
placeholder={t('setting.emailPlaceholder') || '请输入邀请用户的邮箱地址'}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setInviteDialogOpen(false)}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleInviteUser}
|
||||
variant="contained"
|
||||
disabled={loading || !inviteEmail.trim()}
|
||||
>
|
||||
{loading ? t('setting.inviting', 'inviting') : t('setting.invite')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user