feat(settings): add team management functionality with user invite and team operations

This commit is contained in:
2025-10-22 17:26:08 +08:00
parent 73274300ec
commit a1ac879c6c
2 changed files with 409 additions and 3 deletions

View File

@@ -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,
};
}

View File

@@ -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>
);
}