feat(admin-web): add ledger detail display for system accounts
- Add getAllLedger API method in systemAccountReportService - Add LedgerEntryDTO, FixedAccountLedger, RegionAccountLedger types - Add ALL_LEDGER endpoint - Update SystemAccountsTab with ledger detail tab - Add expandable card UI for viewing account ledger entries - Add styles for ledger cards and tables 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c3c15b7880
commit
44a1023cdd
|
|
@ -285,3 +285,126 @@
|
|||
padding-top: 16px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
/* [2026-01-05] 新增:分类账明细样式 */
|
||||
.sectionHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.refreshButton {
|
||||
padding: 6px 12px;
|
||||
background-color: #f3f4f6;
|
||||
color: #374151;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #e5e7eb;
|
||||
}
|
||||
}
|
||||
|
||||
.ledgerGroup {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.ledgerCard {
|
||||
background-color: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ledgerCardHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14px 16px;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
}
|
||||
|
||||
.ledgerCardTitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.accountName {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.accountSequence {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.ledgerCardInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ledgerCount {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.expandIcon {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.ledgerCardBody {
|
||||
padding: 16px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.emptyLedger {
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
color: #9ca3af;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.entryTypeBadge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
background-color: #e5e7eb;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.amountPositive {
|
||||
color: #059669;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.amountNegative {
|
||||
color: #dc2626;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.memo {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* 系统账户报表Tab组件
|
||||
* [2026-01-04] 新增:显示系统账户统计数据
|
||||
* [2026-01-05] 更新:添加分类账明细显示
|
||||
* 回滚方式:删除此文件及整个 system-account-report 目录
|
||||
*/
|
||||
'use client';
|
||||
|
|
@ -10,7 +11,12 @@ import { systemAccountReportService } from '@/services/systemAccountReportServic
|
|||
import type {
|
||||
SystemAccountReportResponse,
|
||||
RegionAccountsSummary,
|
||||
AllSystemAccountsLedgerResponse,
|
||||
FixedAccountLedger,
|
||||
RegionAccountLedger,
|
||||
LedgerEntryDTO,
|
||||
} from '@/types';
|
||||
import { ENTRY_TYPE_LABELS, ACCOUNT_TYPE_LABELS } from '@/types';
|
||||
import styles from './SystemAccountsTab.module.scss';
|
||||
|
||||
/**
|
||||
|
|
@ -46,7 +52,9 @@ export default function SystemAccountsTab() {
|
|||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [reportData, setReportData] = useState<SystemAccountReportResponse | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<'fixed' | 'province' | 'city' | 'settlement' | 'expired'>('fixed');
|
||||
const [ledgerData, setLedgerData] = useState<AllSystemAccountsLedgerResponse | null>(null);
|
||||
const [ledgerLoading, setLedgerLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'fixed' | 'province' | 'city' | 'settlement' | 'expired' | 'ledger'>('fixed');
|
||||
|
||||
// 加载报表数据
|
||||
const loadReportData = useCallback(async () => {
|
||||
|
|
@ -65,10 +73,32 @@ export default function SystemAccountsTab() {
|
|||
}
|
||||
}, []);
|
||||
|
||||
// 加载分类账明细
|
||||
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);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadReportData();
|
||||
}, [loadReportData]);
|
||||
|
||||
// 切换到分类账明细时加载数据
|
||||
useEffect(() => {
|
||||
if (activeTab === 'ledger' && !ledgerData && !ledgerLoading) {
|
||||
loadLedgerData();
|
||||
}
|
||||
}, [activeTab, ledgerData, ledgerLoading, loadLedgerData]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={styles.loading}>
|
||||
|
|
@ -127,6 +157,12 @@ export default function SystemAccountsTab() {
|
|||
>
|
||||
过期收益
|
||||
</button>
|
||||
<button
|
||||
className={`${styles.tab} ${activeTab === 'ledger' ? styles.active : ''}`}
|
||||
onClick={() => setActiveTab('ledger')}
|
||||
>
|
||||
分类账明细
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
|
|
@ -136,6 +172,7 @@ export default function SystemAccountsTab() {
|
|||
{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} />}
|
||||
</div>
|
||||
|
||||
{/* 报表生成时间 */}
|
||||
|
|
@ -392,3 +429,209 @@ function ExpiredRewardsSection({ data }: { data: SystemAccountReportResponse['ex
|
|||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个账户的分类账卡片
|
||||
*/
|
||||
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)} {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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,5 +214,7 @@ export const API_ENDPOINTS = {
|
|||
CITY_SUMMARY: '/v1/system-account-reports/city-summary',
|
||||
OFFLINE_SETTLEMENT: '/v1/system-account-reports/offline-settlement',
|
||||
EXPIRED_REWARDS: '/v1/system-account-reports/expired-rewards',
|
||||
// [2026-01-05] 新增:所有系统账户分类账明细
|
||||
ALL_LEDGER: '/v1/system-account-reports/all-ledger',
|
||||
},
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import type {
|
|||
RegionAccountsSummary,
|
||||
OfflineSettlementSummary,
|
||||
ExpiredRewardsSummary,
|
||||
AllSystemAccountsLedgerResponse,
|
||||
} from '@/types';
|
||||
|
||||
/**
|
||||
|
|
@ -68,6 +69,14 @@ export const systemAccountReportService = {
|
|||
async getExpiredRewards(params?: DateRangeParams): Promise<ApiResponse<ExpiredRewardsSummary>> {
|
||||
return apiClient.get(API_ENDPOINTS.SYSTEM_ACCOUNT_REPORTS.EXPIRED_REWARDS, { params });
|
||||
},
|
||||
|
||||
// [2026-01-05] 新增:获取所有系统账户分类账明细
|
||||
/**
|
||||
* 获取所有系统账户的分类账明细
|
||||
*/
|
||||
async getAllLedger(params?: DateRangeParams & { pageSize?: number }): Promise<ApiResponse<AllSystemAccountsLedgerResponse>> {
|
||||
return apiClient.get(API_ENDPOINTS.SYSTEM_ACCOUNT_REPORTS.ALL_LEDGER, { params });
|
||||
},
|
||||
};
|
||||
|
||||
export default systemAccountReportService;
|
||||
|
|
|
|||
|
|
@ -120,3 +120,68 @@ export const RIGHT_TYPE_LABELS: Record<string, string> = {
|
|||
CITY_REGION: '市区域权益',
|
||||
COMMUNITY: '社区权益',
|
||||
};
|
||||
|
||||
// [2026-01-05] 新增:分类账明细类型
|
||||
/**
|
||||
* 分类账条目
|
||||
*/
|
||||
export interface LedgerEntryDTO {
|
||||
id: string;
|
||||
entryType: string;
|
||||
amount: number;
|
||||
assetType: string;
|
||||
balanceAfter: number | null;
|
||||
refOrderId: string | null;
|
||||
refTxHash: string | null;
|
||||
memo: string | null;
|
||||
allocationType: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 固定账户分类账
|
||||
*/
|
||||
export interface FixedAccountLedger {
|
||||
accountSequence: string;
|
||||
accountType: string;
|
||||
ledger: LedgerEntryDTO[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 区域账户分类账
|
||||
*/
|
||||
export interface RegionAccountLedger {
|
||||
accountSequence: string;
|
||||
regionCode: string;
|
||||
regionName: string;
|
||||
ledger: LedgerEntryDTO[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有系统账户分类账响应
|
||||
*/
|
||||
export interface AllSystemAccountsLedgerResponse {
|
||||
fixedAccountsLedger: FixedAccountLedger[];
|
||||
provinceAccountsLedger: RegionAccountLedger[];
|
||||
cityAccountsLedger: RegionAccountLedger[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 流水类型显示名称映射
|
||||
*/
|
||||
export const ENTRY_TYPE_LABELS: Record<string, string> = {
|
||||
DEPOSIT: '充值',
|
||||
WITHDRAW: '提现',
|
||||
TRANSFER_IN: '转入',
|
||||
TRANSFER_OUT: '转出',
|
||||
REWARD_INCOME: '奖励收入',
|
||||
REWARD_SETTLE: '奖励结算',
|
||||
REWARD_EXPIRED: '奖励过期',
|
||||
PLANTING_PAYMENT: '认种支付',
|
||||
OFFLINE_SETTLEMENT: '面对面结算',
|
||||
FEE_DEDUCTION: '手续费扣除',
|
||||
ALLOCATION_IN: '分配收入',
|
||||
ALLOCATION_OUT: '分配支出',
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue