Merge branch 'main' of https://gitee.com/wg123/video-gen
This commit is contained in:
Vendored
+6
-6
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -28,7 +28,7 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="/assets/index-Dfcsk8qj.js"></script>
|
<script type="module" crossorigin src="/assets/index-kx3oQI_t.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -456,6 +456,10 @@ export async function updateUserMenus(userId: string, allowedMenus: string[] | n
|
|||||||
await api.put(`/admin/users/${userId}/menus`, { allowed_menus: allowedMenus });
|
await api.put(`/admin/users/${userId}/menus`, { allowed_menus: allowedMenus });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateUserAdminStatus(userId: string, isAdmin: boolean): Promise<void> {
|
||||||
|
await api.put(`/admin/users/${userId}/admin-status`, { is_admin: isAdmin });
|
||||||
|
}
|
||||||
|
|
||||||
export async function resetUserPassword(userId: string, newPassword: string): Promise<void> {
|
export async function resetUserPassword(userId: string, newPassword: string): Promise<void> {
|
||||||
await api.put(`/admin/users/${userId}/reset-password`, { new_password: newPassword });
|
await api.put(`/admin/users/${userId}/reset-password`, { new_password: newPassword });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -287,7 +287,6 @@ const AdminLayout: React.FC = () => {
|
|||||||
bottom: 16,
|
bottom: 16,
|
||||||
left: 16,
|
left: 16,
|
||||||
right: 16,
|
right: 16,
|
||||||
padding: 12,
|
|
||||||
background: 'linear-gradient(135deg, rgba(248, 250, 252, 0.9) 0%, rgba(241, 245, 249, 0.9) 100%)',
|
background: 'linear-gradient(135deg, rgba(248, 250, 252, 0.9) 0%, rgba(241, 245, 249, 0.9) 100%)',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
border: '1px solid rgba(0, 0, 0, 0.06)',
|
border: '1px solid rgba(0, 0, 0, 0.06)',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
Button, Card, Checkbox, Form, Input, InputNumber, message, Modal, Popconfirm, Progress, Select, Space, Switch, Table, Tabs, Tag, Typography,
|
Button, Card, Checkbox, Form, Input, InputNumber, message, Modal, Popconfirm, Progress, Select, Space, Switch, Table, Tabs, Tag, Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
UserOutlined, WalletOutlined, SearchOutlined, StopOutlined, CheckCircleOutlined, PlusOutlined, MenuOutlined, LockOutlined, SettingOutlined, SaveOutlined, DatabaseOutlined, TeamOutlined, PictureOutlined,
|
UserOutlined, WalletOutlined, SearchOutlined, StopOutlined, CheckCircleOutlined, PlusOutlined, MenuOutlined, LockOutlined, SettingOutlined, SaveOutlined, DatabaseOutlined, TeamOutlined, PictureOutlined, SecurityScanOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
adjustCredits,
|
adjustCredits,
|
||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
updateUserTeam,
|
updateUserTeam,
|
||||||
updateSystemConfig,
|
updateSystemConfig,
|
||||||
updateUserMenus,
|
updateUserMenus,
|
||||||
|
updateUserAdminStatus,
|
||||||
} from '../api';
|
} from '../api';
|
||||||
import type { AdminTeamOption, AdminUser, AdminUserResourceCapacityOut, PrivatePortraitConfig, ResourceCapacityUnit, ResourceCapacityUsage, SystemConfig } from '../types';
|
import type { AdminTeamOption, AdminUser, AdminUserResourceCapacityOut, PrivatePortraitConfig, ResourceCapacityUnit, ResourceCapacityUsage, SystemConfig } from '../types';
|
||||||
import { formatDate } from '../utils/formatDate';
|
import { formatDate } from '../utils/formatDate';
|
||||||
@@ -379,6 +380,16 @@ const AdminUsers: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleAdminStatus = async (user: AdminUser) => {
|
||||||
|
try {
|
||||||
|
await updateUserAdminStatus(user.id, !user.isAdmin);
|
||||||
|
message.success(user.isAdmin ? '已取消超级管理员' : '已设为超级管理员');
|
||||||
|
load();
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.message || '设置失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const isAdminTab = activeTab === 'admin';
|
const isAdminTab = activeTab === 'admin';
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
@@ -397,7 +408,7 @@ const AdminUsers: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<div style={{ fontWeight: 600 }}>
|
<div style={{ fontWeight: 600 }}>
|
||||||
{r.username}
|
{r.username}
|
||||||
{r.isAdmin && <Tag color="orange" style={{ marginLeft: 6, fontSize: 10 }}>管理员</Tag>}
|
{r.isAdmin && <Tag color="orange" style={{ marginLeft: 6, fontSize: 10 }}>超级管理员</Tag>}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: '#94a3b8', fontSize: 12 }}>{r.email}</div>
|
<div style={{ color: '#94a3b8', fontSize: 12 }}>{r.email}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -476,7 +487,7 @@ const AdminUsers: React.FC = () => {
|
|||||||
render: (v: string) => <Typography.Text type="secondary" style={{ fontSize: 12 }}>{formatDate(v)}</Typography.Text>,
|
render: (v: string) => <Typography.Text type="secondary" style={{ fontSize: 12 }}>{formatDate(v)}</Typography.Text>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作', key: 'action', width: 540, fixed: 'right' as const,
|
title: '操作', key: 'action', width: 560, fixed: 'right' as const,
|
||||||
render: (_: any, r: AdminUser) => (
|
render: (_: any, r: AdminUser) => (
|
||||||
<Space size={4} wrap>
|
<Space size={4} wrap>
|
||||||
{!isAdminTab && (
|
{!isAdminTab && (
|
||||||
@@ -509,6 +520,17 @@ const AdminUsers: React.FC = () => {
|
|||||||
{!isAdminTab && r.frontendUserKind === 'internal' && (
|
{!isAdminTab && r.frontendUserKind === 'internal' && (
|
||||||
<Button type="link" size="small" onClick={() => handleUpdateFrontendKind(r, 'external')}>取消内部</Button>
|
<Button type="link" size="small" onClick={() => handleUpdateFrontendKind(r, 'external')}>取消内部</Button>
|
||||||
)}
|
)}
|
||||||
|
{isAdminTab && (
|
||||||
|
<Popconfirm
|
||||||
|
title={r.isAdmin ? '确定取消该用户的超级管理员权限?' : '确定将该用户设为超级管理员?'}
|
||||||
|
onConfirm={() => handleToggleAdminStatus(r)}
|
||||||
|
>
|
||||||
|
<Button type="link" size="small" style={{ color: r.isAdmin ? '#f59e0b' : '#6366f1' }}
|
||||||
|
icon={<SecurityScanOutlined />}>
|
||||||
|
{r.isAdmin ? '取消超级管理员' : '设为超级管理员'}
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
<Button type="link" size="small" icon={<MenuOutlined />}
|
<Button type="link" size="small" icon={<MenuOutlined />}
|
||||||
onClick={() => openMenuModal(r)}>
|
onClick={() => openMenuModal(r)}>
|
||||||
菜单权限
|
菜单权限
|
||||||
|
|||||||
@@ -279,6 +279,26 @@ async def update_user_status(
|
|||||||
return {"message": "ok"}
|
return {"message": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/users/{user_id}/admin-status")
|
||||||
|
async def update_user_admin_status(
|
||||||
|
user_id: str,
|
||||||
|
body: dict,
|
||||||
|
admin: User = Depends(get_admin_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
is_admin = body.get("is_admin", False)
|
||||||
|
result = await db.execute(select(User).where(User.id == user_id).limit(1))
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="用户不存在")
|
||||||
|
if user.user_type != "admin":
|
||||||
|
raise HTTPException(status_code=400, detail="仅后台用户支持设置超级管理员状态")
|
||||||
|
user.is_admin = is_admin
|
||||||
|
await db.flush()
|
||||||
|
await log_operation(db, admin.id, admin.username, f"{is_admin and '设为' or '取消'}超级管理员", "PUT", f"/admin/users/{user_id}/admin-status")
|
||||||
|
return {"message": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@router.put("/users/{user_id}/frontend-kind", response_model=AdminUserOut)
|
@router.put("/users/{user_id}/frontend-kind", response_model=AdminUserOut)
|
||||||
async def update_user_frontend_kind(
|
async def update_user_frontend_kind(
|
||||||
user_id: str,
|
user_id: str,
|
||||||
|
|||||||
@@ -245,13 +245,14 @@ async def get_join_info_public(
|
|||||||
|
|
||||||
@router.get("/join-requests", )
|
@router.get("/join-requests", )
|
||||||
async def list_join_requests(
|
async def list_join_requests(
|
||||||
|
status: str | None = Query(None),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
team = await get_managed_team(db, current_user.id)
|
team = await get_managed_team(db, current_user.id)
|
||||||
if not team:
|
if not team:
|
||||||
raise HTTPException(status_code=403, detail="只有团队管理人可查看")
|
raise HTTPException(status_code=403, detail="只有团队管理人可查看")
|
||||||
requests = await team_invitation_service.get_pending_requests(db, team.id)
|
requests = await team_invitation_service.get_all_requests(db, team.id, status)
|
||||||
|
|
||||||
# 获取团队名
|
# 获取团队名
|
||||||
team_name_result = await db.execute(
|
team_name_result = await db.execute(
|
||||||
@@ -269,7 +270,8 @@ async def list_join_requests(
|
|||||||
phone=r.get("phone"),
|
phone=r.get("phone"),
|
||||||
status=r["status"],
|
status=r["status"],
|
||||||
note=r.get("note"),
|
note=r.get("note"),
|
||||||
created_at=r.get("created_at"),
|
created_at=r["created_at"],
|
||||||
|
handled_at=r.get("handled_at"),
|
||||||
)
|
)
|
||||||
for r in requests
|
for r in requests
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -11,5 +11,19 @@ TEAM_STATUS_LABELS = {
|
|||||||
TeamStatus.DISABLED.value: "禁用",
|
TeamStatus.DISABLED.value: "禁用",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TeamJoinRequestStatus(str, Enum):
|
||||||
|
PENDING = "pending"
|
||||||
|
APPROVED = "approved"
|
||||||
|
REJECTED = "rejected"
|
||||||
|
|
||||||
|
|
||||||
|
TEAM_JOIN_REQUEST_STATUS_LABELS = {
|
||||||
|
TeamJoinRequestStatus.PENDING.value: "待处理",
|
||||||
|
TeamJoinRequestStatus.APPROVED.value: "已通过",
|
||||||
|
TeamJoinRequestStatus.REJECTED.value: "已拒绝",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# 前端筛选“未分配团队”时使用的稳定哨兵值,不与真实团队ID混用。
|
# 前端筛选“未分配团队”时使用的稳定哨兵值,不与真实团队ID混用。
|
||||||
TEAM_UNASSIGNED_VALUE = "__none__"
|
TEAM_UNASSIGNED_VALUE = "__none__"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class JoinRequestOut(BaseModel):
|
|||||||
status: str
|
status: str
|
||||||
note: str | None = None
|
note: str | None = None
|
||||||
created_at: NaiveDatetimeOptional = None
|
created_at: NaiveDatetimeOptional = None
|
||||||
|
handled_at: NaiveDatetimeOptional = None
|
||||||
|
|
||||||
model_config = {"from_attributes": True}
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|||||||
@@ -222,6 +222,40 @@ async def get_pending_requests(db: AsyncSession, team_id: str) -> list[dict[str,
|
|||||||
"status": req.status,
|
"status": req.status,
|
||||||
"note": req.note,
|
"note": req.note,
|
||||||
"created_at": req.created_at,
|
"created_at": req.created_at,
|
||||||
|
"handled_at": req.updated_at if req.status != "pending" else None,
|
||||||
|
}
|
||||||
|
for req, username, phone in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_all_requests(
|
||||||
|
db: AsyncSession,
|
||||||
|
team_id: str,
|
||||||
|
status: str | None = None,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""获取团队所有加入申请列表,支持按状态筛选。"""
|
||||||
|
where = [TeamJoinRequest.team_id == team_id]
|
||||||
|
if status:
|
||||||
|
where.append(TeamJoinRequest.status == status)
|
||||||
|
|
||||||
|
result = await db.execute(
|
||||||
|
select(TeamJoinRequest, User.username, User.phone)
|
||||||
|
.join(User, User.id == TeamJoinRequest.user_id)
|
||||||
|
.where(*where)
|
||||||
|
.order_by(TeamJoinRequest.created_at.desc())
|
||||||
|
)
|
||||||
|
rows = result.all()
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": req.id,
|
||||||
|
"team_id": req.team_id,
|
||||||
|
"user_id": req.user_id,
|
||||||
|
"username": username,
|
||||||
|
"phone": phone,
|
||||||
|
"status": req.status,
|
||||||
|
"note": req.note,
|
||||||
|
"created_at": req.created_at,
|
||||||
|
"handled_at": req.updated_at if req.status != "pending" else None,
|
||||||
}
|
}
|
||||||
for req, username, phone in rows
|
for req, username, phone in rows
|
||||||
]
|
]
|
||||||
|
|||||||
+506
File diff suppressed because one or more lines are too long
-506
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -28,7 +28,7 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="/assets/index-C6k2TIpY.js"></script>
|
<script type="module" crossorigin src="/assets/index-BIirSavc.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-JhRVnnL-.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-JhRVnnL-.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -922,8 +922,9 @@ export async function revokeInvitation(invitationId: string): Promise<void> {
|
|||||||
await api.delete(`/team/invitations/${invitationId}`);
|
await api.delete(`/team/invitations/${invitationId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPendingJoinRequests(): Promise<any[]> {
|
export async function getPendingJoinRequests(status?: string): Promise<any[]> {
|
||||||
return api.get('/team/join-requests');
|
const url = status ? `/team/join-requests?status=${status}` : '/team/join-requests';
|
||||||
|
return api.get(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleJoinRequest(requestId: string, action: 'approve' | 'reject', note?: string): Promise<void> {
|
export async function handleJoinRequest(requestId: string, action: 'approve' | 'reject', note?: string): Promise<void> {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
Button, Empty, Form, Input, InputNumber, message, Modal, Pagination, Radio, Select, Space, Table, Tabs, Tag, Tooltip, Typography,
|
Button, Empty, Form, Input, InputNumber, message, Modal, Pagination, Radio, Select, Segmented, Space, Table, Tabs, Tag, Tooltip, Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { DatePicker } from 'antd';
|
import { DatePicker } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@@ -190,18 +190,20 @@ const TeamManagementPage: React.FC = () => {
|
|||||||
// ── Tab 3: 加入申请 ──
|
// ── Tab 3: 加入申请 ──
|
||||||
const [requests, setRequests] = useState<TeamJoinRequest[]>([]);
|
const [requests, setRequests] = useState<TeamJoinRequest[]>([]);
|
||||||
const [reqLoading, setReqLoading] = useState(false);
|
const [reqLoading, setReqLoading] = useState(false);
|
||||||
|
const [reqStatusFilter, setReqStatusFilter] = useState<string>('pending');
|
||||||
const [activeTab, setActiveTab] = useState('members');
|
const [activeTab, setActiveTab] = useState('members');
|
||||||
const initialNoticeShownRef = useRef(false);
|
const initialNoticeShownRef = useRef(false);
|
||||||
const lastRequestCountRef = useRef(0);
|
const lastRequestCountRef = useRef(0);
|
||||||
|
|
||||||
const loadRequests = useCallback(async (isInitial = false) => {
|
const loadRequests = useCallback(async (status: string, isInitial = false) => {
|
||||||
setReqLoading(true);
|
setReqLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await getPendingJoinRequests();
|
const data = await getPendingJoinRequests(status);
|
||||||
const currentCount = data?.length || 0;
|
const currentCount = data?.length || 0;
|
||||||
setRequests(data || []);
|
setRequests(data || []);
|
||||||
|
|
||||||
if (currentCount > 0) {
|
// 只有待处理状态才显示弹窗通知
|
||||||
|
if (status === 'pending' && currentCount > 0) {
|
||||||
if (isInitial && !initialNoticeShownRef.current) {
|
if (isInitial && !initialNoticeShownRef.current) {
|
||||||
initialNoticeShownRef.current = true;
|
initialNoticeShownRef.current = true;
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
@@ -231,7 +233,9 @@ const TeamManagementPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lastRequestCountRef.current = currentCount;
|
if (status === 'pending') {
|
||||||
|
lastRequestCountRef.current = currentCount;
|
||||||
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.error(e?.message || '加载申请失败');
|
message.error(e?.message || '加载申请失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -240,13 +244,13 @@ const TeamManagementPage: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadRequests(true);
|
loadRequests('pending', true);
|
||||||
}, [loadRequests]);
|
}, [loadRequests]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!team) return;
|
if (!team) return;
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
loadRequests(false);
|
loadRequests('pending', false);
|
||||||
}, 60000);
|
}, 60000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [team, loadRequests]);
|
}, [team, loadRequests]);
|
||||||
@@ -255,7 +259,7 @@ const TeamManagementPage: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
await handleJoinRequest(requestId, action, note);
|
await handleJoinRequest(requestId, action, note);
|
||||||
message.success(action === 'approve' ? '已通过' : '已拒绝');
|
message.success(action === 'approve' ? '已通过' : '已拒绝');
|
||||||
loadRequests();
|
loadRequests(reqStatusFilter);
|
||||||
loadMembers();
|
loadMembers();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.error(e?.message || '操作失败');
|
message.error(e?.message || '操作失败');
|
||||||
@@ -397,33 +401,49 @@ const TeamManagementPage: React.FC = () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const renderStatusTag = (status: string) => {
|
||||||
|
const map: Record<string, { color: string; text: string }> = {
|
||||||
|
pending: { color: 'orange', text: '待处理' },
|
||||||
|
approved: { color: 'green', text: '已通过' },
|
||||||
|
rejected: { color: 'red', text: '已拒绝' },
|
||||||
|
};
|
||||||
|
const info = map[status] || { color: 'default', text: status };
|
||||||
|
return <Tag color={info.color}>{info.text}</Tag>;
|
||||||
|
};
|
||||||
|
|
||||||
const reqColumns = [
|
const reqColumns = [
|
||||||
{ title: '申请人', dataIndex: 'username', width: 140, render: (v: string) => <Typography.Text strong>{v}</Typography.Text> },
|
{ title: '申请人', dataIndex: 'username', width: 140, render: (v: string) => <Typography.Text strong>{v}</Typography.Text> },
|
||||||
{ title: '手机号', dataIndex: 'phone', width: 130, render: (v: string) => v || '-' },
|
{ title: '手机号', dataIndex: 'phone', width: 130, render: (v: string) => v || '-' },
|
||||||
|
{ title: '状态', dataIndex: 'status', width: 100, render: (v: string) => renderStatusTag(v) },
|
||||||
{ title: '申请时间', dataIndex: 'createdAt', width: 170, render: (v: string) => formatDateTime(v) },
|
{ title: '申请时间', dataIndex: 'createdAt', width: 170, render: (v: string) => formatDateTime(v) },
|
||||||
|
{ title: '处理时间', dataIndex: 'handledAt', width: 170, render: (v: string) => v ? formatDateTime(v) : '-' },
|
||||||
|
{ title: '备注', dataIndex: 'note', width: 180, ellipsis: true, render: (v: string) => v || '-' },
|
||||||
{
|
{
|
||||||
title: '操作', key: 'action', width: 160,
|
title: '操作', key: 'action', width: 160,
|
||||||
render: (_: any, r: TeamJoinRequest) => (
|
render: (_: any, r: TeamJoinRequest) => {
|
||||||
<Space size={4}>
|
if (r.status !== 'pending') return <span style={{ color: '#94a3b8', fontSize: 12 }}>已处理</span>;
|
||||||
<Button size="small" type="link" icon={<CheckOutlined />} style={{ color: '#16a34a', padding: 0 }} onClick={() => handleRequest(r.id, 'approve')}>通过</Button>
|
return (
|
||||||
<Button size="small" type="link" icon={<CloseOutlined />} danger style={{ padding: 0 }} onClick={() => {
|
<Space size={4}>
|
||||||
Modal.confirm({
|
<Button size="small" type="link" icon={<CheckOutlined />} style={{ color: '#16a34a', padding: 0 }} onClick={() => handleRequest(r.id, 'approve')}>通过</Button>
|
||||||
title: '拒绝申请',
|
<Button size="small" type="link" icon={<CloseOutlined />} danger style={{ padding: 0 }} onClick={() => {
|
||||||
content: (
|
Modal.confirm({
|
||||||
<Form layout="vertical" style={{ marginTop: 12 }}>
|
title: '拒绝申请',
|
||||||
<Form.Item name="note" label="拒绝原因(可选)">
|
content: (
|
||||||
<Input.TextArea rows={2} placeholder="选填" id="reject-note-input" />
|
<Form layout="vertical" style={{ marginTop: 12 }}>
|
||||||
</Form.Item>
|
<Form.Item name="note" label="拒绝原因(可选)">
|
||||||
</Form>
|
<Input.TextArea rows={2} placeholder="选填" id="reject-note-input" />
|
||||||
),
|
</Form.Item>
|
||||||
onOk: () => {
|
</Form>
|
||||||
const note = (document.getElementById('reject-note-input') as HTMLTextAreaElement)?.value || undefined;
|
),
|
||||||
handleRequest(r.id, 'reject', note);
|
onOk: () => {
|
||||||
},
|
const note = (document.getElementById('reject-note-input') as HTMLTextAreaElement)?.value || undefined;
|
||||||
});
|
handleRequest(r.id, 'reject', note);
|
||||||
}}>拒绝</Button>
|
},
|
||||||
</Space>
|
});
|
||||||
),
|
}}>拒绝</Button>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -568,20 +588,47 @@ const TeamManagementPage: React.FC = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'requests',
|
key: 'requests',
|
||||||
label: <Space><HistoryOutlined />加入申请{requests.length > 0 && <Tag color="red">{requests.length}</Tag>}</Space>,
|
label: <Space><HistoryOutlined />加入申请{requests.length > 0 && reqStatusFilter === 'pending' && <Tag color="red">{requests.length}</Tag>}</Space>,
|
||||||
children: (
|
children: (
|
||||||
<div style={tableWrapper}>
|
<>
|
||||||
<Table
|
<div style={{ marginBottom: 16, display: 'flex', alignItems: 'center' }}>
|
||||||
columns={reqColumns}
|
<Segmented
|
||||||
dataSource={requests}
|
value={reqStatusFilter}
|
||||||
rowKey="id"
|
onChange={(v) => {
|
||||||
loading={reqLoading}
|
setReqStatusFilter(v as string);
|
||||||
pagination={false}
|
loadRequests(v as string);
|
||||||
bordered={false}
|
}}
|
||||||
scroll={{ x: 600 }}
|
options={[
|
||||||
locale={{ emptyText: <Empty description="暂无待审批申请" /> }}
|
{ label: '待处理', value: 'pending' },
|
||||||
/>
|
{ label: '已通过', value: 'approved' },
|
||||||
</div>
|
{ label: '已拒绝', value: 'rejected' },
|
||||||
|
{ label: '全部', value: '' },
|
||||||
|
]}
|
||||||
|
style={{
|
||||||
|
borderRadius: 12,
|
||||||
|
border: '1px solid #e2e8f0',
|
||||||
|
padding: 3,
|
||||||
|
background: '#f8fafc',
|
||||||
|
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
|
||||||
|
'--ant-segmented-item-selected-bg': '#6366f1',
|
||||||
|
'--ant-segmented-item-selected-color': '#ffffff',
|
||||||
|
} as React.CSSProperties}
|
||||||
|
size="middle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={tableWrapper}>
|
||||||
|
<Table
|
||||||
|
columns={reqColumns}
|
||||||
|
dataSource={requests}
|
||||||
|
rowKey="id"
|
||||||
|
loading={reqLoading}
|
||||||
|
pagination={false}
|
||||||
|
bordered={false}
|
||||||
|
scroll={{ x: 1000 }}
|
||||||
|
locale={{ emptyText: <Empty description={reqStatusFilter === 'pending' ? '暂无待审批申请' : '暂无记录'} /> }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -599,7 +646,7 @@ const TeamManagementPage: React.FC = () => {
|
|||||||
<Typography.Title level={4} style={{ margin: 0 }}>{team.name}</Typography.Title>
|
<Typography.Title level={4} style={{ margin: 0 }}>{team.name}</Typography.Title>
|
||||||
<Typography.Text type="secondary">代码: {team.code || '-'} 成员: {team.memberCount} 人</Typography.Text>
|
<Typography.Text type="secondary">代码: {team.code || '-'} 成员: {team.memberCount} 人</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Button icon={<ReloadOutlined />} onClick={() => { loadTeam(); loadMembers(); loadInvitations(); loadRequests(); loadCreditRecords(); }}>刷新</Button>
|
<Button icon={<ReloadOutlined />} onClick={() => { loadTeam(); loadMembers(); loadInvitations(); loadRequests(reqStatusFilter); loadCreditRecords(); }}>刷新</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Typography.Text type="secondary">无法获取团队信息</Typography.Text>
|
<Typography.Text type="secondary">无法获取团队信息</Typography.Text>
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export interface TeamJoinRequest {
|
|||||||
status: string;
|
status: string;
|
||||||
note?: string | null;
|
note?: string | null;
|
||||||
createdAt?: string | null;
|
createdAt?: string | null;
|
||||||
|
handledAt?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ManagedTeam {
|
export interface ManagedTeam {
|
||||||
|
|||||||
Reference in New Issue
Block a user