会员积分改版V1
This commit is contained in:
@@ -6,12 +6,15 @@ import {
|
||||
UserOutlined, WalletOutlined, SearchOutlined, StopOutlined, CheckCircleOutlined, PlusOutlined, MinusOutlined, MenuOutlined, LockOutlined, SettingOutlined, SaveOutlined, DatabaseOutlined, TeamOutlined, PictureOutlined, SecurityScanOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
adjustCredits,
|
||||
adminDeductCredits,
|
||||
adminGrantCredits,
|
||||
adminGetPrivatePortraitConfig,
|
||||
adminUpdatePrivatePortraitConfig,
|
||||
createUser,
|
||||
deleteUserResourceCapacity,
|
||||
getAdminUsers,
|
||||
getAdminUserCreditBalances,
|
||||
getAdminUserCreditSummary,
|
||||
getMenuConfigs,
|
||||
getTeamOptions,
|
||||
getSystemConfigs,
|
||||
@@ -66,6 +69,12 @@ const AdminUsers: React.FC = () => {
|
||||
const [teamFilter, setTeamFilter] = useState<string>('');
|
||||
const [teamOptions, setTeamOptions] = useState<AdminTeamOption[]>([]);
|
||||
const [creditModal, setCreditModal] = useState<{ open: boolean; user: AdminUser | null }>({ open: false, user: null });
|
||||
const [creditDetailModal, setCreditDetailModal] = useState<{ open: boolean; user: AdminUser | null }>({ open: false, user: null });
|
||||
const [creditDetailLoading, setCreditDetailLoading] = useState(false);
|
||||
const [creditSummary, setCreditSummary] = useState<any>(null);
|
||||
const [creditBalances, setCreditBalances] = useState<any[]>([]);
|
||||
const [creditBalanceStatus, setCreditBalanceStatus] = useState<string>('');
|
||||
const [creditOperation, setCreditOperation] = useState<'grant' | 'deduct'>('grant');
|
||||
const [createModal, setCreateModal] = useState(false);
|
||||
const [createType, setCreateType] = useState<string>('frontend');
|
||||
const [menuModal, setMenuModal] = useState<{ open: boolean; user: AdminUser | null }>({ open: false, user: null });
|
||||
@@ -164,12 +173,44 @@ const AdminUsers: React.FC = () => {
|
||||
const values = await form.validateFields();
|
||||
const { user } = creditModal;
|
||||
if (!user) return;
|
||||
await adjustCredits(user.id, values.amount, values.description);
|
||||
message.success(`已${values.amount > 0 ? '增加' : '扣除'} ${Math.abs(values.amount)} 积分`);
|
||||
if (creditOperation === 'grant') {
|
||||
await adminGrantCredits(user.id, {
|
||||
amount: values.amount,
|
||||
description: values.description,
|
||||
validity_unit: values.validityUnit || 'month',
|
||||
validity_value: values.validityValue || 1,
|
||||
credit_level: 'general',
|
||||
});
|
||||
} else {
|
||||
await adminDeductCredits(user.id, { amount: values.amount, description: values.description });
|
||||
}
|
||||
message.success(`已${creditOperation === 'grant' ? '增加' : '扣除'} ${values.amount} 积分`);
|
||||
setCreditModal({ open: false, user: null });
|
||||
setCreditOperation('grant');
|
||||
form.resetFields();
|
||||
load();
|
||||
} catch { /* validation */ }
|
||||
} catch (e: any) {
|
||||
if (e?.errorFields) return;
|
||||
message.error(e?.message || '积分操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
const openCreditDetailModal = async (user: AdminUser, status = '') => {
|
||||
setCreditDetailModal({ open: true, user });
|
||||
setCreditBalanceStatus(status);
|
||||
setCreditDetailLoading(true);
|
||||
try {
|
||||
const [summary, balances] = await Promise.all([
|
||||
getAdminUserCreditSummary(user.id),
|
||||
getAdminUserCreditBalances(user.id, 1, 200, status || undefined),
|
||||
]);
|
||||
setCreditSummary(summary);
|
||||
setCreditBalances(balances || []);
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '加载用户积分明细失败');
|
||||
} finally {
|
||||
setCreditDetailLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleStatus = async (user: AdminUser) => {
|
||||
@@ -493,10 +534,20 @@ const AdminUsers: React.FC = () => {
|
||||
<Space size={4} wrap>
|
||||
{!isAdminTab && (
|
||||
<Button type="link" size="small" icon={<WalletOutlined />}
|
||||
onClick={() => { setCreditModal({ open: true, user: r }); form.resetFields(); }}>
|
||||
onClick={() => {
|
||||
setCreditOperation('grant');
|
||||
setCreditModal({ open: true, user: r });
|
||||
form.setFieldsValue({ amount: undefined, description: '', validityUnit: 'month', validityValue: 1 });
|
||||
}}>
|
||||
调整积分
|
||||
</Button>
|
||||
)}
|
||||
{!isAdminTab && (
|
||||
<Button type="link" size="small" icon={<WalletOutlined />}
|
||||
onClick={() => openCreditDetailModal(r)}>
|
||||
积分明细
|
||||
</Button>
|
||||
)}
|
||||
{!isAdminTab && (
|
||||
<Button type="link" size="small" icon={<DatabaseOutlined />}
|
||||
onClick={() => openCapacityModal(r)}>
|
||||
@@ -685,50 +736,103 @@ const AdminUsers: React.FC = () => {
|
||||
title={<Space><WalletOutlined />调整积分 - {creditModal.user?.username}</Space>}
|
||||
open={creditModal.open}
|
||||
onOk={handleAdjustCredits}
|
||||
onCancel={() => { setCreditModal({ open: false, user: null }); form.resetFields(); }}
|
||||
okText="确认" cancelText="取消" width={440}
|
||||
onCancel={() => { setCreditModal({ open: false, user: null }); setCreditOperation('grant'); form.resetFields(); }}
|
||||
okText="确认" cancelText="取消" width={480}
|
||||
>
|
||||
<div style={{ marginBottom: 16, padding: '12px 16px', background: '#f8fafc', borderRadius: 8 }}>
|
||||
<span style={{ color: '#64748b' }}>当前积分:</span>
|
||||
<span style={{ color: '#64748b' }}>当前有效积分:</span>
|
||||
<span style={{ fontWeight: 800, fontSize: 18, color: '#6366f1' }}>
|
||||
{creditModal.user?.credits.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
{/* 快捷操作 */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 8 }}>快捷操作</Typography.Text>
|
||||
<Space wrap>
|
||||
<Button size="small" icon={<PlusOutlined />} style={{ color: '#10b981' }} onClick={() => form.setFieldsValue({ amount: 1000, description: '积分赠送' })}>
|
||||
+1000 / 积分赠送
|
||||
</Button>
|
||||
<Button size="small" icon={<PlusOutlined />} style={{ color: '#10b981' }} onClick={() => form.setFieldsValue({ amount: 500, description: '积分赠送' })}>
|
||||
+500 / 积分赠送
|
||||
</Button>
|
||||
<Button size="small" icon={<MinusOutlined />} style={{ color: '#ef4444' }} onClick={() => form.setFieldsValue({ amount: -500, description: '积分扣除' })}>
|
||||
-500 / 积分扣除
|
||||
</Button>
|
||||
<Button size="small" icon={<MinusOutlined />} style={{ color: '#ef4444' }} onClick={() => form.setFieldsValue({ amount: -1000, description: '积分扣除' })}>
|
||||
-1000 / 积分扣除
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="amount" label="积分变动"
|
||||
rules={[{ required: true, message: '请输入积分数量' }]}>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
size="large"
|
||||
placeholder="正数增加,负数扣除"
|
||||
formatter={v => `${v}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}
|
||||
<Form form={form} layout="vertical" initialValues={{ validityUnit: 'month', validityValue: 1 }}>
|
||||
<Form.Item label="操作类型">
|
||||
<Select
|
||||
value={creditOperation}
|
||||
onChange={(value) => setCreditOperation(value)}
|
||||
options={[
|
||||
{ value: 'grant', label: '增加积分' },
|
||||
{ value: 'deduct', label: '扣除积分' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="原因"
|
||||
rules={[{ required: true, message: '请输入调整原因' }]}>
|
||||
<Form.Item name="amount" label="积分数量" rules={[{ required: true, message: '请输入积分数量' }]}>
|
||||
<InputNumber min={0.01} precision={2} style={{ width: '100%' }} size="large" placeholder="请输入正数积分数量" />
|
||||
</Form.Item>
|
||||
{creditOperation === 'grant' && (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
||||
<Form.Item name="validityUnit" label="有效期单位" rules={[{ required: true }]}>
|
||||
<Select options={[{ value: 'day', label: '天' }, { value: 'month', label: '自然月' }]} />
|
||||
</Form.Item>
|
||||
<Form.Item name="validityValue" label="有效期数值" rules={[{ required: true }]}>
|
||||
<InputNumber min={1} max={120} precision={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
<Form.Item name="description" label="原因" rules={[{ required: true, message: '请输入调整原因' }]}>
|
||||
<Input.TextArea rows={2} placeholder="请输入调整原因" size="large" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={<Space><WalletOutlined />积分明细 - {creditDetailModal.user?.username}</Space>}
|
||||
open={creditDetailModal.open}
|
||||
footer={null}
|
||||
width={1180}
|
||||
onCancel={() => {
|
||||
setCreditDetailModal({ open: false, user: null });
|
||||
setCreditSummary(null);
|
||||
setCreditBalances([]);
|
||||
setCreditBalanceStatus('');
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 12 }}>
|
||||
<Card size="small"><Typography.Text type="secondary">当前有效积分</Typography.Text><div style={{ fontSize: 22, fontWeight: 700 }}>{Number(creditSummary?.availableCredits || creditSummary?.credits || 0).toLocaleString()}</div></Card>
|
||||
<Card size="small"><Typography.Text type="secondary">最近即将过期积分</Typography.Text><div style={{ fontSize: 22, fontWeight: 700 }}>{Number(creditSummary?.nextExpiringCredits || 0).toLocaleString()}</div></Card>
|
||||
<Card size="small"><Typography.Text type="secondary">最近最后可用时间</Typography.Text><div style={{ fontSize: 15, fontWeight: 600 }}>{creditSummary?.nextLastUsableAt ? formatDate(creditSummary.nextLastUsableAt) : '-'}</div></Card>
|
||||
</div>
|
||||
<Space>
|
||||
<Typography.Text strong>积分批次</Typography.Text>
|
||||
<Select
|
||||
value={creditBalanceStatus}
|
||||
style={{ width: 140 }}
|
||||
options={[
|
||||
{ value: '', label: '全部状态' },
|
||||
{ value: 'scheduled', label: '未生效' },
|
||||
{ value: 'active', label: '有效' },
|
||||
{ value: 'consumed', label: '已消费' },
|
||||
{ value: 'expired', label: '已过期' },
|
||||
{ value: 'revoked', label: '已撤销' },
|
||||
]}
|
||||
onChange={(value) => creditDetailModal.user && openCreditDetailModal(creditDetailModal.user, value)}
|
||||
/>
|
||||
</Space>
|
||||
<Table
|
||||
size="small"
|
||||
loading={creditDetailLoading}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
dataSource={creditBalances}
|
||||
columns={[
|
||||
{ title: '来源', dataIndex: 'sourceType', width: 150, render: (v: string) => v || '-' },
|
||||
{ title: '来源ID', dataIndex: 'sourceId', width: 220, ellipsis: true, render: (v: string) => v || '-' },
|
||||
{ title: '积分等级', dataIndex: 'creditLevel', width: 100, render: (v: string) => v === 'promotional' ? '活动积分' : '普通积分' },
|
||||
{ title: '发放', dataIndex: 'grantAmount', width: 100, render: (v: number) => Number(v || 0).toLocaleString() },
|
||||
{ title: '剩余', dataIndex: 'unspentAmount', width: 100, render: (v: number) => Number(v || 0).toLocaleString() },
|
||||
{ title: '已消费', dataIndex: 'consumedAmount', width: 100, render: (v: number) => Number(v || 0).toLocaleString() },
|
||||
{ title: '已过期', dataIndex: 'expiredAmount', width: 100, render: (v: number) => Number(v || 0).toLocaleString() },
|
||||
{ title: '已撤销', dataIndex: 'revokedAmount', width: 100, render: (v: number) => Number(v || 0).toLocaleString() },
|
||||
{ title: '生效时间', dataIndex: 'validFrom', width: 170, render: (v: string) => formatDate(v) },
|
||||
{ title: '最后可用时间', dataIndex: 'lastUsableAt', width: 180, render: (v: string) => formatDate(v) },
|
||||
{ title: '状态', dataIndex: 'status', width: 90, render: (v: string) => <Tag color={v === 'active' ? 'green' : v === 'expired' ? 'orange' : v === 'revoked' ? 'red' : 'default'}>{v}</Tag> },
|
||||
]}
|
||||
scroll={{ x: 1410, y: 460 }}
|
||||
/>
|
||||
</Space>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={<Space><TeamOutlined />团队设置 - {teamModal.user?.username}</Space>}
|
||||
open={teamModal.open}
|
||||
@@ -893,7 +997,7 @@ const AdminUsers: React.FC = () => {
|
||||
<Input.Password placeholder="请输入密码(至少6位)" size="large" />
|
||||
</Form.Item>
|
||||
{createType === 'frontend' && (
|
||||
<Form.Item name="credits" label="初始积分" initialValue={0}>
|
||||
<Form.Item name="credits" label="初始积分(一个自然月有效)" initialValue={0}>
|
||||
<InputNumber min={0} style={{ width: '100%' }} size="large" />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user