This commit is contained in:
2026-07-06 16:46:10 +08:00
12 changed files with 892 additions and 641 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
}
})();
</script>
<script type="module" crossorigin src="/assets/index-Dx-bvo36.js"></script>
<script type="module" crossorigin src="/assets/index-C4H6x_-U.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
</head>
<body>
@@ -35,6 +35,7 @@ const RECORD_TYPE_MAP: Record<string, { text: string; color: string; icon: React
recharge: { text: '充值', color: 'green', icon: <ArrowUpOutlined /> },
consume: { text: '消费', color: 'red', icon: <ArrowDownOutlined /> },
refund: { text: '回退', color: 'blue', icon: <RollbackOutlined /> },
team_internal: { text: '团队内部', color: 'cyan', icon: <WalletOutlined /> },
};
const userScopeOptions = [
@@ -49,6 +50,7 @@ const recordTypeOptions = [
{ value: 'recharge', label: '充值' },
{ value: 'consume', label: '消费' },
{ value: 'refund', label: '回退' },
{ value: 'team_internal', label: '团队内部' },
];
const creditSubjectOptions = [
+91 -4
View File
@@ -5,6 +5,7 @@ from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import StreamingResponse
from app.config import settings
from app.dependencies import get_current_user, get_db
@@ -99,6 +100,7 @@ async def transfer_credits(
current_user.id,
req.target_user_id,
req.amount,
req.direction or "increase",
req.description,
)
return {"message": "ok"}
@@ -261,9 +263,10 @@ 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),
phone: str | None = Query(None, description="按手机号搜索"),
record_type: str | None = Query(None, pattern="^(recharge|consume|refund|team_internal)$", description="流水类型"),
start_date: str | None = Query(None, description="起始日期 YYYY-MM-DD"),
end_date: str | None = Query(None, description="截止日期 YYYY-MM-DD"),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
@@ -273,13 +276,97 @@ async def list_team_credit_records(
raise HTTPException(status_code=403, detail="只有团队管理人可查看")
from app.services.admin_credit_record_service import list_admin_credit_records
# 如果传了 phone,先找到对应的 user_id
resolved_user_id = user_id
if phone and not user_id:
phone_result = await db.execute(
select(User.id).where(
User.team_id == team.id,
User.phone == phone,
User.is_active.is_(True),
).limit(1)
)
resolved_user_id = phone_result.scalar_one_or_none()
if not resolved_user_id:
return {"items": [], "total": 0, "summary": {}}
return await list_admin_credit_records(
db,
page=page,
page_size=page_size,
team_id=team.id,
user_id=user_id,
user_id=resolved_user_id,
record_type=record_type,
start_date=start_date,
end_date=end_date,
)
# ── 团队积分导出 Excel ──────────────────────────────────
@router.get("/credit-records/export")
async def export_team_credit_records(
user_id: str | None = Query(None),
phone: str | None = Query(None),
record_type: str | None = Query(None, pattern="^(recharge|consume|refund|team_internal)$", description="流水类型"),
start_date: str | None = Query(None),
end_date: str | None = Query(None),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""导出团队积分变动记录为 Excel(仅管理人)。"""
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
resolved_user_id = user_id
if phone and not user_id:
phone_result = await db.execute(
select(User.id).where(
User.team_id == team.id,
User.phone == phone,
User.is_active.is_(True),
).limit(1)
)
resolved_user_id = phone_result.scalar_one_or_none()
# 拉取全部记录(不分页)
result = await list_admin_credit_records(
db,
page=1,
page_size=10000,
team_id=team.id,
user_id=resolved_user_id,
record_type=record_type,
start_date=start_date,
end_date=end_date,
)
# 生成 CSV(兼容 Excel 打开)
import csv
import io
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(["用户名", "手机号", "类型", "积分变动", "余额", "说明", "时间"])
for item in result.get("items", []):
writer.writerow([
item.get("username") or "-",
item.get("phone") or "-",
item.get("record_type_label") or item.get("type") or "-",
item.get("amount", 0),
item.get("balance_after", 0),
item.get("description") or "-",
item.get("created_at") or "-",
])
from starlette.responses import StreamingResponse
output.seek(0)
filename = f"team_credits_{team.id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
return StreamingResponse(
iter([output.getvalue()]),
media_type="text/csv; charset=utf-8-sig",
headers={"Content-Disposition": f"attachment; filename*=UTF-8''{filename}"},
)
+2
View File
@@ -5,6 +5,7 @@ class CreditRecordType(str, Enum):
RECHARGE = "recharge"
CONSUME = "consume"
REFUND = "refund"
TEAM_INTERNAL = "team_internal" # 团队内部积分流转(管理人分配)
class CreditRecordOwnerType(str, Enum):
@@ -111,6 +112,7 @@ CREDIT_RECORD_TYPE_LABELS = {
CreditRecordType.RECHARGE.value: "充值",
CreditRecordType.CONSUME.value: "消费",
CreditRecordType.REFUND.value: "回退",
CreditRecordType.TEAM_INTERNAL.value: "团队内部",
}
CREDIT_RECORD_SUBJECT_LABELS = {
@@ -21,6 +21,7 @@ class TeamMemberOut(BaseModel):
class ManagerTransferRequest(BaseModel):
target_user_id: str = Field(..., description="接收积分的成员用户ID")
amount: float = Field(gt=0, description="转账积分数量(正数)")
direction: str = Field(default="increase", pattern="^(increase|decrease)$", description="increase=管理人转给成员;decrease=从成员扣减回管理人")
description: str | None = Field(None, max_length=256, description="转账说明")
@@ -129,11 +129,19 @@ async def transfer_credits_to_member(
manager_id: str,
target_member_id: str,
amount: float,
direction: str = "increase", # "increase" 管理人→成员; "decrease" 成员扣减
description: str | None = None,
) -> None:
"""管理人从自己余额转积分给团队成员。"""
"""管理人为团队成员增加或扣减积分。
direction:
- "increase": 管理人从自己余额转积分给成员(管理人减少,成员增加)
- "decrease": 从成员扣积分回到管理人(成员减少,管理人增加)
"""
if amount <= 0:
raise HTTPException(status_code=400, description="转账积分必须大于0")
raise HTTPException(status_code=400, description="积分数量必须大于0")
if direction not in ("increase", "decrease"):
raise HTTPException(status_code=400, description="无效操作方向")
# 获取管理人
manager_result = await db.execute(
@@ -155,7 +163,7 @@ async def transfer_credits_to_member(
if not manager.team_id:
raise HTTPException(status_code=400, detail="您不在任何团队中")
if member.team_id != manager.team_id:
raise HTTPException(status_code=400, detail="只能转账给同团队成员")
raise HTTPException(status_code=400, detail="只能操作同团队成员")
team_result = await db.execute(
select(Team).where(Team.id == manager.team_id, Team.deleted_at.is_(None)).limit(1)
@@ -164,22 +172,44 @@ async def transfer_credits_to_member(
if not team or team.manager_id != manager_id:
raise HTTPException(status_code=403, detail="只有团队管理人才能分配积分")
desc = description or "团队积分发放"
desc = description or ("团队积分发放" if direction == "increase" else "团队积分扣减")
xfer_id = generate_id()
# 从管理人扣减
if direction == "increase":
# 管理人扣减
await deduct_credits(
db,
manager_id,
amount,
f"分配给成员 {member.username}: {desc}",
biz_key=f"mgr_xfer_out:{manager_id}:{target_member_id}:{generate_id()}",
record_type="team_internal",
biz_key=f"mgr_xfer_out:{manager_id}:{target_member_id}:{xfer_id}",
)
# 成员增加
# 成员增加
await add_credits(
db,
target_member_id,
amount,
f"来自团队管理人: {desc}",
record_type="admin",
biz_key=f"mgr_xfer_in:{manager_id}:{target_member_id}:{generate_id()}",
record_type="team_internal",
biz_key=f"mgr_xfer_in:{manager_id}:{target_member_id}:{xfer_id}",
)
else:
# 成员扣减
await deduct_credits(
db,
target_member_id,
amount,
f"扣减给团队管理人: {desc}",
record_type="team_internal",
biz_key=f"mgr_deduct_out:{manager_id}:{target_member_id}:{xfer_id}",
)
# 管理人增加
await add_credits(
db,
manager_id,
amount,
f"来自成员 {member.username}: {desc}",
record_type="team_internal",
biz_key=f"mgr_deduct_in:{manager_id}:{target_member_id}:{xfer_id}",
)
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
}
})();
</script>
<script type="module" crossorigin src="/assets/index-XSsSl940.js"></script>
<script type="module" crossorigin src="/assets/index-DlbOvnzQ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D9_3MPsN.css">
</head>
<body>
+19 -2
View File
@@ -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()}`;
}
+183 -71
View File
@@ -1,16 +1,21 @@
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 { DatePicker } from 'antd';
import dayjs from 'dayjs';
import {
CopyOutlined, PlusOutlined, ReloadOutlined, UserOutlined, HistoryOutlined, WalletOutlined,
CopyOutlined, DownloadOutlined, PlusOutlined, ReloadOutlined, UserOutlined, HistoryOutlined, WalletOutlined,
} from '@ant-design/icons';
const { RangePicker } = DatePicker;
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';
import { useAuthStore } from '../store/useAuthStore';
const TeamManagementPage: React.FC = () => {
const [team, setTeam] = useState<ManagedTeam | null>(null);
@@ -61,14 +66,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 +174,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 +188,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 +319,105 @@ 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 || undefined}
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
/>
<RangePicker
value={creditFilterStart && creditFilterEnd ? [dayjs(creditFilterStart), dayjs(creditFilterEnd)] : undefined}
onChange={(dates) => {
if (dates && dates[0] && dates[1]) {
setCreditFilterStart(dates[0].format('YYYY-MM-DD'));
setCreditFilterEnd(dates[1].format('YYYY-MM-DD'));
} else {
setCreditFilterStart('');
setCreditFilterEnd('');
}
setCreditPage(1);
}}
/>
<Button onClick={resetCreditFilters}></Button>
<Button type="primary" icon={<DownloadOutlined />} onClick={handleExportCredits}> Excel</Button>
</div>
{/* 汇总统计 */}
<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>
</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 +453,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 +473,48 @@ 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' }}>
{/* 显示管理人当前积分 */}
<div style={{ marginBottom: 16, padding: '8px 12px', background: '#f0f4ff', borderRadius: 8, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography.Text type="secondary"></Typography.Text>
<Typography.Text strong style={{ fontSize: 18, color: '#6366f1' }}>
{useAuthStore.getState().user?.credits?.toFixed(2) ?? '0.00'}
</Typography.Text>
</div>
<Form.Item name="direction" label="操作类型" rules={[{ required: true, message: '请选择操作类型' }]}>
<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' },
{ type: 'number', max: 9999999, message: '单次不能超过 9999999' },
{
validator: (_, value) => {
if (!value || value <= 0) return Promise.reject(new Error('请输入有效的正数'));
const decimals = String(value).split('.')[1];
if (decimals && decimals.length > 2) return Promise.reject(new Error('最多两位小数'));
return Promise.resolve();
},
},
]}
>
<InputNumber style={{ width: '100%' }} step={1} min={0.01} precision={2} placeholder="请输入正数积分数量" size="large" />
</Form.Item>
<Form.Item name="description" label="备注">
<Input.TextArea rows={2} maxLength={256} placeholder="选填,例如:活动奖励" />