feat(admin-web): add system account transfer management page

- Add system-transfer page with transfer form and order history
- Add SystemWithdrawalService for API calls
- Add useSystemWithdrawal hooks for React Query integration
- Add system-withdrawal types definitions
- Add navigation menu item for system transfer

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-06 10:30:35 -08:00
parent fe8e9a9bb6
commit b947fe8205
8 changed files with 1346 additions and 0 deletions

View File

@ -0,0 +1,690 @@
'use client';
import { useState, useCallback } from 'react';
import { Modal, toast, Button } from '@/components/common';
import { PageContainer } from '@/components/layout';
import { cn } from '@/utils/helpers';
import { formatDateTime } from '@/utils/formatters';
import {
useSystemWithdrawalAccounts,
useSystemWithdrawalOrders,
useRequestSystemWithdrawal,
} from '@/hooks/useSystemWithdrawal';
import {
SystemWithdrawalOrder,
SystemWithdrawalStatus,
SystemAccount,
getSystemWithdrawalStatusInfo,
} from '@/types/system-withdrawal.types';
import styles from './system-transfer.module.scss';
type TabType = 'transfer' | 'orders';
/**
*
*/
export default function SystemTransferPage() {
// 当前标签页
const [activeTab, setActiveTab] = useState<TabType>('transfer');
// 筛选状态
const [filters, setFilters] = useState({
fromAccountSequence: '',
toAccountSequence: '',
status: '' as SystemWithdrawalStatus | '',
page: 1,
limit: 20,
});
// 表单状态
const [selectedAccount, setSelectedAccount] = useState<SystemAccount | null>(null);
const [transferForm, setTransferForm] = useState({
toAccountSequence: '',
amount: '',
memo: '',
});
const [formErrors, setFormErrors] = useState<Record<string, string>>({});
// 弹窗状态
const [viewingOrder, setViewingOrder] = useState<SystemWithdrawalOrder | null>(null);
const [confirmModalOpen, setConfirmModalOpen] = useState(false);
// 数据查询
const accountsQuery = useSystemWithdrawalAccounts();
const ordersQuery = useSystemWithdrawalOrders({
fromAccountSequence: filters.fromAccountSequence || undefined,
toAccountSequence: filters.toAccountSequence || undefined,
status: filters.status || undefined,
page: filters.page,
limit: filters.limit,
});
// Mutations
const requestMutation = useRequestSystemWithdrawal();
// 表单验证
const validateForm = (): boolean => {
const errors: Record<string, string> = {};
if (!selectedAccount) {
errors.fromAccount = '请选择源账户';
}
if (!transferForm.toAccountSequence) {
errors.toAccountSequence = '请输入目标账户序列号';
} else if (!/^D\d{11}$/.test(transferForm.toAccountSequence)) {
errors.toAccountSequence = '账户序列号格式不正确,应为 D + 11位数字';
}
if (!transferForm.amount) {
errors.amount = '请输入划转金额';
} else {
const amount = parseFloat(transferForm.amount);
if (isNaN(amount) || amount <= 0) {
errors.amount = '请输入有效的金额';
} else if (amount < 1) {
errors.amount = '最小划转金额为 1 USDT';
}
}
setFormErrors(errors);
return Object.keys(errors).length === 0;
};
// 提交划转请求
const handleSubmitTransfer = async () => {
if (!validateForm() || !selectedAccount) return;
try {
await requestMutation.mutateAsync({
fromAccountSequence: selectedAccount.accountSequence,
toAccountSequence: transferForm.toAccountSequence,
amount: transferForm.amount,
memo: transferForm.memo || undefined,
});
toast.success('划转请求已提交,等待区块链确认');
setConfirmModalOpen(false);
// 重置表单
setSelectedAccount(null);
setTransferForm({ toAccountSequence: '', amount: '', memo: '' });
setFormErrors({});
// 切换到订单列表查看
setActiveTab('orders');
} catch (err) {
toast.error((err as Error).message || '划转失败');
}
};
// 打开确认弹窗
const handleOpenConfirm = () => {
if (validateForm()) {
setConfirmModalOpen(true);
}
};
// 搜索
const handleSearch = useCallback(() => {
setFilters((prev) => ({ ...prev, page: 1 }));
}, []);
// 翻页
const handlePageChange = (page: number) => {
setFilters((prev) => ({ ...prev, page }));
};
// 渲染账户选择卡片
const renderAccountCards = () => {
if (accountsQuery.isLoading) {
return <div className={styles.systemTransfer__loading}>...</div>;
}
if (accountsQuery.error) {
return (
<div className={styles.systemTransfer__error}>
<span>{(accountsQuery.error as Error).message || '加载失败'}</span>
<Button variant="outline" size="sm" onClick={() => accountsQuery.refetch()}>
</Button>
</div>
);
}
const accounts = accountsQuery.data || [];
if (accounts.length === 0) {
return <div className={styles.systemTransfer__empty}></div>;
}
return (
<div className={styles.systemTransfer__accountCards}>
{accounts.map((account) => (
<div
key={account.accountSequence}
className={cn(
styles.systemTransfer__accountCard,
selectedAccount?.accountSequence === account.accountSequence &&
styles['systemTransfer__accountCard--selected']
)}
onClick={() => setSelectedAccount(account)}
>
<div className={styles.systemTransfer__accountCardName}>{account.name}</div>
<div className={styles.systemTransfer__accountCardSequence}>{account.accountSequence}</div>
{account.balance && (
<div className={styles.systemTransfer__accountCardBalance}>
: {parseFloat(account.balance).toFixed(2)} USDT
</div>
)}
</div>
))}
</div>
);
};
// 渲染划转表单
const renderTransferForm = () => {
return (
<>
<div className={styles.systemTransfer__warning}>
</div>
<h3 style={{ marginBottom: '16px', fontSize: '16px' }}>1. </h3>
{renderAccountCards()}
{formErrors.fromAccount && (
<div className={styles.systemTransfer__formError}>{formErrors.fromAccount}</div>
)}
<h3 style={{ marginBottom: '16px', fontSize: '16px' }}>2. </h3>
<div className={styles.systemTransfer__form}>
<div className={styles.systemTransfer__formGroup}>
<label> *</label>
<input
type="text"
value={transferForm.toAccountSequence}
onChange={(e) => {
setTransferForm({ ...transferForm, toAccountSequence: e.target.value });
setFormErrors({ ...formErrors, toAccountSequence: '' });
}}
placeholder="例如D25121400005"
/>
<div className={styles.systemTransfer__formHint}>D开头+11</div>
{formErrors.toAccountSequence && (
<div className={styles.systemTransfer__formError}>{formErrors.toAccountSequence}</div>
)}
</div>
<div className={styles.systemTransfer__formGroup}>
<label> (USDT) *</label>
<input
type="number"
value={transferForm.amount}
onChange={(e) => {
setTransferForm({ ...transferForm, amount: e.target.value });
setFormErrors({ ...formErrors, amount: '' });
}}
placeholder="例如100"
min="1"
step="0.01"
/>
<div className={styles.systemTransfer__formHint}> 1 USDT</div>
{formErrors.amount && (
<div className={styles.systemTransfer__formError}>{formErrors.amount}</div>
)}
</div>
<div className={styles.systemTransfer__formGroup}>
<label> ()</label>
<textarea
value={transferForm.memo}
onChange={(e) => setTransferForm({ ...transferForm, memo: e.target.value })}
placeholder="请输入划转备注..."
rows={3}
/>
</div>
<div style={{ marginTop: '16px' }}>
<Button
variant="primary"
onClick={handleOpenConfirm}
disabled={!selectedAccount || !transferForm.toAccountSequence || !transferForm.amount}
>
</Button>
</div>
</div>
</>
);
};
// 渲染订单列表
const renderOrderList = () => {
const { data, isLoading, error, refetch } = ordersQuery;
return (
<>
{/* 筛选区域 */}
<div className={styles.systemTransfer__filters}>
<input
type="text"
className={styles.systemTransfer__input}
placeholder="源账户"
value={filters.fromAccountSequence}
onChange={(e) => setFilters({ ...filters, fromAccountSequence: e.target.value })}
/>
<input
type="text"
className={styles.systemTransfer__input}
placeholder="目标账户"
value={filters.toAccountSequence}
onChange={(e) => setFilters({ ...filters, toAccountSequence: e.target.value })}
/>
<select
className={styles.systemTransfer__select}
value={filters.status}
onChange={(e) =>
setFilters({ ...filters, status: e.target.value as SystemWithdrawalStatus | '' })
}
>
<option value=""></option>
<option value="PENDING"></option>
<option value="FROZEN"></option>
<option value="BROADCASTED">广</option>
<option value="CONFIRMED"></option>
<option value="FAILED"></option>
</select>
<Button variant="outline" size="sm" onClick={handleSearch}>
</Button>
<Button variant="outline" size="sm" onClick={() => refetch()}>
</Button>
</div>
{/* 列表 */}
<div className={styles.systemTransfer__list}>
{isLoading ? (
<div className={styles.systemTransfer__loading}>...</div>
) : error ? (
<div className={styles.systemTransfer__error}>
<span>{(error as Error).message || '加载失败'}</span>
<Button variant="outline" size="sm" onClick={() => refetch()}>
</Button>
</div>
) : !data || data.items.length === 0 ? (
<div className={styles.systemTransfer__empty}></div>
) : (
<>
{/* 表格 */}
<table className={styles.systemTransfer__table}>
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th>TxHash</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{data.items.map((order) => {
const statusInfo = getSystemWithdrawalStatusInfo(order.status);
return (
<tr key={order.orderNo}>
<td>
<div className={styles.systemTransfer__orderNo}>{order.orderNo}</div>
</td>
<td>
<div className={styles.systemTransfer__accountSequence}>
{order.fromAccountSequence}
</div>
<div className={styles.systemTransfer__accountName}>
{order.fromAccountName}
</div>
</td>
<td>
<div className={styles.systemTransfer__accountSequence}>
{order.toAccountSequence}
</div>
{order.toUserName && (
<div className={styles.systemTransfer__accountName}>
{order.toUserName}
</div>
)}
</td>
<td>
<div className={styles.systemTransfer__amount}>
{parseFloat(order.amount).toFixed(2)} USDT
</div>
</td>
<td>
<span
className={styles.systemTransfer__statusTag}
style={{ backgroundColor: statusInfo.color, color: 'white' }}
>
{statusInfo.label}
</span>
</td>
<td>
{order.txHash ? (
<div className={styles.systemTransfer__txHash} title={order.txHash}>
{order.txHash.slice(0, 10)}...
</div>
) : (
<span style={{ color: '#999' }}>-</span>
)}
</td>
<td>{formatDateTime(order.createdAt)}</td>
<td>
<div className={styles.systemTransfer__actions}>
<button
className={styles.systemTransfer__actionBtn}
onClick={() => setViewingOrder(order)}
>
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
{/* 分页 */}
{data.total > filters.limit && (
<div className={styles.systemTransfer__pagination}>
<span>
{data.total} {data.page} / {Math.ceil(data.total / data.limit)}
</span>
<div className={styles.systemTransfer__pageButtons}>
<Button
variant="outline"
size="sm"
disabled={data.page <= 1}
onClick={() => handlePageChange(data.page - 1)}
>
</Button>
<Button
variant="outline"
size="sm"
disabled={data.page >= Math.ceil(data.total / data.limit)}
onClick={() => handlePageChange(data.page + 1)}
>
</Button>
</div>
</div>
)}
</>
)}
</div>
</>
);
};
return (
<PageContainer title="系统划转">
<div className={styles.systemTransfer}>
{/* 页面标题 */}
<div className={styles.systemTransfer__header}>
<h1 className={styles.systemTransfer__title}></h1>
<p className={styles.systemTransfer__subtitle}>
</p>
</div>
{/* 主内容卡片 */}
<div className={styles.systemTransfer__card}>
{/* 标签页 */}
<div className={styles.systemTransfer__tabs}>
<button
className={cn(
styles.systemTransfer__tab,
activeTab === 'transfer' && styles['systemTransfer__tab--active']
)}
onClick={() => setActiveTab('transfer')}
>
</button>
<button
className={cn(
styles.systemTransfer__tab,
activeTab === 'orders' && styles['systemTransfer__tab--active']
)}
onClick={() => setActiveTab('orders')}
>
</button>
</div>
{/* 内容区域 */}
{activeTab === 'transfer' ? renderTransferForm() : renderOrderList()}
</div>
{/* 查看详情弹窗 */}
<Modal
visible={!!viewingOrder}
title="划转订单详情"
onClose={() => setViewingOrder(null)}
footer={
<div className={styles.systemTransfer__modalFooter}>
<Button variant="outline" onClick={() => setViewingOrder(null)}>
</Button>
</div>
}
width={600}
>
{viewingOrder && (
<div className={styles.systemTransfer__detail}>
<div className={styles.systemTransfer__detailSection}>
<div className={styles.systemTransfer__detailTitle}></div>
<div className={styles.systemTransfer__detailRow}>
<span className={styles.systemTransfer__detailLabel}>:</span>
<span className={styles.systemTransfer__detailValue}>{viewingOrder.orderNo}</span>
</div>
<div className={styles.systemTransfer__detailRow}>
<span className={styles.systemTransfer__detailLabel}>:</span>
<span className={styles.systemTransfer__detailValue}>
{parseFloat(viewingOrder.amount).toFixed(2)} USDT
</span>
</div>
<div className={styles.systemTransfer__detailRow}>
<span className={styles.systemTransfer__detailLabel}>:</span>
<span
className={styles.systemTransfer__statusTag}
style={{
backgroundColor: getSystemWithdrawalStatusInfo(viewingOrder.status).color,
color: 'white',
}}
>
{getSystemWithdrawalStatusInfo(viewingOrder.status).label}
</span>
</div>
<div className={styles.systemTransfer__detailRow}>
<span className={styles.systemTransfer__detailLabel}>:</span>
<span className={styles.systemTransfer__detailValue}>
{formatDateTime(viewingOrder.createdAt)}
</span>
</div>
</div>
<div className={styles.systemTransfer__detailSection}>
<div className={styles.systemTransfer__detailTitle}></div>
<div className={styles.systemTransfer__detailRow}>
<span className={styles.systemTransfer__detailLabel}>:</span>
<span className={styles.systemTransfer__detailValue}>
{viewingOrder.fromAccountSequence}
</span>
</div>
<div className={styles.systemTransfer__detailRow}>
<span className={styles.systemTransfer__detailLabel}>:</span>
<span className={styles.systemTransfer__detailValue}>
{viewingOrder.fromAccountName}
</span>
</div>
</div>
<div className={styles.systemTransfer__detailSection}>
<div className={styles.systemTransfer__detailTitle}></div>
<div className={styles.systemTransfer__detailRow}>
<span className={styles.systemTransfer__detailLabel}>:</span>
<span className={styles.systemTransfer__detailValue}>
{viewingOrder.toAccountSequence}
</span>
</div>
<div className={styles.systemTransfer__detailRow}>
<span className={styles.systemTransfer__detailLabel}>ID:</span>
<span className={styles.systemTransfer__detailValue}>{viewingOrder.toUserId}</span>
</div>
{viewingOrder.toUserName && (
<div className={styles.systemTransfer__detailRow}>
<span className={styles.systemTransfer__detailLabel}>:</span>
<span className={styles.systemTransfer__detailValue}>
{viewingOrder.toUserName}
</span>
</div>
)}
<div className={styles.systemTransfer__detailRow}>
<span className={styles.systemTransfer__detailLabel}>:</span>
<span
className={styles.systemTransfer__detailValue}
style={{ wordBreak: 'break-all' }}
>
{viewingOrder.toAddress}
</span>
</div>
</div>
{(viewingOrder.txHash || viewingOrder.errorMessage) && (
<div className={styles.systemTransfer__detailSection}>
<div className={styles.systemTransfer__detailTitle}></div>
{viewingOrder.txHash && (
<div className={styles.systemTransfer__detailRow}>
<span className={styles.systemTransfer__detailLabel}>TxHash:</span>
<span
className={styles.systemTransfer__detailValue}
style={{ wordBreak: 'break-all', fontFamily: 'monospace' }}
>
{viewingOrder.txHash}
</span>
</div>
)}
{viewingOrder.errorMessage && (
<div className={styles.systemTransfer__detailRow}>
<span className={styles.systemTransfer__detailLabel}>:</span>
<span className={styles.systemTransfer__detailValue} style={{ color: '#ff4d4f' }}>
{viewingOrder.errorMessage}
</span>
</div>
)}
</div>
)}
{viewingOrder.memo && (
<div className={styles.systemTransfer__detailSection}>
<div className={styles.systemTransfer__detailTitle}></div>
<div className={styles.systemTransfer__detailRow}>
<span className={styles.systemTransfer__detailValue}>{viewingOrder.memo}</span>
</div>
</div>
)}
<div className={styles.systemTransfer__detailSection}>
<div className={styles.systemTransfer__detailTitle}></div>
<div className={styles.systemTransfer__detailRow}>
<span className={styles.systemTransfer__detailLabel}>:</span>
<span className={styles.systemTransfer__detailValue}>
{viewingOrder.operatorName || viewingOrder.operatorId}
</span>
</div>
{viewingOrder.frozenAt && (
<div className={styles.systemTransfer__detailRow}>
<span className={styles.systemTransfer__detailLabel}>:</span>
<span className={styles.systemTransfer__detailValue}>
{formatDateTime(viewingOrder.frozenAt)}
</span>
</div>
)}
{viewingOrder.broadcastedAt && (
<div className={styles.systemTransfer__detailRow}>
<span className={styles.systemTransfer__detailLabel}>广:</span>
<span className={styles.systemTransfer__detailValue}>
{formatDateTime(viewingOrder.broadcastedAt)}
</span>
</div>
)}
{viewingOrder.confirmedAt && (
<div className={styles.systemTransfer__detailRow}>
<span className={styles.systemTransfer__detailLabel}>:</span>
<span className={styles.systemTransfer__detailValue}>
{formatDateTime(viewingOrder.confirmedAt)}
</span>
</div>
)}
</div>
</div>
)}
</Modal>
{/* 确认划转弹窗 */}
<Modal
visible={confirmModalOpen}
title="确认划转"
onClose={() => setConfirmModalOpen(false)}
footer={
<div className={styles.systemTransfer__modalFooter}>
<Button variant="outline" onClick={() => setConfirmModalOpen(false)}>
</Button>
<Button
variant="primary"
onClick={handleSubmitTransfer}
loading={requestMutation.isPending}
>
</Button>
</div>
}
width={500}
>
<div className={styles.systemTransfer__detail}>
<div className={styles.systemTransfer__warning}>
</div>
<div className={styles.systemTransfer__detailSection}>
<div className={styles.systemTransfer__detailRow}>
<span className={styles.systemTransfer__detailLabel}>:</span>
<span className={styles.systemTransfer__detailValue}>
{selectedAccount?.name} ({selectedAccount?.accountSequence})
</span>
</div>
<div className={styles.systemTransfer__detailRow}>
<span className={styles.systemTransfer__detailLabel}>:</span>
<span className={styles.systemTransfer__detailValue}>
{transferForm.toAccountSequence}
</span>
</div>
<div className={styles.systemTransfer__detailRow}>
<span className={styles.systemTransfer__detailLabel}>:</span>
<span className={styles.systemTransfer__detailValue}>
{transferForm.amount} USDT
</span>
</div>
{transferForm.memo && (
<div className={styles.systemTransfer__detailRow}>
<span className={styles.systemTransfer__detailLabel}>:</span>
<span className={styles.systemTransfer__detailValue}>{transferForm.memo}</span>
</div>
)}
</div>
</div>
</Modal>
</div>
</PageContainer>
);
}

View File

@ -0,0 +1,427 @@
@use '@/styles/variables' as *;
.systemTransfer {
display: flex;
flex-direction: column;
gap: 24px;
&__header {
display: flex;
flex-direction: column;
gap: 8px;
}
&__title {
font-size: 24px;
font-weight: 600;
color: $text-primary;
}
&__subtitle {
font-size: 14px;
color: $text-secondary;
}
&__card {
background: $card-background;
border-radius: 12px;
padding: 24px;
box-shadow: $shadow-base;
}
// 标签页
&__tabs {
display: flex;
gap: 4px;
margin-bottom: 20px;
border-bottom: 1px solid $border-color;
padding-bottom: 0;
}
&__tab {
padding: 12px 20px;
font-size: 14px;
font-weight: 500;
color: $text-secondary;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
transition: all 0.2s;
margin-bottom: -1px;
&:hover {
color: $primary-color;
}
&--active {
color: $primary-color;
border-bottom-color: $primary-color;
}
}
// 筛选区域
&__filters {
display: flex;
gap: 12px;
margin-bottom: 20px;
flex-wrap: wrap;
}
&__input {
padding: 8px 12px;
border: 1px solid $border-color;
border-radius: 8px;
font-size: 14px;
background: white;
min-width: 140px;
&:focus {
outline: none;
border-color: $primary-color;
}
}
&__select {
padding: 8px 12px;
border: 1px solid $border-color;
border-radius: 8px;
font-size: 14px;
background: white;
min-width: 140px;
&:focus {
outline: none;
border-color: $primary-color;
}
}
// 列表区域
&__list {
display: flex;
flex-direction: column;
gap: 16px;
}
&__loading,
&__empty,
&__error {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
padding: 60px 20px;
color: $text-secondary;
font-size: 14px;
}
&__error {
color: $error-color;
}
// 表格样式
&__table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
th,
td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid $border-color;
}
th {
font-weight: 600;
color: $text-secondary;
background: #fafafa;
}
td {
color: $text-primary;
}
tbody tr {
transition: background 0.15s;
&:hover {
background: #f5f7fa;
}
}
}
&__orderNo {
font-family: monospace;
font-size: 12px;
color: $text-secondary;
}
&__amount {
font-weight: 600;
color: $primary-color;
font-size: 15px;
}
&__accountTag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
background: #f0f5ff;
color: #2f54eb;
&--system {
background: #fff7e6;
color: #fa8c16;
}
&--user {
background: #e6f7ff;
color: #1677ff;
}
}
&__statusTag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
&__accountSequence {
font-weight: 600;
color: $primary-color;
font-size: 13px;
}
&__accountName {
font-size: 12px;
color: $text-secondary;
}
&__txHash {
font-family: monospace;
font-size: 11px;
color: $text-secondary;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
}
// 操作按钮
&__actions {
display: flex;
gap: 8px;
}
&__actionBtn {
padding: 4px 10px;
font-size: 13px;
color: $text-secondary;
background: transparent;
border: 1px solid $border-color;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
&:hover {
color: $primary-color;
border-color: $primary-color;
}
&--primary {
color: $primary-color;
border-color: $primary-color;
&:hover {
background: #e6f4ff;
}
}
}
// 分页
&__pagination {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid $border-color;
font-size: 14px;
color: $text-secondary;
}
&__pageButtons {
display: flex;
gap: 8px;
}
// 新建划转表单
&__form {
display: flex;
flex-direction: column;
gap: 16px;
}
&__formGroup {
display: flex;
flex-direction: column;
gap: 6px;
label {
font-size: 14px;
font-weight: 500;
color: $text-primary;
}
input,
select,
textarea {
padding: 10px 12px;
border: 1px solid $border-color;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.2s;
&:focus {
outline: none;
border-color: $primary-color;
}
&::placeholder {
color: $text-disabled;
}
}
textarea {
resize: vertical;
min-height: 80px;
}
}
&__formHint {
font-size: 12px;
color: $text-secondary;
margin-top: 4px;
}
&__formError {
font-size: 12px;
color: $error-color;
margin-top: 4px;
}
&__modalFooter {
display: flex;
justify-content: flex-end;
gap: 12px;
}
// 详情样式
&__detail {
display: flex;
flex-direction: column;
gap: 12px;
}
&__detailSection {
padding: 16px;
background: #fafafa;
border-radius: 8px;
}
&__detailTitle {
font-size: 14px;
font-weight: 600;
color: $text-primary;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid $border-color;
}
&__detailRow {
display: flex;
gap: 12px;
align-items: flex-start;
margin-bottom: 8px;
&:last-child {
margin-bottom: 0;
}
}
&__detailLabel {
flex-shrink: 0;
width: 80px;
font-size: 13px;
color: $text-secondary;
}
&__detailValue {
flex: 1;
font-size: 13px;
color: $text-primary;
}
// 账户卡片列表
&__accountCards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
&__accountCard {
padding: 16px;
background: #fafafa;
border: 1px solid $border-color;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: $primary-color;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
&--selected {
border-color: $primary-color;
background: #e6f4ff;
}
}
&__accountCardName {
font-size: 15px;
font-weight: 600;
color: $text-primary;
margin-bottom: 8px;
}
&__accountCardSequence {
font-size: 13px;
color: $text-secondary;
font-family: monospace;
}
&__accountCardBalance {
font-size: 14px;
color: $primary-color;
font-weight: 500;
margin-top: 8px;
}
// 警告信息
&__warning {
padding: 12px 16px;
background: #fffbe6;
border: 1px solid #ffe58f;
border-radius: 8px;
color: #ad6800;
font-size: 13px;
line-height: 1.5;
margin-bottom: 16px;
}
}

View File

@ -31,6 +31,7 @@ const topMenuItems: MenuItem[] = [
{ key: 'notifications', icon: '/images/Container3.svg', label: '通知管理', path: '/notifications' },
{ key: 'pending-actions', icon: '/images/Container3.svg', label: '待办操作', path: '/pending-actions' },
{ key: 'withdrawals', icon: '/images/Container5.svg', label: '提现审核', path: '/withdrawals' },
{ key: 'system-transfer', icon: '/images/Container5.svg', label: '系统划转', path: '/system-transfer' },
{ key: 'statistics', icon: '/images/Container5.svg', label: '数据统计', path: '/statistics' },
{ key: 'maintenance', icon: '/images/Container6.svg', label: '系统维护', path: '/maintenance' },
{ key: 'settings', icon: '/images/Container6.svg', label: '系统设置', path: '/settings' },

View File

@ -3,3 +3,4 @@
export * from './useDashboard';
export * from './useUsers';
export * from './useAuthorizations';
export * from './useSystemWithdrawal';

View File

@ -0,0 +1,71 @@
/**
* Hooks
* [2026-01-06]
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { systemWithdrawalService } from '@/services/systemWithdrawalService';
import type {
SystemWithdrawalQueryParams,
SystemWithdrawalRequest,
} from '@/types/system-withdrawal.types';
/** Query Keys */
export const systemWithdrawalKeys = {
all: ['systemWithdrawals'] as const,
accounts: () => [...systemWithdrawalKeys.all, 'accounts'] as const,
orders: (params: SystemWithdrawalQueryParams) => [...systemWithdrawalKeys.all, 'orders', params] as const,
accountName: (accountSequence: string) => [...systemWithdrawalKeys.all, 'accountName', accountSequence] as const,
};
/**
*
*/
export function useSystemWithdrawalAccounts() {
return useQuery({
queryKey: systemWithdrawalKeys.accounts(),
queryFn: () => systemWithdrawalService.getAccounts(),
staleTime: 5 * 60 * 1000, // 5分钟
gcTime: 10 * 60 * 1000,
});
}
/**
*
*/
export function useSystemWithdrawalOrders(params: SystemWithdrawalQueryParams = {}) {
return useQuery({
queryKey: systemWithdrawalKeys.orders(params),
queryFn: () => systemWithdrawalService.getOrders(params),
staleTime: 30 * 1000, // 30秒
gcTime: 5 * 60 * 1000,
});
}
/**
*
*/
export function useAccountName(accountSequence: string) {
return useQuery({
queryKey: systemWithdrawalKeys.accountName(accountSequence),
queryFn: () => systemWithdrawalService.getAccountName(accountSequence),
enabled: !!accountSequence && accountSequence.length > 0,
staleTime: 10 * 60 * 1000, // 10分钟
gcTime: 30 * 60 * 1000,
});
}
/**
*
*/
export function useRequestSystemWithdrawal() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: SystemWithdrawalRequest) => systemWithdrawalService.request(data),
onSuccess: () => {
// 成功后刷新账户列表和订单列表
queryClient.invalidateQueries({ queryKey: systemWithdrawalKeys.all });
},
});
}

