1、生成邀请码默认过期时间设置一天
2、邀请链接内容在列表没有显示完全,复制也没有生效 3、检查用户通过邀请链接访问是否未注册需要注册登陆,如果有账户直接登陆直接进行申请,如果已经登陆直接弹窗显示是否加入具体团队,避免单用户多次提交申请 4、如果团队负责人有未处理的加入申请,弹窗通知
This commit is contained in:
@@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from starlette.responses import StreamingResponse
|
||||
|
||||
from app.config import settings
|
||||
from app.dependencies import get_current_user, get_db
|
||||
from app.dependencies import get_current_user, get_db, get_optional_current_user
|
||||
from app.models.team import Team
|
||||
from app.models.team_invitation import TeamInvitation
|
||||
from app.models.team_join_request import TeamJoinRequest
|
||||
@@ -189,7 +189,7 @@ async def join_by_code(
|
||||
@router.get("/join-info", )
|
||||
async def get_join_info(
|
||||
code: str = Query(...),
|
||||
current_user: User = Depends(get_current_user),
|
||||
current_user: User | None = Depends(get_optional_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""验证邀请码并返回团队信息(用于加入页面展示)。"""
|
||||
@@ -202,16 +202,52 @@ async def get_join_info(
|
||||
)
|
||||
team_name = team.scalar_one_or_none() or ""
|
||||
|
||||
already_in_team = current_user.team_id == invitation.team_id
|
||||
already_in_team = current_user and current_user.team_id == invitation.team_id
|
||||
|
||||
has_pending_request = False
|
||||
if current_user:
|
||||
from app.models.team_join_request import TeamJoinRequest
|
||||
pending = await db.execute(
|
||||
select(TeamJoinRequest).where(
|
||||
TeamJoinRequest.user_id == current_user.id,
|
||||
TeamJoinRequest.team_id == invitation.team_id,
|
||||
TeamJoinRequest.status == "pending",
|
||||
).limit(1)
|
||||
)
|
||||
has_pending = pending.scalar_one_or_none()
|
||||
has_pending_request = has_pending is not None
|
||||
|
||||
return JoinTeamInfoOut(
|
||||
team_name=team_name,
|
||||
team_id=invitation.team_id,
|
||||
valid=True,
|
||||
already_in_team=already_in_team,
|
||||
has_pending_request=has_pending_request,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/join-info/public", )
|
||||
async def get_join_info_public(
|
||||
code: str = Query(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""公开接口:验证邀请码并返回团队信息(无需登录)。"""
|
||||
invitation = await team_invitation_service.get_invitation_by_code(db, code)
|
||||
if not invitation:
|
||||
return {"team_name": "", "team_id": "", "valid": False}
|
||||
|
||||
team = await db.execute(
|
||||
select(Team.name).where(Team.id == invitation.team_id, Team.deleted_at.is_(None)).limit(1)
|
||||
)
|
||||
team_name = team.scalar_one_or_none() or ""
|
||||
|
||||
return {
|
||||
"team_name": team_name,
|
||||
"team_id": invitation.team_id,
|
||||
"valid": True,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/join-requests", )
|
||||
async def list_join_requests(
|
||||
current_user: User = Depends(get_current_user),
|
||||
|
||||
@@ -70,6 +70,31 @@ async def get_current_user(
|
||||
return current_user
|
||||
|
||||
|
||||
async def get_optional_current_user(
|
||||
credentials: HTTPAuthorizationCredentials | None = Depends(security),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> User | None:
|
||||
if not credentials:
|
||||
return None
|
||||
|
||||
user_id = decode_access_token(credentials.credentials)
|
||||
if not user_id:
|
||||
return None
|
||||
|
||||
if user_id.startswith("captcha:"):
|
||||
return None
|
||||
|
||||
result = await db.execute(select(User).where(User.id == user_id).limit(1))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user or not user.is_active:
|
||||
return None
|
||||
|
||||
if user_must_set_password(user):
|
||||
return None
|
||||
|
||||
return user
|
||||
|
||||
|
||||
async def get_admin_user(
|
||||
current_user: User = Depends(get_current_user_allow_password_pending),
|
||||
) -> User:
|
||||
|
||||
@@ -31,3 +31,4 @@ class JoinTeamInfoOut(BaseModel):
|
||||
team_id: str
|
||||
valid: bool
|
||||
already_in_team: bool = False
|
||||
has_pending_request: bool = False
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
from fastapi import HTTPException
|
||||
@@ -46,6 +46,8 @@ async def create_invitation(
|
||||
"""创建邀请码(仅团队管理人)。"""
|
||||
await _assert_is_manager(db, created_by, team_id)
|
||||
code = _generate_invite_code()
|
||||
if expires_at is None:
|
||||
expires_at = datetime.now(timezone.utc) + timedelta(days=1)
|
||||
invitation = TeamInvitation(
|
||||
id=generate_id(),
|
||||
team_id=team_id,
|
||||
|
||||
+113
-113
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -28,7 +28,7 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-DzBPiPEO.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BriVPrZ6.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D9_3MPsN.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -92,6 +92,7 @@ const App = () => {
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/join-team" element={<JoinTeamPage />} />
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
@@ -127,7 +128,6 @@ const App = () => {
|
||||
<Route path="authacc" element={<AuthAccountPage />} />
|
||||
<Route path="creativeplaza" element={<CreativePlazaPage />} />
|
||||
<Route path="team-management" element={<TeamManagementPage />} />
|
||||
<Route path="join-team" element={<JoinTeamPage />} />
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/projects" replace />} />
|
||||
</Routes>
|
||||
|
||||
@@ -843,7 +843,11 @@ export async function handleJoinRequest(requestId: string, action: 'approve' | '
|
||||
}
|
||||
|
||||
export async function getJoinTeamInfo(code: string): Promise<any> {
|
||||
return api.get(`/team/join-info?code=${encodeURIComponent(code)}`);
|
||||
return api.get(`/team/join-info?code=${encodeURIComponent(code)}`, { auth: true, skipAuthRedirect: true });
|
||||
}
|
||||
|
||||
export async function getJoinTeamInfoPublic(code: string): Promise<any> {
|
||||
return api.get(`/team/join-info/public?code=${encodeURIComponent(code)}`, false);
|
||||
}
|
||||
|
||||
export async function submitJoinRequest(code: string): Promise<void> {
|
||||
|
||||
@@ -1,54 +1,76 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Card, Result, Spin, Typography, Modal, message } from 'antd';
|
||||
import { CheckCircleOutlined, TeamOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button, Card, Result, Spin, Typography, Modal, message, Space,
|
||||
} from 'antd';
|
||||
import {
|
||||
CheckCircleOutlined, TeamOutlined, LoginOutlined, UserAddOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { getJoinTeamInfo, submitJoinRequest } from '../api';
|
||||
import type { JoinTeamInfo } from '../types';
|
||||
import { useAuthStore } from '../store/useAuthStore';
|
||||
|
||||
const JoinTeamPage: React.FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const code = searchParams.get('code') || '';
|
||||
const { user, loading: authLoading, checkAuth } = useAuthStore();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [infoLoading, setInfoLoading] = useState(true);
|
||||
const [info, setInfo] = useState<JoinTeamInfo | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [confirmModalOpen, setConfirmModalOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
checkAuth();
|
||||
}, [checkAuth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!code) {
|
||||
setLoading(false);
|
||||
setInfoLoading(false);
|
||||
return;
|
||||
}
|
||||
if (authLoading) return;
|
||||
|
||||
setInfoLoading(true);
|
||||
getJoinTeamInfo(code)
|
||||
.then((data) => setInfo(data))
|
||||
.then((data) => {
|
||||
setInfo(data);
|
||||
if (data?.valid && user && !data.alreadyInTeam && !data.hasPendingRequest) {
|
||||
setConfirmModalOpen(true);
|
||||
}
|
||||
})
|
||||
.catch(() => setInfo(null))
|
||||
.finally(() => setLoading(false));
|
||||
}, [code]);
|
||||
.finally(() => setInfoLoading(false));
|
||||
}, [code, user, authLoading]);
|
||||
|
||||
const handleJoin = async () => {
|
||||
if (!code) return;
|
||||
Modal.confirm({
|
||||
title: '确认加入团队',
|
||||
icon: <TeamOutlined style={{ color: '#6366f1' }} />,
|
||||
content: info?.teamName ? `您确定要加入团队「${info.teamName}」吗?提交后需等待团队管理人审批。` : '您确定要加入该团队吗?',
|
||||
okText: '确认加入',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
setSubmitting(true);
|
||||
await submitJoinRequest(code);
|
||||
setSubmitted(true);
|
||||
setConfirmModalOpen(false);
|
||||
message.success('申请已提交');
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '申请失败');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
const handleLogin = () => {
|
||||
const redirect = encodeURIComponent(window.location.pathname + window.location.search);
|
||||
navigate(`/login?redirect=${redirect}`);
|
||||
};
|
||||
|
||||
const handleRegister = () => {
|
||||
const redirect = encodeURIComponent(window.location.pathname + window.location.search);
|
||||
navigate(`/login?tab=register&redirect=${redirect}`);
|
||||
};
|
||||
|
||||
if (authLoading || infoLoading) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '60vh' }}>
|
||||
<Spin size="large" />
|
||||
@@ -64,7 +86,14 @@ const JoinTeamPage: React.FC = () => {
|
||||
icon={<CheckCircleOutlined style={{ color: '#6366f1' }} />}
|
||||
title="申请已提交"
|
||||
subTitle="您的加入申请已提交,请等待团队管理人审批。审批通过后将自动加入团队。"
|
||||
extra={<Button type="primary" onClick={() => navigate('/projects')}>返回首页</Button>}
|
||||
extra={
|
||||
<Space>
|
||||
<Button type="primary" onClick={() => navigate('/projects')}>返回首页</Button>
|
||||
{user && (
|
||||
<Button onClick={() => navigate('/team-management')}>团队管理</Button>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -90,15 +119,42 @@ const JoinTeamPage: React.FC = () => {
|
||||
status="info"
|
||||
title="您已在此团队中"
|
||||
subTitle={`您已经是「${info.teamName}」的成员了,无需再次加入。`}
|
||||
extra={<Button type="primary" onClick={() => navigate('/projects')}>返回首页</Button>}
|
||||
extra={
|
||||
<Space>
|
||||
<Button type="primary" onClick={() => navigate('/projects')}>返回首页</Button>
|
||||
<Button onClick={() => navigate('/team-management')}>团队管理</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (info.hasPendingRequest) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 40 }}>
|
||||
<Result
|
||||
status="info"
|
||||
title="申请待审批"
|
||||
subTitle={`您已提交加入「${info.teamName}」的申请,请等待团队管理人审批。`}
|
||||
extra={
|
||||
<Space>
|
||||
<Button type="primary" onClick={() => navigate('/projects')}>返回首页</Button>
|
||||
<Button onClick={() => navigate('/team-management')}>团队管理</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 40 }}>
|
||||
<Card variant="outlined" style={{ borderRadius: 16, maxWidth: 480, width: '100%', textAlign: 'center' }}>
|
||||
<Card
|
||||
variant="outlined"
|
||||
style={{ borderRadius: 16, maxWidth: 480, width: '100%', textAlign: 'center' }}
|
||||
>
|
||||
<TeamOutlined style={{ fontSize: 48, color: '#6366f1', marginBottom: 16 }} />
|
||||
<Typography.Title level={3}>加入团队</Typography.Title>
|
||||
<Typography.Text style={{ fontSize: 16, color: '#475569', display: 'block', marginBottom: 8 }}>
|
||||
@@ -110,11 +166,67 @@ const JoinTeamPage: React.FC = () => {
|
||||
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 24 }}>
|
||||
加入后团队管理人可以为您分配积分、查看您的积分使用情况。
|
||||
</Typography.Text>
|
||||
<Button type="primary" size="large" block loading={submitting} onClick={handleJoin}>
|
||||
|
||||
{user ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
loading={submitting}
|
||||
onClick={() => setConfirmModalOpen(true)}
|
||||
>
|
||||
申请加入
|
||||
</Button>
|
||||
) : (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={12}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
icon={<LoginOutlined />}
|
||||
onClick={handleLogin}
|
||||
>
|
||||
登录后申请加入
|
||||
</Button>
|
||||
<Button
|
||||
size="large"
|
||||
block
|
||||
icon={<UserAddOutlined />}
|
||||
onClick={handleRegister}
|
||||
>
|
||||
注册新账号
|
||||
</Button>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
登录或注册后将自动提交加入申请
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
<TeamOutlined style={{ color: '#6366f1' }} />
|
||||
确认加入团队
|
||||
</Space>
|
||||
}
|
||||
open={confirmModalOpen}
|
||||
onOk={handleJoin}
|
||||
onCancel={() => setConfirmModalOpen(false)}
|
||||
okText="确认加入"
|
||||
cancelText="取消"
|
||||
confirmLoading={submitting}
|
||||
width={420}
|
||||
>
|
||||
<p style={{ marginBottom: 0 }}>
|
||||
您确定要加入团队 <strong style={{ color: '#6366f1' }}>「{info.teamName}」</strong> 吗?
|
||||
</p>
|
||||
<p style={{ marginTop: 8, color: '#64748b', fontSize: 13 }}>
|
||||
提交后需等待团队管理人审批,审批通过后将自动加入团队。
|
||||
</p>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
PlayCircleOutlined, BulbOutlined, HistoryOutlined,
|
||||
MobileOutlined, SafetyOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useAuthStore } from '../store/useAuthStore';
|
||||
import { sendSms,phonelogin, getSiteInfo, register } from '../api';
|
||||
import './LoginPage.css';
|
||||
@@ -17,6 +17,9 @@ const LoginPage: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [mode, setMode] = useState<'password' | 'phone' | 'register'>('password');
|
||||
const [tab, setTab] = useState<'password' | 'phone'>('password');
|
||||
const [searchParams] = useSearchParams();
|
||||
const redirect = searchParams.get('redirect');
|
||||
const tabParam = searchParams.get('tab');
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
const [regCountdown, setRegCountdown] = useState(0);
|
||||
const [agreed, setAgreed] = useState(false);
|
||||
@@ -62,6 +65,23 @@ const LoginPage: React.FC = () => {
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (tabParam === 'register') {
|
||||
setMode('register');
|
||||
}
|
||||
}, [tabParam]);
|
||||
|
||||
const goToRedirect = () => {
|
||||
if (redirect) {
|
||||
try {
|
||||
const decoded = decodeURIComponent(redirect);
|
||||
navigate(decoded);
|
||||
return;
|
||||
} catch {}
|
||||
}
|
||||
navigate('/home');
|
||||
};
|
||||
|
||||
const checkAgreed = (): boolean => {
|
||||
if (!agreed) {
|
||||
message.warning('请先阅读并同意用户协议及隐私政策');
|
||||
@@ -77,7 +97,7 @@ const LoginPage: React.FC = () => {
|
||||
await login(values.phone, values.password, undefined, values.rememberMe);
|
||||
message.success('登录成功,欢迎回来');
|
||||
await checkAuth();
|
||||
navigate('/home');
|
||||
goToRedirect();
|
||||
} catch (error: any) {
|
||||
const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '登录失败';
|
||||
message.error(errorMsg);
|
||||
@@ -97,7 +117,7 @@ const LoginPage: React.FC = () => {
|
||||
await phonelogin(values.phone, values.code);
|
||||
message.success('登录成功,欢迎回来');
|
||||
await checkAuth();
|
||||
navigate('/home');
|
||||
goToRedirect();
|
||||
} catch (error: any) {
|
||||
const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '登录失败';
|
||||
message.error(errorMsg);
|
||||
@@ -114,7 +134,7 @@ const LoginPage: React.FC = () => {
|
||||
const user = await register(values.phone, values.regCode, values.password);
|
||||
message.success('注册成功');
|
||||
await checkAuth();
|
||||
navigate('/home');
|
||||
goToRedirect();
|
||||
} catch (error: any) {
|
||||
const errorMsg = error?.response?.data?.detail || error?.response?.data?.message || error?.message || '注册失败';
|
||||
message.error(errorMsg);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
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,
|
||||
} from 'antd';
|
||||
import { DatePicker } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
CopyOutlined, DownloadOutlined, PlusOutlined, ReloadOutlined, UserOutlined, HistoryOutlined, WalletOutlined,
|
||||
CopyOutlined, DownloadOutlined, PlusOutlined, ReloadOutlined, UserOutlined, HistoryOutlined, WalletOutlined, BellOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
@@ -159,22 +159,80 @@ const TeamManagementPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const copyInviteLink = (link: string) => {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(link).then(() => {
|
||||
message.success('邀请链接已复制');
|
||||
}).catch(() => {
|
||||
message.warning('复制失败,请手动复制');
|
||||
fallbackCopy(link);
|
||||
});
|
||||
} else {
|
||||
fallbackCopy(link);
|
||||
}
|
||||
};
|
||||
|
||||
const fallbackCopy = (text: string) => {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-9999px';
|
||||
textArea.style.top = '-9999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
message.success('邀请链接已复制');
|
||||
} catch {
|
||||
message.warning('复制失败,请手动复制');
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
};
|
||||
|
||||
// ── Tab 3: 加入申请 ──
|
||||
const [requests, setRequests] = useState<TeamJoinRequest[]>([]);
|
||||
const [reqLoading, setReqLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('members');
|
||||
const initialNoticeShownRef = useRef(false);
|
||||
const lastRequestCountRef = useRef(0);
|
||||
|
||||
const loadRequests = useCallback(async () => {
|
||||
const loadRequests = useCallback(async (isInitial = false) => {
|
||||
setReqLoading(true);
|
||||
try {
|
||||
const data = await getPendingJoinRequests();
|
||||
const currentCount = data?.length || 0;
|
||||
setRequests(data || []);
|
||||
|
||||
if (currentCount > 0) {
|
||||
if (isInitial && !initialNoticeShownRef.current) {
|
||||
initialNoticeShownRef.current = true;
|
||||
Modal.confirm({
|
||||
title: (
|
||||
<Space>
|
||||
<BellOutlined style={{ color: '#f59e0b' }} />
|
||||
待处理的加入申请
|
||||
</Space>
|
||||
),
|
||||
content: (
|
||||
<div>
|
||||
<p>您有 <strong style={{ color: '#ef4444' }}>{currentCount}</strong> 条待处理的团队加入申请。</p>
|
||||
<p style={{ color: '#64748b', fontSize: 13, marginBottom: 0 }}>请及时处理新成员的加入申请。</p>
|
||||
</div>
|
||||
),
|
||||
okText: '立即处理',
|
||||
cancelText: '稍后处理',
|
||||
onOk: () => {
|
||||
setActiveTab('requests');
|
||||
},
|
||||
});
|
||||
} else if (!isInitial && currentCount > lastRequestCountRef.current) {
|
||||
message.info({
|
||||
content: `有 ${currentCount - lastRequestCountRef.current} 条新的加入申请待处理`,
|
||||
duration: 5,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
lastRequestCountRef.current = currentCount;
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '加载申请失败');
|
||||
} finally {
|
||||
@@ -182,7 +240,17 @@ const TeamManagementPage: React.FC = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { loadRequests(); }, [loadRequests]);
|
||||
useEffect(() => {
|
||||
loadRequests(true);
|
||||
}, [loadRequests]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!team) return;
|
||||
const interval = setInterval(() => {
|
||||
loadRequests(false);
|
||||
}, 60000);
|
||||
return () => clearInterval(interval);
|
||||
}, [team, loadRequests]);
|
||||
|
||||
const handleRequest = async (requestId: string, action: 'approve' | 'reject', note?: string) => {
|
||||
try {
|
||||
@@ -286,12 +354,27 @@ const TeamManagementPage: React.FC = () => {
|
||||
const invColumns = [
|
||||
{ title: '邀请码', dataIndex: 'code', width: 200, render: (v: string) => <Typography.Text copyable style={{ fontFamily: 'monospace' }}>{v}</Typography.Text> },
|
||||
{
|
||||
title: '邀请链接', dataIndex: 'inviteLink', ellipsis: true,
|
||||
title: '邀请链接', dataIndex: 'inviteLink',
|
||||
render: (v: string) => (
|
||||
<Space>
|
||||
<Typography.Text ellipsis style={{ maxWidth: 250, fontSize: 12 }}>{v}</Typography.Text>
|
||||
<Space style={{ width: '100%' }}>
|
||||
<Typography.Text
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 12,
|
||||
wordBreak: 'break-all',
|
||||
fontFamily: 'monospace',
|
||||
color: '#64748b',
|
||||
}}
|
||||
>
|
||||
{v}
|
||||
</Typography.Text>
|
||||
<Tooltip title="复制链接">
|
||||
<Button size="small" type="text" icon={<CopyOutlined />} onClick={() => copyInviteLink(v)} />
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => copyInviteLink(v)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
),
|
||||
@@ -517,7 +600,7 @@ const TeamManagementPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* 标签页 */}
|
||||
<Tabs items={tabItems} defaultActiveKey="members" size="large" />
|
||||
<Tabs items={tabItems} activeKey={activeTab} onChange={setActiveTab} size="large" />
|
||||
|
||||
{/* 调整积分弹窗 */}
|
||||
<Modal
|
||||
@@ -608,7 +691,7 @@ const TeamManagementPage: React.FC = () => {
|
||||
<InputNumber style={{ width: '100%' }} min={1} placeholder="留空表示不限" size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item name="expiresAt" label="过期时间">
|
||||
<Input type="datetime-local" style={{ width: '100%' }} placeholder="留空表示永不过期" size="large" />
|
||||
<Input type="datetime-local" style={{ width: '100%' }} placeholder="默认24小时后过期" size="large" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@@ -77,6 +77,7 @@ export interface JoinTeamInfo {
|
||||
teamId: string;
|
||||
valid: boolean;
|
||||
alreadyInTeam: boolean;
|
||||
hasPendingRequest: boolean;
|
||||
}
|
||||
|
||||
export interface CreditRecord {
|
||||
|
||||
Reference in New Issue
Block a user