From 16b3d223c100bb84fc0cddb0d290defdff9fc35d Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Mon, 6 Jul 2026 13:38:27 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E5=90=8E=E5=8F=B0/teams=E9=9C=80?= =?UTF-8?q?=E8=A6=81=E5=8F=AF=E4=BB=A5=E8=AE=BE=E7=BD=AE=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E4=BA=BA=EF=BC=8C=E5=90=8E=E5=8F=B0=E5=8F=AA=E7=BB=99=E8=BF=99?= =?UTF-8?q?=E4=B8=AA=E7=AE=A1=E7=90=86=E4=BA=BA=E5=88=86=E9=85=8D=E7=A7=AF?= =?UTF-8?q?=E5=88=86=EF=BC=8C=E7=84=B6=E5=90=8E=E5=89=8D=E5=8F=B0=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=91=98=E8=87=AA=E5=B7=B1=E5=88=86=E9=85=8D=E7=A7=AF?= =?UTF-8?q?=E5=88=86=202=E3=80=81=E5=89=8D=E5=8F=B0=E5=88=A4=E6=96=AD?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=98=AF=E7=AE=A1=E7=90=86=E4=BA=BA=EF=BC=8C?= =?UTF-8?q?=E5=B7=A6=E4=B8=8B=E8=A7=92=E6=98=BE=E7=A4=BA=E5=9B=A2=E9=98=9F?= =?UTF-8?q?=E7=AE=A1=E7=90=86=203=E3=80=81=E5=9B=A2=E9=98=9F=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=8F=AF=E4=BB=A5=E7=9C=8B=E5=88=B0=E5=9B=A2=E9=98=9F?= =?UTF-8?q?=E7=9A=84=E4=BA=BA=E5=91=98=E3=80=81=E5=88=86=E9=85=8D=E4=BA=BA?= =?UTF-8?q?=E5=91=98=E7=A7=AF=E5=88=86=E3=80=81=E4=BA=BA=E5=91=98=E7=9A=84?= =?UTF-8?q?=E7=A7=AF=E5=88=86=E6=83=85=E5=86=B5=E7=9A=84=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=204=E3=80=81=E5=9B=A2=E9=98=9F=E7=AE=A1=E7=90=86=E8=BF=98?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E9=82=80=E8=AF=B7=E7=94=A8=E6=88=B7=EF=BC=8C?= =?UTF-8?q?=E6=AF=94=E5=A6=82=E7=94=9F=E6=88=90=E4=B8=AA=E9=93=BE=E6=8E=A5?= =?UTF-8?q?=EF=BC=8C=E6=9C=AA=E6=B3=A8=E5=86=8C=E9=9C=80=E8=A6=81=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E7=BB=91=E5=AE=9A=E5=9B=A2=E9=98=9F=EF=BC=8C=E5=B7=B2?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E7=94=A8=E6=88=B7=E8=AE=BF=E9=97=AE=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E6=98=AF=E5=90=A6=E5=8A=A0=E5=85=A5=E8=BF=99=E4=B8=AA?= =?UTF-8?q?=E5=9B=A2=E9=98=9F=EF=BC=8C=E7=84=B6=E5=90=8E=E9=83=BD=E9=9C=80?= =?UTF-8?q?=E8=A6=81=E7=AE=A1=E7=90=86=E5=90=8C=E6=84=8F=E6=89=8D=E5=8F=AF?= =?UTF-8?q?=E4=BB=A5=E5=8A=A0=E5=85=A5=E5=9B=A2=E9=98=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- video-gen-admin/src/api/index.ts | 13 + video-gen-admin/src/pages/AdminTeams.tsx | 88 ++++- video-gen-admin/src/types/index.ts | 2 + .../20260706_20260706_团队管理人和邀请系统.py | 82 ++++ video-gen-api/app/api/admin/team.py | 91 +++-- video-gen-api/app/api/v1/__init__.py | 2 + video-gen-api/app/api/v1/auth.py | 17 +- video-gen-api/app/api/v1/team.py | 255 +++++++++++++ video-gen-api/app/models/__init__.py | 4 +- video-gen-api/app/models/team.py | 5 +- video-gen-api/app/models/team_invitation.py | 22 ++ video-gen-api/app/models/team_join_request.py | 19 + video-gen-api/app/schemas/team.py | 2 + video-gen-api/app/schemas/team_invitation.py | 22 ++ .../app/schemas/team_join_request.py | 33 ++ video-gen-api/app/schemas/team_manager.py | 37 ++ video-gen-api/app/schemas/user.py | 3 + .../app/services/team_invitation_service.py | 235 ++++++++++++ .../app/services/team_manager_service.py | 183 +++++++++ video-gen-app/src/App.tsx | 4 + video-gen-app/src/api/index.ts | 44 +++ .../src/components/Layout/AppLayout.tsx | 10 + video-gen-app/src/pages/JoinTeamPage.tsx | 121 ++++++ .../src/pages/TeamManagementPage.tsx | 351 ++++++++++++++++++ video-gen-app/src/types/index.ts | 55 +++ 25 files changed, 1671 insertions(+), 29 deletions(-) create mode 100644 video-gen-api/alembic/versions/20260706_20260706_团队管理人和邀请系统.py create mode 100644 video-gen-api/app/api/v1/team.py create mode 100644 video-gen-api/app/models/team_invitation.py create mode 100644 video-gen-api/app/models/team_join_request.py create mode 100644 video-gen-api/app/schemas/team_invitation.py create mode 100644 video-gen-api/app/schemas/team_join_request.py create mode 100644 video-gen-api/app/schemas/team_manager.py create mode 100644 video-gen-api/app/services/team_invitation_service.py create mode 100644 video-gen-api/app/services/team_manager_service.py create mode 100644 video-gen-app/src/pages/JoinTeamPage.tsx create mode 100644 video-gen-app/src/pages/TeamManagementPage.tsx diff --git a/video-gen-admin/src/api/index.ts b/video-gen-admin/src/api/index.ts index 7af2799a..dc40e4fc 100644 --- a/video-gen-admin/src/api/index.ts +++ b/video-gen-admin/src/api/index.ts @@ -180,6 +180,19 @@ export async function deleteAdminTeam(id: string): Promise { await api.delete(`/admin/teams/${id}`); } +export async function setTeamManager(teamId: string, userId: string | null): Promise { + return api.put(`/admin/teams/${teamId}/manager`, { user_id: userId }); +} + +export async function getTeamMembersForAdmin(teamId: string, page = 1, pageSize = 50): Promise<{ items: AdminUser[]; total: number }> { + const params = new URLSearchParams(); + params.set('page', String(page)); + params.set('page_size', String(pageSize)); + params.set('user_type', 'frontend'); + params.set('team_id', teamId); + return api.get(`/admin/users?${params.toString()}`); +} + export async function adjustCredits(userId: string, amount: number, description: string): Promise { await api.post(`/admin/users/${userId}/credits`, { amount, description }); } diff --git a/video-gen-admin/src/pages/AdminTeams.tsx b/video-gen-admin/src/pages/AdminTeams.tsx index e74faba0..55df1a30 100644 --- a/video-gen-admin/src/pages/AdminTeams.tsx +++ b/video-gen-admin/src/pages/AdminTeams.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useMemo, useState } from 'react'; import { Button, Card, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Table, Tag, Typography } from 'antd'; -import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined, SearchOutlined, TeamOutlined } from '@ant-design/icons'; -import { deleteAdminTeam, getAdminTeams, saveAdminTeam } from '../api'; -import type { AdminTeam } from '../types'; +import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined, SearchOutlined, SettingOutlined, TeamOutlined, UserOutlined } from '@ant-design/icons'; +import { deleteAdminTeam, getAdminTeams, getTeamMembersForAdmin, saveAdminTeam, setTeamManager } from '../api'; +import type { AdminTeam, AdminUser } from '../types'; import { formatDate } from '../utils/formatDate'; const statusOptions = [ @@ -34,6 +34,12 @@ const AdminTeams: React.FC = () => { const [modal, setModal] = useState<{ open: boolean; item: AdminTeam | null }>({ open: false, item: null }); const [form] = Form.useForm(); + // 管理人弹窗状态 + const [managerModal, setManagerModal] = useState<{ open: boolean; team: AdminTeam | null }>({ open: false, team: null }); + const [members, setMembers] = useState([]); + const [managerLoading, setManagerLoading] = useState(false); + const [selectedManagerId, setSelectedManagerId] = useState(null); + const query = useMemo(() => ({ page, pageSize, @@ -107,6 +113,36 @@ const AdminTeams: React.FC = () => { } }; + const openManagerModal = async (team: AdminTeam) => { + setManagerModal({ open: true, team }); + setSelectedManagerId(team.managerId || null); + setManagerLoading(true); + try { + const res = await getTeamMembersForAdmin(team.id); + setMembers(res.items || []); + } catch (e: any) { + message.error(e?.message || '加载成员失败'); + setMembers([]); + } finally { + setManagerLoading(false); + } + }; + + const handleSetManager = async () => { + if (!managerModal.team) return; + try { + setManagerLoading(true); + await setTeamManager(managerModal.team.id, selectedManagerId); + message.success(selectedManagerId ? '已设置管理人' : '已取消管理人'); + setManagerModal({ open: false, team: null }); + load(); + } catch (e: any) { + message.error(e?.message || '设置失败'); + } finally { + setManagerLoading(false); + } + }; + const columns = [ { title: '团队名称', @@ -136,6 +172,16 @@ const AdminTeams: React.FC = () => { width: 100, render: (v: number) => {Number(v || 0).toLocaleString()}, }, + { + title: '管理人', + key: 'manager', + width: 140, + render: (_: any, r: AdminTeam) => ( + + {r.managerName || 未设置} + + ), + }, { title: '排序', dataIndex: 'sortOrder', @@ -161,6 +207,7 @@ const AdminTeams: React.FC = () => { fixed: 'right' as const, render: (_: any, r: AdminTeam) => ( + { + + {/* 设置管理人弹窗 */} + 设置团队管理人} + open={managerModal.open} + confirmLoading={managerLoading} + onOk={handleSetManager} + onCancel={() => { setManagerModal({ open: false, team: null }); setSelectedManagerId(null); }} + okText="保存" + cancelText="取消" + width={480} + > +
+ + 团队:{managerModal.team?.name || '-'} + + + 选择该团队的一名成员作为管理人。管理人可在前台管理团队成员和分配积分。 + + + + + +
+ ); +}; + +export default TeamManagementPage; diff --git a/video-gen-app/src/types/index.ts b/video-gen-app/src/types/index.ts index 38260eb0..ca8e2ef7 100644 --- a/video-gen-app/src/types/index.ts +++ b/video-gen-app/src/types/index.ts @@ -22,6 +22,61 @@ export interface User { allowedMenus?: string[] | null; mustSetPassword?: boolean; resourceCapacity?: ResourceCapacity; + teamId?: string | null; + teamName?: string | null; + isTeamManager?: boolean; +} + +// ── Team Management Types ───────────────────────────── + +export interface TeamMember { + id: string; + username: string; + phone?: string | null; + credits: number; + isActive: boolean; + joinedAt: string; +} + +export interface TeamInvitation { + id: string; + code: string; + status: string; + maxUses?: number | null; + useCount: number; + expiresAt?: string | null; + inviteLink: string; + createdAt?: string | null; +} + +export interface TeamJoinRequest { + id: string; + teamId: string; + teamName: string; + userId: string; + username: string; + phone?: string | null; + status: string; + note?: string | null; + createdAt?: string | null; +} + +export interface ManagedTeam { + id: string; + name: string; + code?: string | null; + description?: string | null; + status: string; + memberCount: number; + managerId?: string | null; + managerName?: string | null; +} + +export interface JoinTeamInfo { + teamName: string; + teamId: string; + valid: boolean; + alreadyInTeam: boolean; } export interface CreditRecord {