1、团队积分变动增加对应手机号和日期搜索
2、翻页样式应该放在左下角,别的页面也在左下角吧 3、列表页面显示总使用积分和下载excel 4、团队管理员分配积分的逻辑有问题,团队积分变动不显示,后台显示流水类型也不对,应该增加团队的内部积分变动类型,同步搜索下载 5、调整积分弹窗应该让用户显示增加还是减少的button 6、检查代码是否有不合理的地方,比如枚举是否放在enums里了
This commit is contained in:
+505
File diff suppressed because one or more lines are too long
-505
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-XSsSl940.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-D7ovjI8j.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D9_3MPsN.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -818,8 +818,8 @@ export async function getTeamMembers(page = 1, pageSize = 20): Promise<any> {
|
||||
return api.get(`/team/members?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function transferCredits(memberId: string, amount: number, description?: string): Promise<void> {
|
||||
await api.post(`/team/members/${memberId}/credits`, { target_user_id: memberId, amount, description: description || null });
|
||||
export async function transferCredits(memberId: string, amount: number, direction: string = "increase", description?: string): Promise<void> {
|
||||
await api.post(`/team/members/${memberId}/credits`, { target_user_id: memberId, amount, direction, description: description || null });
|
||||
}
|
||||
|
||||
export async function getTeamInvitations(): Promise<any[]> {
|
||||
@@ -854,6 +854,7 @@ export async function getTeamCreditRecords(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
userId?: string;
|
||||
phone?: string;
|
||||
recordType?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
@@ -862,8 +863,24 @@ export async function getTeamCreditRecords(params: {
|
||||
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.phone) p.set('phone', params.phone);
|
||||
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()}`);
|
||||
}
|
||||
|
||||
export function getTeamCreditExportUrl(params: {
|
||||
phone?: string;
|
||||
recordType?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}): string {
|
||||
const p = new URLSearchParams();
|
||||
if (params.phone) p.set('phone', params.phone);
|
||||
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);
|
||||
const base = (import.meta as any).env?.VITE_API_BASE || 'http://localhost:8000';
|
||||
return `${base}/api/team/credit-records/export?${p.toString()}`;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import {
|
||||
Button, Card, Empty, Form, Input, InputNumber, message, Modal, Select, Space, Table, Tabs, Tag, Tooltip, Typography,
|
||||
Button, Card, Empty, Form, Input, InputNumber, message, Modal, Radio, Select, Space, Table, Tabs, Tag, Tooltip, Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
CopyOutlined, PlusOutlined, ReloadOutlined, UserOutlined, HistoryOutlined, WalletOutlined,
|
||||
CopyOutlined, DownloadOutlined, PlusOutlined, ReloadOutlined, UserOutlined, HistoryOutlined, WalletOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
createTeamInvitation, getJoinTeamInfo, getManagedTeam, getPendingJoinRequests,
|
||||
getTeamCreditRecords, getTeamInvitations, getTeamMembers, handleJoinRequest, revokeInvitation, submitJoinRequest, transferCredits,
|
||||
getTeamCreditExportUrl, getTeamCreditRecords, getTeamInvitations, getTeamMembers, handleJoinRequest, revokeInvitation, submitJoinRequest, transferCredits,
|
||||
} from '../api';
|
||||
import type { ManagedTeam, TeamInvitation, TeamJoinRequest, TeamMember } from '../types';
|
||||
import { formatDate } from '../utils/formatDate';
|
||||
@@ -61,14 +61,20 @@ const TeamManagementPage: React.FC = () => {
|
||||
try {
|
||||
const values = await creditForm.validateFields();
|
||||
setCreditSaving(true);
|
||||
await transferCredits(creditModal.member.id, values.amount, values.description);
|
||||
message.success('积分转账成功');
|
||||
await transferCredits(
|
||||
creditModal.member.id,
|
||||
values.amount,
|
||||
values.direction || "increase",
|
||||
values.description,
|
||||
);
|
||||
message.success(values.direction === "decrease" ? '积分扣减成功' : '积分增加成功');
|
||||
setCreditModal({ open: false, member: null });
|
||||
creditForm.resetFields();
|
||||
loadMembers();
|
||||
loadCreditRecords();
|
||||
} catch (e: any) {
|
||||
if (e?.errorFields) return;
|
||||
message.error(e?.message || '转账失败');
|
||||
message.error(e?.message || '操作失败');
|
||||
} finally {
|
||||
setCreditSaving(false);
|
||||
}
|
||||
@@ -163,9 +169,13 @@ const TeamManagementPage: React.FC = () => {
|
||||
// ── Tab 4: 团队积分变动 ──
|
||||
const [creditRecords, setCreditRecords] = useState<any[]>([]);
|
||||
const [creditTotal, setCreditTotal] = useState(0);
|
||||
const [creditSummary, setCreditSummary] = useState<any>(null);
|
||||
const [creditLoading, setCreditLoading] = useState(false);
|
||||
const [creditPage, setCreditPage] = useState(1);
|
||||
const [creditFilterType, setCreditFilterType] = useState<string>('');
|
||||
const [creditFilterPhone, setCreditFilterPhone] = useState<string>('');
|
||||
const [creditFilterStart, setCreditFilterStart] = useState<string>('');
|
||||
const [creditFilterEnd, setCreditFilterEnd] = useState<string>('');
|
||||
|
||||
const loadCreditRecords = useCallback(async () => {
|
||||
setCreditLoading(true);
|
||||
@@ -173,19 +183,53 @@ const TeamManagementPage: React.FC = () => {
|
||||
const res = await getTeamCreditRecords({
|
||||
page: creditPage,
|
||||
pageSize: 20,
|
||||
phone: creditFilterPhone || undefined,
|
||||
recordType: creditFilterType || undefined,
|
||||
startDate: creditFilterStart || undefined,
|
||||
endDate: creditFilterEnd || undefined,
|
||||
});
|
||||
setCreditRecords(res.items || []);
|
||||
setCreditTotal(res.total || 0);
|
||||
setCreditSummary(res.summary || null);
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '加载积分记录失败');
|
||||
} finally {
|
||||
setCreditLoading(false);
|
||||
}
|
||||
}, [creditPage, creditFilterType]);
|
||||
}, [creditPage, creditFilterType, creditFilterPhone, creditFilterStart, creditFilterEnd]);
|
||||
|
||||
useEffect(() => { loadCreditRecords(); }, [loadCreditRecords]);
|
||||
|
||||
const resetCreditFilters = () => {
|
||||
setCreditFilterType('');
|
||||
setCreditFilterPhone('');
|
||||
setCreditFilterStart('');
|
||||
setCreditFilterEnd('');
|
||||
setCreditPage(1);
|
||||
};
|
||||
|
||||
const handleExportCredits = () => {
|
||||
const url = getTeamCreditExportUrl({
|
||||
phone: creditFilterPhone || undefined,
|
||||
recordType: creditFilterType || undefined,
|
||||
startDate: creditFilterStart || undefined,
|
||||
endDate: creditFilterEnd || undefined,
|
||||
});
|
||||
// 携带 token 下载
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const headers: Record<string, string> = token ? { Authorization: `Bearer ${token}` } : {};
|
||||
fetch(url, { headers })
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => {
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = `team_credits_${Date.now()}.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
})
|
||||
.catch(() => message.error('导出失败'));
|
||||
};
|
||||
|
||||
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 || '-' },
|
||||
@@ -270,12 +314,107 @@ const TeamManagementPage: React.FC = () => {
|
||||
total: membersTotal,
|
||||
onChange: (p) => setMembersPage(p),
|
||||
showTotal: (t) => `共 ${t} 人`,
|
||||
position: ['bottomLeft'],
|
||||
}}
|
||||
scroll={{ x: 800 }}
|
||||
locale={{ emptyText: <Empty description="暂无成员" /> }}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'credits',
|
||||
label: <Space><WalletOutlined />团队积分变动</Space>,
|
||||
children: (
|
||||
<div>
|
||||
{/* 搜索栏 */}
|
||||
<div style={{ marginBottom: 12, display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<Select
|
||||
value={creditFilterType}
|
||||
onChange={(v) => { setCreditFilterType(v || ''); setCreditPage(1); }}
|
||||
allowClear
|
||||
placeholder="全部类型"
|
||||
style={{ width: 130 }}
|
||||
options={[
|
||||
{ value: 'recharge', label: '充值' },
|
||||
{ value: 'consume', label: '消费' },
|
||||
{ value: 'team_internal', label: '团队内部' },
|
||||
{ value: 'refund', label: '退款' },
|
||||
]}
|
||||
/>
|
||||
<Input
|
||||
placeholder="搜索手机号"
|
||||
value={creditFilterPhone}
|
||||
onChange={(e) => { setCreditFilterPhone(e.target.value); setCreditPage(1); }}
|
||||
style={{ width: 160 }}
|
||||
allowClear
|
||||
/>
|
||||
<Input
|
||||
placeholder="起始日期 YYYY-MM-DD"
|
||||
value={creditFilterStart}
|
||||
onChange={(e) => { setCreditFilterStart(e.target.value); setCreditPage(1); }}
|
||||
style={{ width: 160 }}
|
||||
/>
|
||||
<Input
|
||||
placeholder="截止日期 YYYY-MM-DD"
|
||||
value={creditFilterEnd}
|
||||
onChange={(e) => { setCreditFilterEnd(e.target.value); setCreditPage(1); }}
|
||||
style={{ width: 160 }}
|
||||
/>
|
||||
<Button onClick={resetCreditFilters}>重置</Button>
|
||||
<Button type="primary" icon={<DownloadOutlined />} onClick={handleExportCredits}>导出 Excel</Button>
|
||||
</div>
|
||||
|
||||
{/* 汇总统计 */}
|
||||
{creditSummary && (
|
||||
<div style={{ marginBottom: 12, padding: '8px 12px', background: '#f8f9fc', borderRadius: 8, display: 'flex', gap: 20, flexWrap: 'wrap', fontSize: 13 }}>
|
||||
<span>总充值:<strong style={{ color: '#16a34a' }}>+{creditSummary.total_recharge ?? 0}</strong></span>
|
||||
<span>总消费:<strong style={{ color: '#dc2626' }}>-{creditSummary.total_consume ?? 0}</strong></span>
|
||||
<span>笔数:<strong>{creditSummary.transaction_count ?? 0}</strong></span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Table
|
||||
size="small"
|
||||
rowKey="id"
|
||||
loading={creditLoading}
|
||||
dataSource={creditRecords}
|
||||
pagination={{
|
||||
current: creditPage,
|
||||
pageSize: 20,
|
||||
total: creditTotal,
|
||||
onChange: (p) => setCreditPage(p),
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
position: ['bottomLeft'],
|
||||
}}
|
||||
scroll={{ x: 950 }}
|
||||
columns={[
|
||||
{ title: '用户名', key: 'username', width: 120, render: (_: any, r: any) => <Typography.Text strong>{r.username || '-'}</Typography.Text> },
|
||||
{ title: '手机号', key: 'phone', width: 120, render: (_: any, r: any) => r.phone || '-' },
|
||||
{
|
||||
title: '类型', key: 'type', width: 100,
|
||||
render: (_: any, r: any) => {
|
||||
const colorMap: Record<string, string> = { recharge: 'green', consume: 'red', refund: 'orange', team_internal: 'blue' };
|
||||
const labelMap: Record<string, string> = { recharge: '充值', consume: '消费', refund: '退款', team_internal: '团队内部' };
|
||||
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>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'invitations',
|
||||
label: <Space><CopyOutlined />邀请管理</Space>,
|
||||
@@ -311,65 +450,6 @@ 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}
|
||||
onChange={(v) => { setCreditFilterType(v); setCreditPage(1); }}
|
||||
style={{ width: 140 }}
|
||||
options={[
|
||||
{ value: '', label: '全部类型' },
|
||||
{ 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 (
|
||||
@@ -390,19 +470,25 @@ const TeamManagementPage: React.FC = () => {
|
||||
<Tabs items={tabItems} defaultActiveKey="members" />
|
||||
</Card>
|
||||
|
||||
{/* 积分转账弹窗 */}
|
||||
{/* 调整积分弹窗 */}
|
||||
<Modal
|
||||
title={<Space><UserOutlined />调整成员积分 - {creditModal.member?.username}</Space>}
|
||||
open={creditModal.open}
|
||||
confirmLoading={creditSaving}
|
||||
onOk={handleTransfer}
|
||||
onCancel={() => setCreditModal({ open: false, member: null })}
|
||||
okText="确认转账"
|
||||
okText="确认"
|
||||
width={480}
|
||||
>
|
||||
<Form form={creditForm} layout="vertical" style={{ marginTop: 16 }}>
|
||||
<Form.Item name="amount" label="转账积分" rules={[{ required: true, message: '请输入转账数量' }, { type: 'number', min: 0.01, message: '必须大于 0' }]}>
|
||||
<InputNumber style={{ width: '100%' }} step={1} min={0.01} placeholder="正数:从您余额转给该成员" size="large" />
|
||||
<Form form={creditForm} layout="vertical" style={{ marginTop: 16 }} initialValues={{ direction: 'increase' }}>
|
||||
<Form.Item name="direction" label="操作类型" rules={[{ required: true }]}>
|
||||
<Radio.Group buttonStyle="solid" size="large" style={{ width: '100%' }}>
|
||||
<Radio.Button value="increase" style={{ width: '50%', textAlign: 'center' }}>增加成员积分</Radio.Button>
|
||||
<Radio.Button value="decrease" style={{ width: '50%', textAlign: 'center' }}>扣减成员积分</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item name="amount" label="积分数量" rules={[{ required: true, message: '请输入数量' }, { type: 'number', min: 0.01, message: '必须大于 0' }]}>
|
||||
<InputNumber style={{ width: '100%' }} step={1} min={0.01} placeholder="请输入正数积分数量" size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="备注">
|
||||
<Input.TextArea rows={2} maxLength={256} placeholder="选填,例如:活动奖励" />
|
||||
|
||||
Reference in New Issue
Block a user