1
This commit is contained in:
@@ -253,3 +253,33 @@ async def handle_join_request(
|
|||||||
db, request_id, current_user.id, req.action, req.note
|
db, request_id, current_user.id, req.action, req.note
|
||||||
)
|
)
|
||||||
return {"message": "ok"}
|
return {"message": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
# ── 团队积分变动记录 ────────────────────────────────────
|
||||||
|
@router.get("/credit-records")
|
||||||
|
async def list_team_credit_records(
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(20, ge=1, le=100),
|
||||||
|
user_id: str | None = Query(None),
|
||||||
|
record_type: str | None = Query(None, pattern="^(recharge|consume|refund|admin)$"),
|
||||||
|
start_date: str | None = Query(None),
|
||||||
|
end_date: 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="只有团队管理人可查看")
|
||||||
|
|
||||||
|
from app.services.admin_credit_record_service import list_admin_credit_records
|
||||||
|
return await list_admin_credit_records(
|
||||||
|
db,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
team_id=team.id,
|
||||||
|
user_id=user_id,
|
||||||
|
record_type=record_type,
|
||||||
|
start_date=start_date,
|
||||||
|
end_date=end_date,
|
||||||
|
)
|
||||||
|
|||||||
@@ -782,3 +782,21 @@ export async function getJoinTeamInfo(code: string): Promise<any> {
|
|||||||
export async function submitJoinRequest(code: string): Promise<void> {
|
export async function submitJoinRequest(code: string): Promise<void> {
|
||||||
await api.post('/team/join', { invitation_code: code });
|
await api.post('/team/join', { invitation_code: code });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getTeamCreditRecords(params: {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
userId?: string;
|
||||||
|
recordType?: string;
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
}): Promise<any> {
|
||||||
|
const p = new URLSearchParams();
|
||||||
|
if (params.page) p.set('page', String(params.page));
|
||||||
|
if (params.pageSize) p.set('page_size', String(params.pageSize));
|
||||||
|
if (params.userId) p.set('user_id', params.userId);
|
||||||
|
if (params.recordType) p.set('record_type', params.recordType);
|
||||||
|
if (params.startDate) p.set('start_date', params.startDate);
|
||||||
|
if (params.endDate) p.set('end_date', params.endDate);
|
||||||
|
return api.get(`/team/credit-records?${p.toString()}`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -597,6 +597,7 @@ const AppLayout: React.FC = () => {
|
|||||||
{ key: 'profile', icon: <UserOutlined />, label: `账号: ${user?.username}`, disabled: true },
|
{ key: 'profile', icon: <UserOutlined />, label: `账号: ${user?.username}`, disabled: true },
|
||||||
{ key: 'credits', icon: <WalletOutlined style={{ color: '#c9a96e' }} />, label: `积分: ${user?.credits ?? 0}`, disabled: true },
|
{ key: 'credits', icon: <WalletOutlined style={{ color: '#c9a96e' }} />, label: `积分: ${user?.credits ?? 0}`, disabled: true },
|
||||||
{ type: 'divider' as const },
|
{ type: 'divider' as const },
|
||||||
|
...(user?.isTeamManager ? [{ key: 'teamManagement' as const, icon: <TeamOutlined style={{ color: '#6366f1' }} />, label: '团队管理' }] : []),
|
||||||
{ key: 'myCredits', icon: <WalletOutlined />, label: '积分明细' },
|
{ key: 'myCredits', icon: <WalletOutlined />, label: '积分明细' },
|
||||||
{ key: 'orderRecords', icon: <FileTextOutlined />, label: '订单记录' },
|
{ key: 'orderRecords', icon: <FileTextOutlined />, label: '订单记录' },
|
||||||
{ key: 'messages', icon: <BellOutlined />, label: `消息中心${unreadCount > 0 ? `(${unreadCount})` : ''}` },
|
{ key: 'messages', icon: <BellOutlined />, label: `消息中心${unreadCount > 0 ? `(${unreadCount})` : ''}` },
|
||||||
@@ -612,6 +613,7 @@ const AppLayout: React.FC = () => {
|
|||||||
else if (key === 'changePwd') { setPwdModalOpen(true); }
|
else if (key === 'changePwd') { setPwdModalOpen(true); }
|
||||||
else if (key === 'messages') { navigate('/messages'); }
|
else if (key === 'messages') { navigate('/messages'); }
|
||||||
else if (key === 'recharge') { setRechargeModalOpen(true); }
|
else if (key === 'recharge') { setRechargeModalOpen(true); }
|
||||||
|
else if (key === 'teamManagement') { navigate('/team-management'); }
|
||||||
else if (key === 'myCredits') { navigate('/user-center?tab=credits'); }
|
else if (key === 'myCredits') { navigate('/user-center?tab=credits'); }
|
||||||
else if (key === 'orderRecords') { navigate('/user-center?tab=orders'); }
|
else if (key === 'orderRecords') { navigate('/user-center?tab=orders'); }
|
||||||
else if (key === 'manual') { window.open(operationManualUrl, '_blank'); }
|
else if (key === 'manual') { window.open(operationManualUrl, '_blank'); }
|
||||||
@@ -837,16 +839,6 @@ const AppLayout: React.FC = () => {
|
|||||||
|
|
||||||
<div style={{ flex: 1, padding: '8px 8px', overflow: 'auto' }}>
|
<div style={{ flex: 1, padding: '8px 8px', overflow: 'auto' }}>
|
||||||
{topLevelItems.map(item => renderMenuItem(item))}
|
{topLevelItems.map(item => renderMenuItem(item))}
|
||||||
{user?.isTeamManager && renderMenuItem({
|
|
||||||
id: 'team-management',
|
|
||||||
key: 'team-management',
|
|
||||||
label: '团队管理',
|
|
||||||
path: '/team-management',
|
|
||||||
icon: 'TeamOutlined',
|
|
||||||
sortOrder: 999,
|
|
||||||
isActive: true,
|
|
||||||
menuType: 'page',
|
|
||||||
} as any)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div onClick={() => setRechargeModalOpen(true)} style={{
|
<div onClick={() => setRechargeModalOpen(true)} style={{
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import {
|
|||||||
Button, Card, Empty, Form, Input, InputNumber, message, Modal, Space, Table, Tabs, Tag, Tooltip, Typography,
|
Button, Card, Empty, Form, Input, InputNumber, message, Modal, Space, Table, Tabs, Tag, Tooltip, Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
CopyOutlined, PlusOutlined, ReloadOutlined, UserOutlined, HistoryOutlined,
|
CopyOutlined, PlusOutlined, ReloadOutlined, UserOutlined, HistoryOutlined, WalletOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
createTeamInvitation, getJoinTeamInfo, getManagedTeam, getPendingJoinRequests,
|
createTeamInvitation, getJoinTeamInfo, getManagedTeam, getPendingJoinRequests,
|
||||||
getTeamInvitations, getTeamMembers, handleJoinRequest, revokeInvitation, submitJoinRequest, transferCredits,
|
getTeamCreditRecords, getTeamInvitations, getTeamMembers, handleJoinRequest, revokeInvitation, submitJoinRequest, transferCredits,
|
||||||
} from '../api';
|
} from '../api';
|
||||||
import type { ManagedTeam, TeamInvitation, TeamJoinRequest, TeamMember } from '../types';
|
import type { ManagedTeam, TeamInvitation, TeamJoinRequest, TeamMember } from '../types';
|
||||||
import { formatDate } from '../utils/formatDate';
|
import { formatDate } from '../utils/formatDate';
|
||||||
@@ -160,6 +160,32 @@ const TeamManagementPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Tab 4: 团队积分变动 ──
|
||||||
|
const [creditRecords, setCreditRecords] = useState<any[]>([]);
|
||||||
|
const [creditTotal, setCreditTotal] = useState(0);
|
||||||
|
const [creditLoading, setCreditLoading] = useState(false);
|
||||||
|
const [creditPage, setCreditPage] = useState(1);
|
||||||
|
const [creditFilterType, setCreditFilterType] = useState<string>('');
|
||||||
|
|
||||||
|
const loadCreditRecords = useCallback(async () => {
|
||||||
|
setCreditLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await getTeamCreditRecords({
|
||||||
|
page: creditPage,
|
||||||
|
pageSize: 20,
|
||||||
|
recordType: creditFilterType || undefined,
|
||||||
|
});
|
||||||
|
setCreditRecords(res.items || []);
|
||||||
|
setCreditTotal(res.total || 0);
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.message || '加载积分记录失败');
|
||||||
|
} finally {
|
||||||
|
setCreditLoading(false);
|
||||||
|
}
|
||||||
|
}, [creditPage, creditFilterType]);
|
||||||
|
|
||||||
|
useEffect(() => { loadCreditRecords(); }, [loadCreditRecords]);
|
||||||
|
|
||||||
const memberColumns = [
|
const memberColumns = [
|
||||||
{ title: '用户名', dataIndex: 'username', width: 150, render: (v: string) => <Typography.Text strong>{v}</Typography.Text> },
|
{ title: '用户名', dataIndex: 'username', width: 150, 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 || '-' },
|
||||||
@@ -285,6 +311,66 @@ const TeamManagementPage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'credits',
|
||||||
|
label: <Space><WalletOutlined />团队积分变动</Space>,
|
||||||
|
children: (
|
||||||
|
<div>
|
||||||
|
<div style={{ marginBottom: 12, display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||||
|
<Select
|
||||||
|
value={creditFilterType || undefined}
|
||||||
|
onChange={(v) => { setCreditFilterType(v || ''); setCreditPage(1); }}
|
||||||
|
allowClear
|
||||||
|
placeholder="全部类型"
|
||||||
|
style={{ width: 140 }}
|
||||||
|
options={[
|
||||||
|
{ value: 'recharge', label: '充值' },
|
||||||
|
{ value: 'consume', label: '消费' },
|
||||||
|
{ value: 'admin', label: '管理员调整' },
|
||||||
|
{ value: 'refund', label: '退款' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
size="small"
|
||||||
|
rowKey="id"
|
||||||
|
loading={creditLoading}
|
||||||
|
dataSource={creditRecords}
|
||||||
|
pagination={{
|
||||||
|
current: creditPage,
|
||||||
|
pageSize: 20,
|
||||||
|
total: creditTotal,
|
||||||
|
onChange: (p) => setCreditPage(p),
|
||||||
|
showTotal: (t) => `共 ${t} 条`,
|
||||||
|
}}
|
||||||
|
scroll={{ x: 900 }}
|
||||||
|
columns={[
|
||||||
|
{ title: '用户名', key: 'username', width: 120, render: (_: any, r: any) => <Typography.Text strong>{r.username || r.user_id || '-'}</Typography.Text> },
|
||||||
|
{
|
||||||
|
title: '类型', key: 'type', width: 100,
|
||||||
|
render: (_: any, r: any) => {
|
||||||
|
const colorMap: Record<string, string> = { recharge: 'green', consume: 'red', admin: 'blue', refund: 'orange' };
|
||||||
|
const labelMap: Record<string, string> = { recharge: '充值', consume: '消费', admin: '管理员调整', refund: '退款' };
|
||||||
|
return <Tag color={colorMap[r.type] || 'default'}>{labelMap[r.type] || r.type}</Tag>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '积分变动', key: 'amount', width: 100,
|
||||||
|
render: (_: any, r: any) => (
|
||||||
|
<Typography.Text style={{ color: r.amount >= 0 ? '#16a34a' : '#dc2626', fontWeight: 600 }}>
|
||||||
|
{r.amount >= 0 ? '+' : ''}{(r.amount ?? 0).toFixed(2)}
|
||||||
|
</Typography.Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ title: '余额', key: 'balance_after', width: 100, render: (_: any, r: any) => (r.balance_after ?? 0).toFixed(2) },
|
||||||
|
{ title: '说明', dataIndex: 'description', ellipsis: true, render: (v: string) => v || '-' },
|
||||||
|
{ title: '时间', key: 'created_at', width: 160, render: (_: any, r: any) => formatDate(r.created_at) },
|
||||||
|
]}
|
||||||
|
locale={{ emptyText: <Empty description="暂无积分记录" /> }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -296,7 +382,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(); }}>刷新</Button>
|
<Button icon={<ReloadOutlined />} onClick={() => { loadTeam(); loadMembers(); loadInvitations(); loadRequests(); loadCreditRecords(); }}>刷新</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user