From 9cef3106f37f71959645d89eaaf226bf7b55b09a Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Tue, 7 Jul 2026 18:58:53 +0800 Subject: [PATCH] 1 --- video-gen-api/app/api/v1/team.py | 6 +- video-gen-api/app/enums/team.py | 14 +++ .../app/schemas/team_join_request.py | 1 + .../app/services/team_invitation_service.py | 34 ++++++ video-gen-app/src/api/index.ts | 5 +- .../src/pages/TeamManagementPage.tsx | 101 ++++++++++++------ video-gen-app/src/types/index.ts | 1 + 7 files changed, 125 insertions(+), 37 deletions(-) diff --git a/video-gen-api/app/api/v1/team.py b/video-gen-api/app/api/v1/team.py index 38f22dd0..91345b76 100644 --- a/video-gen-api/app/api/v1/team.py +++ b/video-gen-api/app/api/v1/team.py @@ -245,13 +245,14 @@ async def get_join_info_public( @router.get("/join-requests", ) async def list_join_requests( + status: str | None = Query(None), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): team = await get_managed_team(db, current_user.id) if not team: 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( @@ -269,7 +270,8 @@ async def list_join_requests( phone=r.get("phone"), status=r["status"], 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 ] diff --git a/video-gen-api/app/enums/team.py b/video-gen-api/app/enums/team.py index ba9fe260..be8f17bd 100644 --- a/video-gen-api/app/enums/team.py +++ b/video-gen-api/app/enums/team.py @@ -11,5 +11,19 @@ TEAM_STATUS_LABELS = { 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混用。 TEAM_UNASSIGNED_VALUE = "__none__" diff --git a/video-gen-api/app/schemas/team_join_request.py b/video-gen-api/app/schemas/team_join_request.py index b715ffb6..0a9e7c9b 100644 --- a/video-gen-api/app/schemas/team_join_request.py +++ b/video-gen-api/app/schemas/team_join_request.py @@ -22,6 +22,7 @@ class JoinRequestOut(BaseModel): status: str note: str | None = None created_at: NaiveDatetimeOptional = None + handled_at: NaiveDatetimeOptional = None model_config = {"from_attributes": True} diff --git a/video-gen-api/app/services/team_invitation_service.py b/video-gen-api/app/services/team_invitation_service.py index 4c9a4e2f..49d93612 100644 --- a/video-gen-api/app/services/team_invitation_service.py +++ b/video-gen-api/app/services/team_invitation_service.py @@ -222,6 +222,40 @@ async def get_pending_requests(db: AsyncSession, team_id: str) -> list[dict[str, "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 + ] + + +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 ] diff --git a/video-gen-app/src/api/index.ts b/video-gen-app/src/api/index.ts index 228ec4b1..67984d6e 100644 --- a/video-gen-app/src/api/index.ts +++ b/video-gen-app/src/api/index.ts @@ -922,8 +922,9 @@ export async function revokeInvitation(invitationId: string): Promise { await api.delete(`/team/invitations/${invitationId}`); } -export async function getPendingJoinRequests(): Promise { - return api.get('/team/join-requests'); +export async function getPendingJoinRequests(status?: string): Promise { + 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 { diff --git a/video-gen-app/src/pages/TeamManagementPage.tsx b/video-gen-app/src/pages/TeamManagementPage.tsx index 54377b2f..4ad52923 100644 --- a/video-gen-app/src/pages/TeamManagementPage.tsx +++ b/video-gen-app/src/pages/TeamManagementPage.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useCallback, useRef } from 'react'; 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'; import { DatePicker } from 'antd'; import dayjs from 'dayjs'; @@ -190,18 +190,20 @@ const TeamManagementPage: React.FC = () => { // ── Tab 3: 加入申请 ── const [requests, setRequests] = useState([]); const [reqLoading, setReqLoading] = useState(false); + const [reqStatusFilter, setReqStatusFilter] = useState('pending'); const [activeTab, setActiveTab] = useState('members'); const initialNoticeShownRef = useRef(false); const lastRequestCountRef = useRef(0); - const loadRequests = useCallback(async (isInitial = false) => { + const loadRequests = useCallback(async (status: string, isInitial = false) => { setReqLoading(true); try { - const data = await getPendingJoinRequests(); + const data = await getPendingJoinRequests(status); const currentCount = data?.length || 0; setRequests(data || []); - if (currentCount > 0) { + // 只有待处理状态才显示弹窗通知 + if (status === 'pending' && currentCount > 0) { if (isInitial && !initialNoticeShownRef.current) { initialNoticeShownRef.current = true; Modal.confirm({ @@ -231,7 +233,9 @@ const TeamManagementPage: React.FC = () => { } } - lastRequestCountRef.current = currentCount; + if (status === 'pending') { + lastRequestCountRef.current = currentCount; + } } catch (e: any) { message.error(e?.message || '加载申请失败'); } finally { @@ -240,13 +244,13 @@ const TeamManagementPage: React.FC = () => { }, []); useEffect(() => { - loadRequests(true); + loadRequests('pending', true); }, [loadRequests]); useEffect(() => { if (!team) return; const interval = setInterval(() => { - loadRequests(false); + loadRequests('pending', false); }, 60000); return () => clearInterval(interval); }, [team, loadRequests]); @@ -255,7 +259,7 @@ const TeamManagementPage: React.FC = () => { try { await handleJoinRequest(requestId, action, note); message.success(action === 'approve' ? '已通过' : '已拒绝'); - loadRequests(); + loadRequests(reqStatusFilter); loadMembers(); } catch (e: any) { message.error(e?.message || '操作失败'); @@ -397,33 +401,49 @@ const TeamManagementPage: React.FC = () => { }, ]; + const renderStatusTag = (status: string) => { + const map: Record = { + pending: { color: 'orange', text: '待处理' }, + approved: { color: 'green', text: '已通过' }, + rejected: { color: 'red', text: '已拒绝' }, + }; + const info = map[status] || { color: 'default', text: status }; + return {info.text}; + }; + const reqColumns = [ { title: '申请人', dataIndex: 'username', width: 140, 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: '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, - render: (_: any, r: TeamJoinRequest) => ( - - - - - ), + render: (_: any, r: TeamJoinRequest) => { + if (r.status !== 'pending') return 已处理; + return ( + + + + + ); + }, }, ]; @@ -568,9 +588,24 @@ const TeamManagementPage: React.FC = () => { }, { key: 'requests', - label: 加入申请{requests.length > 0 && {requests.length}}, + label: 加入申请{requests.length > 0 && reqStatusFilter === 'pending' && {requests.length}}, children: (
+
+ { + setReqStatusFilter(v as string); + loadRequests(v as string); + }} + options={[ + { label: '待处理', value: 'pending' }, + { label: '已通过', value: 'approved' }, + { label: '已拒绝', value: 'rejected' }, + { label: '全部', value: '' }, + ]} + /> +
{ loading={reqLoading} pagination={false} bordered={false} - scroll={{ x: 600 }} - locale={{ emptyText: }} + scroll={{ x: 1000 }} + locale={{ emptyText: }} /> ), @@ -599,7 +634,7 @@ const TeamManagementPage: React.FC = () => { {team.name} 代码: {team.code || '-'} 成员: {team.memberCount} 人 - + ) : ( 无法获取团队信息 diff --git a/video-gen-app/src/types/index.ts b/video-gen-app/src/types/index.ts index e8e2312d..da6fc64e 100644 --- a/video-gen-app/src/types/index.ts +++ b/video-gen-app/src/types/index.ts @@ -59,6 +59,7 @@ export interface TeamJoinRequest { status: string; note?: string | null; createdAt?: string | null; + handledAt?: string | null; } export interface ManagedTeam {