View File

@ -207,6 +207,18 @@ export const API_ENDPOINTS = {
COMPLETE_PAYMENT: (orderNo: string) => `/v1/wallets/fiat-withdrawals/${orderNo}/complete-payment`,
},
// [2026-01-06] 新增:系统账户划转 (wallet-service)
SYSTEM_WITHDRAWAL: {
// 发起划转请求
REQUEST: '/v1/wallets/system-withdrawal/request',
// 获取可划转账户列表
ACCOUNTS: '/v1/wallets/system-withdrawal/accounts',
// 查询划转订单
ORDERS: '/v1/wallets/system-withdrawal/orders',
// 获取账户名称
ACCOUNT_NAME: (accountSequence: string) => `/v1/wallets/system-withdrawal/account-name/${accountSequence}`,
},
// [2026-01-04] 新增:系统账户报表 (reporting-service)
// 回滚方式:删除此部分即可
SYSTEM_ACCOUNT_REPORTS: {

View File

@ -0,0 +1,60 @@
/**
*
* [2026-01-06]
*/
import apiClient from '@/infrastructure/api/client';
import { API_ENDPOINTS } from '@/infrastructure/api/endpoints';
import type {
SystemAccount,
SystemWithdrawalRequest,
SystemWithdrawalResponse,
SystemWithdrawalQueryParams,
SystemWithdrawalOrderListResponse,
} from '@/types/system-withdrawal.types';
/**
*
*/
export const systemWithdrawalService = {
/**
*
*/
async getAccounts(): Promise<SystemAccount[]> {
const response = await apiClient.get(API_ENDPOINTS.SYSTEM_WITHDRAWAL.ACCOUNTS);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = (response as any)?.data?.data;
return result ?? [];
},
/**
*
*/
async getOrders(params: SystemWithdrawalQueryParams = {}): Promise<SystemWithdrawalOrderListResponse> {
const response = await apiClient.get(API_ENDPOINTS.SYSTEM_WITHDRAWAL.ORDERS, { params });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = (response as any)?.data?.data;
return result ?? { items: [], total: 0, page: 1, limit: 20 };
},
/**
*
*/
async getAccountName(accountSequence: string): Promise<{ accountSequence: string; name: string }> {
const response = await apiClient.get(API_ENDPOINTS.SYSTEM_WITHDRAWAL.ACCOUNT_NAME(accountSequence));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = (response as any)?.data?.data;
return result ?? { accountSequence, name: '未知账户' };
},
/**
*
*/
async request(data: SystemWithdrawalRequest): Promise<SystemWithdrawalResponse> {
const response = await apiClient.post(API_ENDPOINTS.SYSTEM_WITHDRAWAL.REQUEST, data);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (response as any)?.data?.data;
},
};
export default systemWithdrawalService;

View File

@ -0,0 +1,84 @@
/**
*
* [2026-01-06]
*/
// 划转订单状态
export type SystemWithdrawalStatus = 'PENDING' | 'FROZEN' | 'BROADCASTED' | 'CONFIRMED' | 'FAILED';
// 系统账户信息
export interface SystemAccount {
accountSequence: string;
name: string;
balance?: string;
}
// 划转订单
export interface SystemWithdrawalOrder {
orderId: number;
orderNo: string;
fromAccountSequence: string;
fromAccountName: string;
toAccountSequence: string;
toUserId: number;
toUserName?: string;
toAddress: string;
amount: string;
chainType: string;
txHash?: string;
status: SystemWithdrawalStatus;
errorMessage?: string;
operatorId: string;
operatorName?: string;
memo?: string;
frozenAt?: string;
broadcastedAt?: string;
confirmedAt?: string;
createdAt: string;
}
// 划转请求
export interface SystemWithdrawalRequest {
fromAccountSequence: string;
toAccountSequence: string;
amount: string;
memo?: string;
}
// 划转响应
export interface SystemWithdrawalResponse {
orderNo: string;
status: SystemWithdrawalStatus;
message: string;
}
// 订单查询参数
export interface SystemWithdrawalQueryParams {
fromAccountSequence?: string;
toAccountSequence?: string;
status?: SystemWithdrawalStatus;
page?: number;
limit?: number;
}
// 订单列表响应
export interface SystemWithdrawalOrderListResponse {
items: SystemWithdrawalOrder[];
total: number;
page: number;
limit: number;
}
// 状态配置
export const SYSTEM_WITHDRAWAL_STATUS_CONFIG: Record<SystemWithdrawalStatus, { label: string; color: string }> = {
PENDING: { label: '待处理', color: '#faad14' },
FROZEN: { label: '已冻结', color: '#1890ff' },
BROADCASTED: { label: '已广播', color: '#722ed1' },
CONFIRMED: { label: '已确认', color: '#52c41a' },
FAILED: { label: '失败', color: '#ff4d4f' },
};
// 获取状态信息
export function getSystemWithdrawalStatusInfo(status: SystemWithdrawalStatus) {
return SYSTEM_WITHDRAWAL_STATUS_CONFIG[status] || { label: status, color: '#999' };
}