会员积分改版V1

This commit is contained in:
2026-08-11 09:24:18 +08:00
parent fe24e51b97
commit b9fd07f293
111 changed files with 9355 additions and 3900 deletions
+141 -37
View File
@@ -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>
)}