963 lines
32 KiB
TypeScript
963 lines
32 KiB
TypeScript
/**
|
||
* 系统账户报表Tab组件
|
||
* [2026-01-04] 新增:显示系统账户统计数据
|
||
* [2026-01-05] 更新:添加分类账明细显示
|
||
* [2026-01-06] 更新:添加收益类型汇总统计(手续费、省团队、市团队、分享引荐、社区)
|
||
* 回滚方式:删除此文件及整个 system-account-report 目录
|
||
*/
|
||
'use client';
|
||
|
||
import { useState, useEffect, useCallback } from 'react';
|
||
import { systemAccountReportService } from '@/services/systemAccountReportService';
|
||
import type {
|
||
SystemAccountReportResponse,
|
||
RegionAccountsSummary,
|
||
AllSystemAccountsLedgerResponse,
|
||
FixedAccountLedger,
|
||
RegionAccountLedger,
|
||
LedgerEntryDTO,
|
||
AllRewardTypeSummariesResponse,
|
||
RewardTypeSummary,
|
||
FeeAccountSummary,
|
||
} from '@/types';
|
||
import { ENTRY_TYPE_LABELS, ACCOUNT_TYPE_LABELS, FEE_TYPE_LABELS } from '@/types';
|
||
import styles from './SystemAccountsTab.module.scss';
|
||
|
||
/**
|
||
* 格式化金额显示
|
||
*/
|
||
const formatAmount = (value: string | number | undefined): string => {
|
||
if (value === undefined || value === null) return '0.00';
|
||
const num = typeof value === 'string' ? parseFloat(value) : value;
|
||
if (isNaN(num)) return '0.00';
|
||
return num.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||
};
|
||
|
||
/**
|
||
* 权益类型显示名称
|
||
*/
|
||
const getRightTypeName = (type: string): string => {
|
||
const labels: Record<string, string> = {
|
||
SETTLEABLE: '可结算收益',
|
||
SHARE: '分享权益',
|
||
PROVINCE_TEAM: '省团队权益',
|
||
CITY_TEAM: '市团队权益',
|
||
PROVINCE_REGION: '省区域权益',
|
||
CITY_REGION: '市区域权益',
|
||
COMMUNITY: '社区权益',
|
||
};
|
||
return labels[type] || type;
|
||
};
|
||
|
||
/**
|
||
* 系统账户报表Tab组件
|
||
*/
|
||
// Tab类型定义
|
||
type TabType = 'fixed' | 'province' | 'city' | 'settlement' | 'expired' | 'ledger' | 'feeAccount' | 'provinceTeam' | 'cityTeam' | 'shareReferral' | 'community';
|
||
|
||
export default function SystemAccountsTab() {
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [reportData, setReportData] = useState<SystemAccountReportResponse | null>(null);
|
||
const [ledgerData, setLedgerData] = useState<AllSystemAccountsLedgerResponse | null>(null);
|
||
const [ledgerLoading, setLedgerLoading] = useState(false);
|
||
// [2026-01-06] 新增:收益类型汇总数据
|
||
const [rewardTypeSummaries, setRewardTypeSummaries] = useState<AllRewardTypeSummariesResponse | null>(null);
|
||
const [rewardTypeSummariesLoading, setRewardTypeSummariesLoading] = useState(false);
|
||
const [activeTab, setActiveTab] = useState<TabType>('fixed');
|
||
|
||
// 加载报表数据
|
||
const loadReportData = useCallback(async () => {
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const response = await systemAccountReportService.getFullReport();
|
||
if (response.data) {
|
||
setReportData(response.data);
|
||
}
|
||
} catch (err) {
|
||
setError('加载系统账户报表失败');
|
||
console.error('Failed to load system account report:', err);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
// 加载分类账明细
|
||
const loadLedgerData = useCallback(async () => {
|
||
setLedgerLoading(true);
|
||
try {
|
||
const response = await systemAccountReportService.getAllLedger({ pageSize: 50 });
|
||
if (response.data) {
|
||
setLedgerData(response.data);
|
||
}
|
||
} catch (err) {
|
||
console.error('Failed to load ledger data:', err);
|
||
} finally {
|
||
setLedgerLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
// [2026-01-06] 新增:加载收益类型汇总数据
|
||
const loadRewardTypeSummaries = useCallback(async () => {
|
||
setRewardTypeSummariesLoading(true);
|
||
try {
|
||
const response = await systemAccountReportService.getRewardTypeSummaries();
|
||
if (response.data) {
|
||
setRewardTypeSummaries(response.data);
|
||
}
|
||
} catch (err) {
|
||
console.error('Failed to load reward type summaries:', err);
|
||
} finally {
|
||
setRewardTypeSummariesLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
loadReportData();
|
||
}, [loadReportData]);
|
||
|
||
// 切换到分类账明细时加载数据
|
||
useEffect(() => {
|
||
if (activeTab === 'ledger' && !ledgerData && !ledgerLoading) {
|
||
loadLedgerData();
|
||
}
|
||
}, [activeTab, ledgerData, ledgerLoading, loadLedgerData]);
|
||
|
||
// [2026-01-06] 切换到收益类型汇总Tab时加载数据
|
||
const rewardTypeTabs: TabType[] = ['feeAccount', 'provinceTeam', 'cityTeam', 'shareReferral', 'community'];
|
||
useEffect(() => {
|
||
if (rewardTypeTabs.includes(activeTab) && !rewardTypeSummaries && !rewardTypeSummariesLoading) {
|
||
loadRewardTypeSummaries();
|
||
}
|
||
}, [activeTab, rewardTypeSummaries, rewardTypeSummariesLoading, loadRewardTypeSummaries]);
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className={styles.loading}>
|
||
<div className={styles.spinner} />
|
||
<span>加载中...</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div className={styles.error}>
|
||
<span>{error}</span>
|
||
<button onClick={loadReportData} className={styles.retryButton}>
|
||
重试
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!reportData) {
|
||
return <div className={styles.empty}>暂无数据</div>;
|
||
}
|
||
|
||
return (
|
||
<div className={styles.container}>
|
||
{/* Tab 切换 */}
|
||
<div className={styles.tabs}>
|
||
<button
|
||
className={`${styles.tab} ${activeTab === 'fixed' ? styles.active : ''}`}
|
||
onClick={() => setActiveTab('fixed')}
|
||
>
|
||
固定系统账户
|
||
</button>
|
||
<button
|
||
className={`${styles.tab} ${activeTab === 'province' ? styles.active : ''}`}
|
||
onClick={() => setActiveTab('province')}
|
||
>
|
||
省区域账户汇总
|
||
</button>
|
||
<button
|
||
className={`${styles.tab} ${activeTab === 'city' ? styles.active : ''}`}
|
||
onClick={() => setActiveTab('city')}
|
||
>
|
||
市区域账户汇总
|
||
</button>
|
||
<button
|
||
className={`${styles.tab} ${activeTab === 'settlement' ? styles.active : ''}`}
|
||
onClick={() => setActiveTab('settlement')}
|
||
>
|
||
面对面结算
|
||
</button>
|
||
<button
|
||
className={`${styles.tab} ${activeTab === 'expired' ? styles.active : ''}`}
|
||
onClick={() => setActiveTab('expired')}
|
||
>
|
||
过期收益
|
||
</button>
|
||
<button
|
||
className={`${styles.tab} ${activeTab === 'ledger' ? styles.active : ''}`}
|
||
onClick={() => setActiveTab('ledger')}
|
||
>
|
||
分类账明细
|
||
</button>
|
||
{/* [2026-01-06] 新增:收益类型汇总Tab */}
|
||
<button
|
||
className={`${styles.tab} ${activeTab === 'feeAccount' ? styles.active : ''}`}
|
||
onClick={() => setActiveTab('feeAccount')}
|
||
>
|
||
手续费账户汇总
|
||
</button>
|
||
<button
|
||
className={`${styles.tab} ${activeTab === 'provinceTeam' ? styles.active : ''}`}
|
||
onClick={() => setActiveTab('provinceTeam')}
|
||
>
|
||
省团队收益汇总
|
||
</button>
|
||
<button
|
||
className={`${styles.tab} ${activeTab === 'cityTeam' ? styles.active : ''}`}
|
||
onClick={() => setActiveTab('cityTeam')}
|
||
>
|
||
市团队收益汇总
|
||
</button>
|
||
<button
|
||
className={`${styles.tab} ${activeTab === 'shareReferral' ? styles.active : ''}`}
|
||
onClick={() => setActiveTab('shareReferral')}
|
||
>
|
||
分享引荐收益汇总
|
||
</button>
|
||
<button
|
||
className={`${styles.tab} ${activeTab === 'community' ? styles.active : ''}`}
|
||
onClick={() => setActiveTab('community')}
|
||
>
|
||
社区收益汇总
|
||
</button>
|
||
</div>
|
||
|
||
{/* 内容区域 */}
|
||
<div className={styles.content}>
|
||
{activeTab === 'fixed' && <FixedAccountsSection data={reportData.fixedAccounts} />}
|
||
{activeTab === 'province' && <RegionAccountsSection data={reportData.provinceSummary} type="province" />}
|
||
{activeTab === 'city' && <RegionAccountsSection data={reportData.citySummary} type="city" />}
|
||
{activeTab === 'settlement' && <OfflineSettlementSection data={reportData.offlineSettlement} />}
|
||
{activeTab === 'expired' && <ExpiredRewardsSection data={reportData.expiredRewards} />}
|
||
{activeTab === 'ledger' && <LedgerSection data={ledgerData} loading={ledgerLoading} onRefresh={loadLedgerData} />}
|
||
{/* [2026-01-06] 新增:收益类型汇总Tab内容 */}
|
||
{activeTab === 'feeAccount' && (
|
||
<FeeAccountSection
|
||
data={rewardTypeSummaries?.feeAccountSummary}
|
||
loading={rewardTypeSummariesLoading}
|
||
onRefresh={loadRewardTypeSummaries}
|
||
/>
|
||
)}
|
||
{activeTab === 'provinceTeam' && (
|
||
<RewardTypeSummarySection
|
||
title="省团队收益汇总"
|
||
data={rewardTypeSummaries?.provinceTeamSummary}
|
||
loading={rewardTypeSummariesLoading}
|
||
onRefresh={loadRewardTypeSummaries}
|
||
/>
|
||
)}
|
||
{activeTab === 'cityTeam' && (
|
||
<RewardTypeSummarySection
|
||
title="市团队收益汇总"
|
||
data={rewardTypeSummaries?.cityTeamSummary}
|
||
loading={rewardTypeSummariesLoading}
|
||
onRefresh={loadRewardTypeSummaries}
|
||
/>
|
||
)}
|
||
{activeTab === 'shareReferral' && (
|
||
<RewardTypeSummarySection
|
||
title="分享引荐收益汇总"
|
||
data={rewardTypeSummaries?.shareReferralSummary}
|
||
loading={rewardTypeSummariesLoading}
|
||
onRefresh={loadRewardTypeSummaries}
|
||
/>
|
||
)}
|
||
{activeTab === 'community' && (
|
||
<RewardTypeSummarySection
|
||
title="社区收益汇总"
|
||
data={rewardTypeSummaries?.communitySummary}
|
||
loading={rewardTypeSummariesLoading}
|
||
onRefresh={loadRewardTypeSummaries}
|
||
/>
|
||
)}
|
||
</div>
|
||
|
||
{/* 报表生成时间 */}
|
||
<div className={styles.footer}>
|
||
报表生成时间: {new Date(reportData.generatedAt).toLocaleString('zh-CN')}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 固定系统账户区域
|
||
* [2026-01-05] 更新:根据业务需求调整显示名称,USDT改为绿积分
|
||
*/
|
||
function FixedAccountsSection({ data }: { data: SystemAccountReportResponse['fixedAccounts'] }) {
|
||
const accounts = [
|
||
{ key: 'costAccount', label: '总部储备', sequence: 'S0000000001', data: data.costAccount },
|
||
{ key: 'operationAccount', label: '运营账户1', sequence: 'S0000000002', data: data.operationAccount },
|
||
{ key: 'hqCommunity', label: '运营账户2', sequence: 'S0000000003', data: data.hqCommunity },
|
||
{ key: 'rwadPoolPending', label: '积分股池', sequence: 'S0000000004', data: data.rwadPoolPending },
|
||
{ key: 'platformFee', label: '平台手续费', sequence: 'S0000000005', data: data.platformFee },
|
||
];
|
||
|
||
return (
|
||
<div className={styles.section}>
|
||
<h3 className={styles.sectionTitle}>固定系统账户</h3>
|
||
<div className={styles.cardGrid}>
|
||
{accounts.map(({ key, label, sequence, data: accountData }) => (
|
||
<div key={key} className={styles.accountCard}>
|
||
<div className={styles.cardHeader}>
|
||
<span className={styles.accountLabel}>{label}</span>
|
||
<span className={styles.accountSequence}>{sequence}</span>
|
||
</div>
|
||
<div className={styles.cardBody}>
|
||
<div className={styles.statRow}>
|
||
<span className={styles.statLabel}>账户余额</span>
|
||
<span className={styles.statValue}>
|
||
{accountData ? formatAmount(accountData.usdtBalance) : '0.00'} 绿积分
|
||
</span>
|
||
</div>
|
||
<div className={styles.statRow}>
|
||
<span className={styles.statLabel}>累计收入</span>
|
||
<span className={styles.statValue}>
|
||
{accountData ? formatAmount(accountData.totalReceived) : '0.00'} 绿积分
|
||
</span>
|
||
</div>
|
||
<div className={styles.statRow}>
|
||
<span className={styles.statLabel}>累计转出</span>
|
||
<span className={styles.statValue}>
|
||
{accountData ? formatAmount(accountData.totalTransferred) : '0.00'} 绿积分
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 区域账户汇总区域
|
||
* [2026-01-05] 更新:USDT改为绿积分
|
||
*/
|
||
function RegionAccountsSection({ data, type }: { data: RegionAccountsSummary; type: 'province' | 'city' }) {
|
||
const typeLabel = type === 'province' ? '省' : '市';
|
||
|
||
return (
|
||
<div className={styles.section}>
|
||
<h3 className={styles.sectionTitle}>{typeLabel}区域账户汇总</h3>
|
||
|
||
{/* 汇总卡片 */}
|
||
<div className={styles.summaryCards}>
|
||
<div className={styles.summaryCard}>
|
||
<span className={styles.summaryLabel}>账户数量</span>
|
||
<span className={styles.summaryValue}>{data.summary.count}</span>
|
||
</div>
|
||
<div className={styles.summaryCard}>
|
||
<span className={styles.summaryLabel}>总余额</span>
|
||
<span className={styles.summaryValue}>{formatAmount(data.summary.totalBalance)} 绿积分</span>
|
||
</div>
|
||
<div className={styles.summaryCard}>
|
||
<span className={styles.summaryLabel}>累计收入</span>
|
||
<span className={styles.summaryValue}>{formatAmount(data.summary.totalReceived)} 绿积分</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 账户列表 */}
|
||
{data.accounts.length > 0 ? (
|
||
<div className={styles.tableWrapper}>
|
||
<table className={styles.table}>
|
||
<thead>
|
||
<tr>
|
||
<th>区域代码</th>
|
||
<th>区域名称</th>
|
||
<th>余额 (绿积分)</th>
|
||
<th>累计收入 (绿积分)</th>
|
||
<th>状态</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{data.accounts.map((account) => (
|
||
<tr key={account.id}>
|
||
<td>{account.regionCode || '-'}</td>
|
||
<td>{account.regionName || '-'}</td>
|
||
<td>{formatAmount(account.usdtBalance)}</td>
|
||
<td>{formatAmount(account.totalReceived)}</td>
|
||
<td>
|
||
<span className={`${styles.statusBadge} ${account.status === 'ACTIVE' ? styles.active : ''}`}>
|
||
{account.status === 'ACTIVE' ? '正常' : account.status}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
) : (
|
||
<div className={styles.emptyTable}>暂无{typeLabel}区域账户数据</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 面对面结算统计区域
|
||
* [2026-01-05] 更新:USDT改为绿积分,添加空值检查
|
||
*/
|
||
function OfflineSettlementSection({ data }: { data: SystemAccountReportResponse['offlineSettlement'] | null | undefined }) {
|
||
if (!data) {
|
||
return (
|
||
<div className={styles.section}>
|
||
<h3 className={styles.sectionTitle}>面对面结算统计</h3>
|
||
<div className={styles.emptyTable}>暂无面对面结算数据</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className={styles.section}>
|
||
<h3 className={styles.sectionTitle}>面对面结算统计</h3>
|
||
|
||
{/* 汇总卡片 */}
|
||
<div className={styles.summaryCards}>
|
||
<div className={styles.summaryCard}>
|
||
<span className={styles.summaryLabel}>总笔数</span>
|
||
<span className={styles.summaryValue}>{data.totalCount ?? 0}</span>
|
||
</div>
|
||
<div className={styles.summaryCard}>
|
||
<span className={styles.summaryLabel}>总金额</span>
|
||
<span className={styles.summaryValue}>{formatAmount(data.totalAmount)} 绿积分</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 按月统计 */}
|
||
{data.byMonth && data.byMonth.length > 0 && (
|
||
<>
|
||
<h4 className={styles.subTitle}>按月统计</h4>
|
||
<div className={styles.tableWrapper}>
|
||
<table className={styles.table}>
|
||
<thead>
|
||
<tr>
|
||
<th>月份</th>
|
||
<th>笔数</th>
|
||
<th>金额 (绿积分)</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{data.byMonth.map((item) => (
|
||
<tr key={item.month}>
|
||
<td>{item.month}</td>
|
||
<td>{item.count}</td>
|
||
<td>{formatAmount(item.amount)}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{(!data.byMonth || data.byMonth.length === 0) && (
|
||
<div className={styles.emptyTable}>暂无面对面结算数据</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 过期收益统计区域
|
||
* [2026-01-05] 更新:USDT改为绿积分,添加空值检查
|
||
*/
|
||
function ExpiredRewardsSection({ data }: { data: SystemAccountReportResponse['expiredRewards'] | null | undefined }) {
|
||
if (!data) {
|
||
return (
|
||
<div className={styles.section}>
|
||
<h3 className={styles.sectionTitle}>过期收益统计</h3>
|
||
<div className={styles.emptyTable}>暂无过期收益数据</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className={styles.section}>
|
||
<h3 className={styles.sectionTitle}>过期收益统计</h3>
|
||
|
||
{/* 汇总卡片 */}
|
||
<div className={styles.summaryCards}>
|
||
<div className={styles.summaryCard}>
|
||
<span className={styles.summaryLabel}>总笔数</span>
|
||
<span className={styles.summaryValue}>{data.totalCount ?? 0}</span>
|
||
</div>
|
||
<div className={styles.summaryCard}>
|
||
<span className={styles.summaryLabel}>总金额</span>
|
||
<span className={styles.summaryValue}>{formatAmount(data.totalAmount)} 绿积分</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 按权益类型统计 */}
|
||
{data.byRightType && data.byRightType.length > 0 && (
|
||
<>
|
||
<h4 className={styles.subTitle}>按权益类型统计</h4>
|
||
<div className={styles.tableWrapper}>
|
||
<table className={styles.table}>
|
||
<thead>
|
||
<tr>
|
||
<th>权益类型</th>
|
||
<th>笔数</th>
|
||
<th>金额 (绿积分)</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{data.byRightType.map((item) => (
|
||
<tr key={item.rightType}>
|
||
<td>{getRightTypeName(item.rightType)}</td>
|
||
<td>{item.count}</td>
|
||
<td>{formatAmount(item.amount)}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* 按月统计 */}
|
||
{data.byMonth && data.byMonth.length > 0 && (
|
||
<>
|
||
<h4 className={styles.subTitle}>按月统计</h4>
|
||
<div className={styles.tableWrapper}>
|
||
<table className={styles.table}>
|
||
<thead>
|
||
<tr>
|
||
<th>月份</th>
|
||
<th>笔数</th>
|
||
<th>金额 (绿积分)</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{data.byMonth.map((item) => (
|
||
<tr key={item.month}>
|
||
<td>{item.month}</td>
|
||
<td>{item.count}</td>
|
||
<td>{formatAmount(item.amount)}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{(!data.byRightType || data.byRightType.length === 0) && (!data.byMonth || data.byMonth.length === 0) && (
|
||
<div className={styles.emptyTable}>暂无过期收益数据</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// [2026-01-05] 新增:分类账明细组件
|
||
/**
|
||
* 分类账明细区域
|
||
*/
|
||
function LedgerSection({
|
||
data,
|
||
loading,
|
||
onRefresh,
|
||
}: {
|
||
data: AllSystemAccountsLedgerResponse | null;
|
||
loading: boolean;
|
||
onRefresh: () => void;
|
||
}) {
|
||
const [expandedAccounts, setExpandedAccounts] = useState<Set<string>>(new Set());
|
||
|
||
const toggleExpand = (accountKey: string) => {
|
||
setExpandedAccounts((prev) => {
|
||
const newSet = new Set(prev);
|
||
if (newSet.has(accountKey)) {
|
||
newSet.delete(accountKey);
|
||
} else {
|
||
newSet.add(accountKey);
|
||
}
|
||
return newSet;
|
||
});
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className={styles.loading}>
|
||
<div className={styles.spinner} />
|
||
<span>加载分类账明细中...</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!data) {
|
||
return (
|
||
<div className={styles.section}>
|
||
<div className={styles.emptyTable}>
|
||
<span>暂无分类账数据</span>
|
||
<button onClick={onRefresh} className={styles.retryButton}>
|
||
加载数据
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const getEntryTypeLabel = (type: string): string => {
|
||
return ENTRY_TYPE_LABELS[type] || type;
|
||
};
|
||
|
||
const getAccountTypeLabel = (type: string): string => {
|
||
return ACCOUNT_TYPE_LABELS[type] || type;
|
||
};
|
||
|
||
return (
|
||
<div className={styles.section}>
|
||
<div className={styles.sectionHeader}>
|
||
<h3 className={styles.sectionTitle}>分类账明细</h3>
|
||
<button onClick={onRefresh} className={styles.refreshButton}>
|
||
刷新
|
||
</button>
|
||
</div>
|
||
|
||
{/* 固定系统账户分类账 */}
|
||
{data.fixedAccountsLedger && data.fixedAccountsLedger.length > 0 && (
|
||
<div className={styles.ledgerGroup}>
|
||
<h4 className={styles.subTitle}>固定系统账户</h4>
|
||
{data.fixedAccountsLedger.map((account) => (
|
||
<LedgerAccountCard
|
||
key={account.accountSequence}
|
||
title={getAccountTypeLabel(account.accountType)}
|
||
subtitle={account.accountSequence}
|
||
ledger={account.ledger}
|
||
total={account.total}
|
||
expanded={expandedAccounts.has(account.accountSequence)}
|
||
onToggle={() => toggleExpand(account.accountSequence)}
|
||
getEntryTypeLabel={getEntryTypeLabel}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* 省区域账户分类账 */}
|
||
{data.provinceAccountsLedger && data.provinceAccountsLedger.length > 0 && (
|
||
<div className={styles.ledgerGroup}>
|
||
<h4 className={styles.subTitle}>省区域账户 ({data.provinceAccountsLedger.length}个)</h4>
|
||
{data.provinceAccountsLedger.map((account) => (
|
||
<LedgerAccountCard
|
||
key={account.accountSequence}
|
||
title={account.regionName || account.regionCode}
|
||
subtitle={account.accountSequence}
|
||
ledger={account.ledger}
|
||
total={account.total}
|
||
expanded={expandedAccounts.has(account.accountSequence)}
|
||
onToggle={() => toggleExpand(account.accountSequence)}
|
||
getEntryTypeLabel={getEntryTypeLabel}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* 市区域账户分类账 */}
|
||
{data.cityAccountsLedger && data.cityAccountsLedger.length > 0 && (
|
||
<div className={styles.ledgerGroup}>
|
||
<h4 className={styles.subTitle}>市区域账户 ({data.cityAccountsLedger.length}个)</h4>
|
||
{data.cityAccountsLedger.map((account) => (
|
||
<LedgerAccountCard
|
||
key={account.accountSequence}
|
||
title={account.regionName || account.regionCode}
|
||
subtitle={account.accountSequence}
|
||
ledger={account.ledger}
|
||
total={account.total}
|
||
expanded={expandedAccounts.has(account.accountSequence)}
|
||
onToggle={() => toggleExpand(account.accountSequence)}
|
||
getEntryTypeLabel={getEntryTypeLabel}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{(!data.fixedAccountsLedger || data.fixedAccountsLedger.length === 0) &&
|
||
(!data.provinceAccountsLedger || data.provinceAccountsLedger.length === 0) &&
|
||
(!data.cityAccountsLedger || data.cityAccountsLedger.length === 0) && (
|
||
<div className={styles.emptyTable}>暂无分类账明细数据</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 资产类型显示名称映射
|
||
* [2026-01-05] 新增:USDT改为绿积分
|
||
*/
|
||
const getAssetTypeLabel = (assetType: string): string => {
|
||
const labels: Record<string, string> = {
|
||
USDT: '绿积分',
|
||
usdt: '绿积分',
|
||
};
|
||
return labels[assetType] || assetType;
|
||
};
|
||
|
||
/**
|
||
* 单个账户的分类账卡片
|
||
* [2026-01-05] 更新:资产类型显示改用中文
|
||
*/
|
||
function LedgerAccountCard({
|
||
title,
|
||
subtitle,
|
||
ledger,
|
||
total,
|
||
expanded,
|
||
onToggle,
|
||
getEntryTypeLabel,
|
||
}: {
|
||
title: string;
|
||
subtitle: string;
|
||
ledger: LedgerEntryDTO[];
|
||
total: number;
|
||
expanded: boolean;
|
||
onToggle: () => void;
|
||
getEntryTypeLabel: (type: string) => string;
|
||
}) {
|
||
return (
|
||
<div className={styles.ledgerCard}>
|
||
<div className={styles.ledgerCardHeader} onClick={onToggle}>
|
||
<div className={styles.ledgerCardTitle}>
|
||
<span className={styles.accountName}>{title}</span>
|
||
<span className={styles.accountSequence}>{subtitle}</span>
|
||
</div>
|
||
<div className={styles.ledgerCardInfo}>
|
||
<span className={styles.ledgerCount}>{total} 条记录</span>
|
||
<span className={styles.expandIcon}>{expanded ? '▼' : '▶'}</span>
|
||
</div>
|
||
</div>
|
||
{expanded && (
|
||
<div className={styles.ledgerCardBody}>
|
||
{ledger.length > 0 ? (
|
||
<div className={styles.tableWrapper}>
|
||
<table className={styles.table}>
|
||
<thead>
|
||
<tr>
|
||
<th>时间</th>
|
||
<th>类型</th>
|
||
<th>金额</th>
|
||
<th>余额</th>
|
||
<th>备注</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{ledger.map((entry) => (
|
||
<tr key={entry.id}>
|
||
<td>{new Date(entry.createdAt).toLocaleString('zh-CN')}</td>
|
||
<td>
|
||
<span className={styles.entryTypeBadge}>
|
||
{getEntryTypeLabel(entry.entryType)}
|
||
</span>
|
||
</td>
|
||
<td className={entry.amount >= 0 ? styles.amountPositive : styles.amountNegative}>
|
||
{entry.amount >= 0 ? '+' : ''}{formatAmount(entry.amount)} {getAssetTypeLabel(entry.assetType)}
|
||
</td>
|
||
<td>{entry.balanceAfter !== null ? formatAmount(entry.balanceAfter) : '-'}</td>
|
||
<td className={styles.memo}>{entry.memo || entry.allocationType || '-'}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
) : (
|
||
<div className={styles.emptyLedger}>暂无流水记录</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// [2026-01-06] 新增:手续费账户汇总组件
|
||
/**
|
||
* 手续费账户汇总区域
|
||
*/
|
||
function FeeAccountSection({
|
||
data,
|
||
loading,
|
||
onRefresh,
|
||
}: {
|
||
data: FeeAccountSummary | undefined;
|
||
loading: boolean;
|
||
onRefresh: () => void;
|
||
}) {
|
||
if (loading) {
|
||
return (
|
||
<div className={styles.loading}>
|
||
<div className={styles.spinner} />
|
||
<span>加载手续费账户汇总中...</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!data) {
|
||
return (
|
||
<div className={styles.section}>
|
||
<div className={styles.emptyTable}>
|
||
<span>暂无手续费账户数据</span>
|
||
<button onClick={onRefresh} className={styles.retryButton}>
|
||
加载数据
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className={styles.section}>
|
||
<div className={styles.sectionHeader}>
|
||
<h3 className={styles.sectionTitle}>手续费账户汇总</h3>
|
||
<button onClick={onRefresh} className={styles.refreshButton}>
|
||
刷新
|
||
</button>
|
||
</div>
|
||
|
||
{/* 汇总卡片 */}
|
||
<div className={styles.summaryCards}>
|
||
<div className={styles.summaryCard}>
|
||
<span className={styles.summaryLabel}>总金额</span>
|
||
<span className={styles.summaryValue}>{formatAmount(data.totalAmount)} 绿积分</span>
|
||
</div>
|
||
<div className={styles.summaryCard}>
|
||
<span className={styles.summaryLabel}>总笔数</span>
|
||
<span className={styles.summaryValue}>{data.totalCount}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 按类型明细 */}
|
||
{data.breakdown && data.breakdown.length > 0 ? (
|
||
<>
|
||
<h4 className={styles.subTitle}>按类型明细</h4>
|
||
<div className={styles.tableWrapper}>
|
||
<table className={styles.table}>
|
||
<thead>
|
||
<tr>
|
||
<th>费用类型</th>
|
||
<th>金额 (绿积分)</th>
|
||
<th>笔数</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{data.breakdown.map((item) => (
|
||
<tr key={item.rightType}>
|
||
<td>{FEE_TYPE_LABELS[item.rightType] || item.rightType}</td>
|
||
<td>{formatAmount(item.amount)}</td>
|
||
<td>{item.count}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className={styles.emptyTable}>暂无手续费数据</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// [2026-01-06] 新增:收益类型汇总组件
|
||
/**
|
||
* 收益类型汇总区域(省团队、市团队、分享引荐、社区)
|
||
*/
|
||
function RewardTypeSummarySection({
|
||
title,
|
||
data,
|
||
loading,
|
||
onRefresh,
|
||
}: {
|
||
title: string;
|
||
data: RewardTypeSummary | undefined;
|
||
loading: boolean;
|
||
onRefresh: () => void;
|
||
}) {
|
||
if (loading) {
|
||
return (
|
||
<div className={styles.loading}>
|
||
<div className={styles.spinner} />
|
||
<span>加载{title}中...</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!data) {
|
||
return (
|
||
<div className={styles.section}>
|
||
<div className={styles.emptyTable}>
|
||
<span>暂无数据</span>
|
||
<button onClick={onRefresh} className={styles.retryButton}>
|
||
加载数据
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className={styles.section}>
|
||
<div className={styles.sectionHeader}>
|
||
<h3 className={styles.sectionTitle}>{title}</h3>
|
||
<button onClick={onRefresh} className={styles.refreshButton}>
|
||
刷新
|
||
</button>
|
||
</div>
|
||
|
||
{/* 汇总卡片 */}
|
||
<div className={styles.summaryCards}>
|
||
<div className={styles.summaryCard}>
|
||
<span className={styles.summaryLabel}>总金额</span>
|
||
<span className={styles.summaryValue}>{formatAmount(data.totalAmount)} 绿积分</span>
|
||
</div>
|
||
<div className={styles.summaryCard}>
|
||
<span className={styles.summaryLabel}>总笔数</span>
|
||
<span className={styles.summaryValue}>{data.totalCount}</span>
|
||
</div>
|
||
<div className={styles.summaryCard}>
|
||
<span className={styles.summaryLabel}>可结算金额</span>
|
||
<span className={styles.summaryValue}>{formatAmount(data.totalSettleableAmount)} 绿积分</span>
|
||
</div>
|
||
<div className={styles.summaryCard}>
|
||
<span className={styles.summaryLabel}>已结算金额</span>
|
||
<span className={styles.summaryValue}>{formatAmount(data.totalSettledAmount)} 绿积分</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 按月统计 */}
|
||
{data.byMonth && data.byMonth.length > 0 ? (
|
||
<>
|
||
<h4 className={styles.subTitle}>按月统计</h4>
|
||
<div className={styles.tableWrapper}>
|
||
<table className={styles.table}>
|
||
<thead>
|
||
<tr>
|
||
<th>月份</th>
|
||
<th>金额 (绿积分)</th>
|
||
<th>笔数</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{data.byMonth.map((item) => (
|
||
<tr key={item.month}>
|
||
<td>{item.month}</td>
|
||
<td>{formatAmount(item.amount)}</td>
|
||
<td>{item.count}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className={styles.emptyTable}>暂无月度数据</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|