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
|
||||
)
|
||||
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> {
|
||||
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: 'credits', icon: <WalletOutlined style={{ color: '#c9a96e' }} />, label: `积分: ${user?.credits ?? 0}`, disabled: true },
|
||||
{ type: 'divider' as const },
|
||||
...(user?.isTeamManager ? [{ key: 'teamManagement' as const, icon: <TeamOutlined style={{ color: '#6366f1' }} />, label: '团队管理' }] : []),
|
||||
{ key: 'myCredits', icon: <WalletOutlined />, label: '积分明细' },
|
||||
{ key: 'orderRecords', icon: <FileTextOutlined />, label: '订单记录' },
|
||||
{ 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 === 'messages') { navigate('/messages'); }
|
||||
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 === 'orderRecords') { navigate('/user-center?tab=orders'); }
|
||||
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' }}>
|
||||
{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 onClick={() => setRechargeModalOpen(true)} style={{
|
||||
|
||||
@@ -3,11 +3,11 @@ import {
|
||||
Button, Card, Empty, Form, Input, InputNumber, message, Modal, Space, Table, Tabs, Tag, Tooltip, Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
CopyOutlined, PlusOutlined, ReloadOutlined, UserOutlined, HistoryOutlined,
|
||||
CopyOutlined, PlusOutlined, ReloadOutlined, UserOutlined, HistoryOutlined, WalletOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
createTeamInvitation, getJoinTeamInfo, getManagedTeam, getPendingJoinRequests,
|
||||
getTeamInvitations, getTeamMembers, handleJoinRequest, revokeInvitation, submitJoinRequest, transferCredits,
|
||||
getTeamCreditRecords, getTeamInvitations, getTeamMembers, handleJoinRequest, revokeInvitation, submitJoinRequest, transferCredits,
|
||||
} from '../api';
|
||||
import type { ManagedTeam, TeamInvitation, TeamJoinRequest, TeamMember } from '../types';
|
||||
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 = [
|
||||
{ title: '用户名', dataIndex: 'username', width: 150, render: (v: string) => <Typography.Text strong>{v}</Typography.Text> },
|
||||
{ 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 (
|
||||
@@ -296,7 +382,7 @@ const TeamManagementPage: React.FC = () => {
|
||||
<Typography.Title level={4} style={{ margin: 0 }}>{team.name}</Typography.Title>
|
||||
<Typography.Text type="secondary">代码: {team.code || '-'} 成员: {team.memberCount} 人</Typography.Text>
|
||||
</div>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => { loadTeam(); loadMembers(); loadInvitations(); loadRequests(); }}>刷新</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => { loadTeam(); loadMembers(); loadInvitations(); loadRequests(); loadCreditRecords(); }}>刷新</Button>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user