feat(pre-planting): Admin Web 预种管理页面 + Sidebar 入口
[2026-02-17] Admin Web 预种计划管理页面完整实现 新增文件: - (dashboard)/pre-planting/page.tsx: 预种管理页面 - 预种开关控制卡片(开启/关闭 + 状态徽章) - 四格统计卡片(总订单、总份数、总金额、合成树数) - Tab 切换:预种订单 / 用户持仓 / 合并记录 - 订单表格:订单号、用户、份数、金额、状态标签、时间 - 持仓表格:用户、累计份数、待合并进度、合成树数、省市 - 合并表格:合并号、用户、树数、来源订单、合同状态、挖矿状态 - 搜索过滤、刷新、加载/错误/空状态处理 - pre-planting.module.scss: 页面样式 - 开关状态卡片(渐变背景,开/关不同主题色) - 统计网格(4列响应式) - Tab、表格、状态标签样式 修改文件: - Sidebar.tsx: 新增"预种管理"菜单项(数据统计与系统维护之间) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
63ae7662a4
commit
765a4f41d3
|
|
@ -0,0 +1,475 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [2026-02-17] 预种计划管理页面
|
||||||
|
*
|
||||||
|
* 管理员端的预种计划管理页面,提供:
|
||||||
|
* - 预种开关控制(开启/关闭预种购买功能)
|
||||||
|
* - 统计汇总(总订单、总份数、总金额、合成树数、参与用户数)
|
||||||
|
* - 预种订单列表(按用户/状态过滤)
|
||||||
|
* - 预种持仓列表(查看用户持仓概况)
|
||||||
|
* - 合并记录列表(查看合并和签约状态)
|
||||||
|
*
|
||||||
|
* === 技术栈 ===
|
||||||
|
* - React Query (usePrePlanting* hooks) 数据获取
|
||||||
|
* - SCSS Modules 样式
|
||||||
|
* - PageContainer 布局包裹
|
||||||
|
*
|
||||||
|
* === 与现有页面的关系 ===
|
||||||
|
* 完全独立页面,不修改任何现有管理页面。
|
||||||
|
* 侧边栏入口在"数据统计"和"系统维护"之间。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Button } from '@/components/common';
|
||||||
|
import { PageContainer } from '@/components/layout';
|
||||||
|
import { cn } from '@/utils/helpers';
|
||||||
|
import { formatDateTime } from '@/utils/formatters';
|
||||||
|
import {
|
||||||
|
usePrePlantingConfig,
|
||||||
|
usePrePlantingStats,
|
||||||
|
usePrePlantingOrders,
|
||||||
|
usePrePlantingPositions,
|
||||||
|
usePrePlantingMerges,
|
||||||
|
useTogglePrePlantingConfig,
|
||||||
|
} from '@/hooks';
|
||||||
|
import type {
|
||||||
|
PrePlantingAdminOrder,
|
||||||
|
PrePlantingAdminPosition,
|
||||||
|
PrePlantingAdminMerge,
|
||||||
|
} from '@/services/prePlantingService';
|
||||||
|
import styles from './pre-planting.module.scss';
|
||||||
|
|
||||||
|
// Tab 类型定义
|
||||||
|
type TabKey = 'orders' | 'positions' | 'merges';
|
||||||
|
|
||||||
|
// 订单状态显示映射
|
||||||
|
const ORDER_STATUS_MAP: Record<string, { label: string; style: string }> = {
|
||||||
|
CREATED: { label: '待支付', style: 'created' },
|
||||||
|
PAID: { label: '已支付', style: 'paid' },
|
||||||
|
MERGED: { label: '已合并', style: 'merged' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// 合同状态显示映射
|
||||||
|
const CONTRACT_STATUS_MAP: Record<string, { label: string; style: string }> = {
|
||||||
|
PENDING: { label: '待签约', style: 'pending' },
|
||||||
|
SIGNED: { label: '已签约', style: 'signed' },
|
||||||
|
EXPIRED: { label: '已过期', style: 'expired' },
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预种计划管理页面
|
||||||
|
*/
|
||||||
|
export default function PrePlantingPage() {
|
||||||
|
// === Tab 与分页状态 ===
|
||||||
|
const [activeTab, setActiveTab] = useState<TabKey>('orders');
|
||||||
|
const [keyword, setKeyword] = useState('');
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const pageSize = 20;
|
||||||
|
|
||||||
|
// === React Query Hooks ===
|
||||||
|
const { data: config, isLoading: configLoading } = usePrePlantingConfig();
|
||||||
|
const { data: stats, isLoading: statsLoading } = usePrePlantingStats();
|
||||||
|
const toggleConfig = useTogglePrePlantingConfig();
|
||||||
|
|
||||||
|
const ordersQuery = usePrePlantingOrders({
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
keyword: activeTab === 'orders' ? keyword : undefined,
|
||||||
|
});
|
||||||
|
const positionsQuery = usePrePlantingPositions({
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
keyword: activeTab === 'positions' ? keyword : undefined,
|
||||||
|
});
|
||||||
|
const mergesQuery = usePrePlantingMerges({
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
keyword: activeTab === 'merges' ? keyword : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// === 开关切换 ===
|
||||||
|
const handleToggle = () => {
|
||||||
|
if (!config || toggleConfig.isPending) return;
|
||||||
|
toggleConfig.mutate(!config.isActive);
|
||||||
|
};
|
||||||
|
|
||||||
|
// === Tab 切换时重置分页 ===
|
||||||
|
const handleTabChange = (tab: TabKey) => {
|
||||||
|
setActiveTab(tab);
|
||||||
|
setPage(1);
|
||||||
|
setKeyword('');
|
||||||
|
};
|
||||||
|
|
||||||
|
// === 格式化数字(千分位) ===
|
||||||
|
const formatNumber = (n: number) =>
|
||||||
|
n.toLocaleString('en-US');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer title="预种管理">
|
||||||
|
<div className={styles.prePlanting}>
|
||||||
|
{/* 页面标题 */}
|
||||||
|
<div className={styles.prePlanting__header}>
|
||||||
|
<h1 className={styles.prePlanting__title}>预种计划管理</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 预种开关卡片 */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
styles.prePlanting__switchCard,
|
||||||
|
config?.isActive && styles['prePlanting__switchCard--active']
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles.prePlanting__switchInfo}>
|
||||||
|
<span className={styles.prePlanting__switchIcon}>
|
||||||
|
{config?.isActive ? '🌱' : '⏸️'}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<div className={styles.prePlanting__switchTitle}>
|
||||||
|
预种功能状态
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
styles.prePlanting__switchBadge,
|
||||||
|
config?.isActive
|
||||||
|
? styles['prePlanting__switchBadge--on']
|
||||||
|
: styles['prePlanting__switchBadge--off']
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{configLoading ? '加载中' : config?.isActive ? '已开启' : '已关闭'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.prePlanting__switchDesc}>
|
||||||
|
{config?.isActive
|
||||||
|
? '用户可正常购买预种份额(3171 USDT/份)'
|
||||||
|
: '新用户不可购买;已有未凑满份额的用户可继续购买至 5 的倍数'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
styles.prePlanting__switchBtn,
|
||||||
|
config?.isActive
|
||||||
|
? styles['prePlanting__switchBtn--on']
|
||||||
|
: styles['prePlanting__switchBtn--off']
|
||||||
|
)}
|
||||||
|
onClick={handleToggle}
|
||||||
|
disabled={configLoading || toggleConfig.isPending}
|
||||||
|
>
|
||||||
|
{toggleConfig.isPending
|
||||||
|
? '切换中...'
|
||||||
|
: config?.isActive
|
||||||
|
? '关闭预种'
|
||||||
|
: '开启预种'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 统计卡片 */}
|
||||||
|
<div className={styles.prePlanting__statsGrid}>
|
||||||
|
<div className={styles.prePlanting__statCard}>
|
||||||
|
<div className={styles.prePlanting__statValue}>
|
||||||
|
{statsLoading ? '-' : formatNumber(stats?.totalOrders ?? 0)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.prePlanting__statLabel}>总订单数</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.prePlanting__statCard}>
|
||||||
|
<div className={styles.prePlanting__statValue}>
|
||||||
|
{statsLoading ? '-' : formatNumber(stats?.totalPortions ?? 0)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.prePlanting__statLabel}>总份数</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.prePlanting__statCard}>
|
||||||
|
<div className={styles.prePlanting__statValue}>
|
||||||
|
{statsLoading ? '-' : formatNumber(stats?.totalAmount ?? 0)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.prePlanting__statLabel}>总金额 (USDT)</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.prePlanting__statCard}>
|
||||||
|
<div className={styles.prePlanting__statValue}>
|
||||||
|
{statsLoading ? '-' : formatNumber(stats?.totalTreesMerged ?? 0)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.prePlanting__statLabel}>已合成树数</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab 切换 */}
|
||||||
|
<div className={styles.prePlanting__tabs}>
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
styles.prePlanting__tab,
|
||||||
|
activeTab === 'orders' && styles['prePlanting__tab--active']
|
||||||
|
)}
|
||||||
|
onClick={() => handleTabChange('orders')}
|
||||||
|
>
|
||||||
|
预种订单
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
styles.prePlanting__tab,
|
||||||
|
activeTab === 'positions' && styles['prePlanting__tab--active']
|
||||||
|
)}
|
||||||
|
onClick={() => handleTabChange('positions')}
|
||||||
|
>
|
||||||
|
用户持仓
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
styles.prePlanting__tab,
|
||||||
|
activeTab === 'merges' && styles['prePlanting__tab--active']
|
||||||
|
)}
|
||||||
|
onClick={() => handleTabChange('merges')}
|
||||||
|
>
|
||||||
|
合并记录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 数据内容区 */}
|
||||||
|
<div className={styles.prePlanting__card}>
|
||||||
|
{/* 搜索栏 */}
|
||||||
|
<div className={styles.prePlanting__toolbar}>
|
||||||
|
<div className={styles.prePlanting__search}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={
|
||||||
|
activeTab === 'orders'
|
||||||
|
? '搜索订单号或用户账号...'
|
||||||
|
: activeTab === 'positions'
|
||||||
|
? '搜索用户账号...'
|
||||||
|
: '搜索合并号或用户账号...'
|
||||||
|
}
|
||||||
|
value={keyword}
|
||||||
|
onChange={(e) => {
|
||||||
|
setKeyword(e.target.value);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (activeTab === 'orders') ordersQuery.refetch();
|
||||||
|
else if (activeTab === 'positions') positionsQuery.refetch();
|
||||||
|
else mergesQuery.refetch();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab 对应的表格 */}
|
||||||
|
{activeTab === 'orders' && (
|
||||||
|
<OrdersTable
|
||||||
|
data={ordersQuery.data?.items ?? []}
|
||||||
|
loading={ordersQuery.isLoading}
|
||||||
|
error={ordersQuery.error}
|
||||||
|
onRetry={() => ordersQuery.refetch()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeTab === 'positions' && (
|
||||||
|
<PositionsTable
|
||||||
|
data={positionsQuery.data?.items ?? []}
|
||||||
|
loading={positionsQuery.isLoading}
|
||||||
|
error={positionsQuery.error}
|
||||||
|
onRetry={() => positionsQuery.refetch()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeTab === 'merges' && (
|
||||||
|
<MergesTable
|
||||||
|
data={mergesQuery.data?.items ?? []}
|
||||||
|
loading={mergesQuery.isLoading}
|
||||||
|
error={mergesQuery.error}
|
||||||
|
onRetry={() => mergesQuery.refetch()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 子组件:订单表格
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
function OrdersTable({
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
onRetry,
|
||||||
|
}: {
|
||||||
|
data: PrePlantingAdminOrder[];
|
||||||
|
loading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
onRetry: () => void;
|
||||||
|
}) {
|
||||||
|
if (loading) return <div className={styles.prePlanting__loading}>加载中...</div>;
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className={styles.prePlanting__error}>
|
||||||
|
<span>{error.message || '加载失败'}</span>
|
||||||
|
<Button variant="outline" size="sm" onClick={onRetry}>重试</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data.length === 0) return <div className={styles.prePlanting__empty}>暂无预种订单</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className={styles.prePlanting__table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>订单号</th>
|
||||||
|
<th>用户账号</th>
|
||||||
|
<th>份数</th>
|
||||||
|
<th>金额 (USDT)</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>支付时间</th>
|
||||||
|
<th>创建时间</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.map((order) => {
|
||||||
|
const status = ORDER_STATUS_MAP[order.status] ?? { label: order.status, style: '' };
|
||||||
|
return (
|
||||||
|
<tr key={order.id}>
|
||||||
|
<td>{order.orderNo}</td>
|
||||||
|
<td>{order.accountSequence}</td>
|
||||||
|
<td>{order.portionCount}</td>
|
||||||
|
<td>{order.totalAmount.toLocaleString()}</td>
|
||||||
|
<td>
|
||||||
|
<span className={cn(styles.prePlanting__status, styles[`prePlanting__status--${status.style}`])}>
|
||||||
|
{status.label}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{order.paidAt ? formatDateTime(order.paidAt) : '-'}</td>
|
||||||
|
<td>{formatDateTime(order.createdAt)}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 子组件:持仓表格
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
function PositionsTable({
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
onRetry,
|
||||||
|
}: {
|
||||||
|
data: PrePlantingAdminPosition[];
|
||||||
|
loading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
onRetry: () => void;
|
||||||
|
}) {
|
||||||
|
if (loading) return <div className={styles.prePlanting__loading}>加载中...</div>;
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className={styles.prePlanting__error}>
|
||||||
|
<span>{error.message || '加载失败'}</span>
|
||||||
|
<Button variant="outline" size="sm" onClick={onRetry}>重试</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data.length === 0) return <div className={styles.prePlanting__empty}>暂无持仓记录</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className={styles.prePlanting__table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>用户账号</th>
|
||||||
|
<th>累计份数</th>
|
||||||
|
<th>待合并</th>
|
||||||
|
<th>已合并</th>
|
||||||
|
<th>合成树数</th>
|
||||||
|
<th>省市</th>
|
||||||
|
<th>首次购买</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.map((pos) => (
|
||||||
|
<tr key={pos.id}>
|
||||||
|
<td>{pos.accountSequence}</td>
|
||||||
|
<td>{pos.totalPortions}</td>
|
||||||
|
<td>{pos.availablePortions} / 5</td>
|
||||||
|
<td>{pos.mergedPortions}</td>
|
||||||
|
<td>{pos.totalTreesMerged}</td>
|
||||||
|
<td>{pos.provinceCode && pos.cityCode ? `${pos.provinceCode} · ${pos.cityCode}` : '-'}</td>
|
||||||
|
<td>{pos.firstPurchaseAt ? formatDateTime(pos.firstPurchaseAt) : '-'}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// 子组件:合并记录表格
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
function MergesTable({
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
onRetry,
|
||||||
|
}: {
|
||||||
|
data: PrePlantingAdminMerge[];
|
||||||
|
loading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
onRetry: () => void;
|
||||||
|
}) {
|
||||||
|
if (loading) return <div className={styles.prePlanting__loading}>加载中...</div>;
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className={styles.prePlanting__error}>
|
||||||
|
<span>{error.message || '加载失败'}</span>
|
||||||
|
<Button variant="outline" size="sm" onClick={onRetry}>重试</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data.length === 0) return <div className={styles.prePlanting__empty}>暂无合并记录</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className={styles.prePlanting__table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>合并号</th>
|
||||||
|
<th>用户账号</th>
|
||||||
|
<th>树数</th>
|
||||||
|
<th>来源订单</th>
|
||||||
|
<th>合同状态</th>
|
||||||
|
<th>签约时间</th>
|
||||||
|
<th>挖矿开启</th>
|
||||||
|
<th>合并时间</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.map((merge) => {
|
||||||
|
const cStatus = CONTRACT_STATUS_MAP[merge.contractStatus] ?? {
|
||||||
|
label: merge.contractStatus,
|
||||||
|
style: '',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<tr key={merge.id}>
|
||||||
|
<td>{merge.mergeNo}</td>
|
||||||
|
<td>{merge.accountSequence}</td>
|
||||||
|
<td>{merge.treeCount}</td>
|
||||||
|
<td>{merge.sourceOrderNos.length} 笔</td>
|
||||||
|
<td>
|
||||||
|
<span className={cn(styles.prePlanting__status, styles[`prePlanting__status--${cStatus.style}`])}>
|
||||||
|
{cStatus.label}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{merge.contractSignedAt ? formatDateTime(merge.contractSignedAt) : '-'}</td>
|
||||||
|
<td>{merge.miningEnabledAt ? formatDateTime(merge.miningEnabledAt) : '-'}</td>
|
||||||
|
<td>{formatDateTime(merge.mergedAt)}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,310 @@
|
||||||
|
/**
|
||||||
|
* [2026-02-17] 预种计划管理页面样式
|
||||||
|
*
|
||||||
|
* 包含:开关状态卡片、统计卡片、Tab 切换、数据表格
|
||||||
|
* 风格与全局管理后台一致
|
||||||
|
*/
|
||||||
|
|
||||||
|
@use '@/styles/variables' as *;
|
||||||
|
|
||||||
|
.prePlanting {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
// 页面标题
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开关状态卡片
|
||||||
|
&__switchCard {
|
||||||
|
background: linear-gradient(135deg, #fff9e6 0%, #fff3cc 100%);
|
||||||
|
border: 1px solid #ffd666;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
background: linear-gradient(135deg, #f6ffed 0%, #d9f7be 100%);
|
||||||
|
border-color: #95de64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__switchInfo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__switchIcon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__switchTitle {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__switchDesc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: $text-secondary;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__switchBadge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-left: 8px;
|
||||||
|
|
||||||
|
&--on {
|
||||||
|
background: #52c41a;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--off {
|
||||||
|
background: #faad14;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__switchBtn {
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&--on {
|
||||||
|
background: #ff4d4f;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #ff7875;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--off {
|
||||||
|
background: #52c41a;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #73d13d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计卡片网格
|
||||||
|
&__statsGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__statCard {
|
||||||
|
background: $card-background;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: $shadow-base;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__statValue {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__statLabel {
|
||||||
|
font-size: 13px;
|
||||||
|
color: $text-secondary;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab 切换
|
||||||
|
&__tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
border-bottom: 1px solid $border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tab {
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $text-secondary;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
color: #d4af37;
|
||||||
|
border-bottom-color: #d4af37;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据卡片容器
|
||||||
|
&__card {
|
||||||
|
background: $card-background;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: $shadow-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__search {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 280px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: #d4af37;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格
|
||||||
|
&__table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid $border-color;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-secondary;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
color: $text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover td {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态标签
|
||||||
|
&__status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&--created {
|
||||||
|
background: #fff7e6;
|
||||||
|
color: #d48806;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--paid {
|
||||||
|
background: #f6ffed;
|
||||||
|
color: #389e0d;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--merged {
|
||||||
|
background: #fff9e6;
|
||||||
|
color: #d4af37;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--pending {
|
||||||
|
background: #fff7e6;
|
||||||
|
color: #d48806;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--signed {
|
||||||
|
background: #f6ffed;
|
||||||
|
color: #389e0d;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--expired {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #8c8c8c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载 / 空 / 错误 状态
|
||||||
|
&__loading,
|
||||||
|
&__empty,
|
||||||
|
&__error {
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px 24px;
|
||||||
|
color: $text-secondary;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__error {
|
||||||
|
color: #ff4d4f;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
&__pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -35,6 +35,8 @@ const topMenuItems: MenuItem[] = [
|
||||||
{ key: 'withdrawals', icon: '/images/Container5.svg', label: '提现审核', path: '/withdrawals' },
|
{ key: 'withdrawals', icon: '/images/Container5.svg', label: '提现审核', path: '/withdrawals' },
|
||||||
{ key: 'system-transfer', icon: '/images/Container5.svg', label: '系统划转', path: '/system-transfer' },
|
{ key: 'system-transfer', icon: '/images/Container5.svg', label: '系统划转', path: '/system-transfer' },
|
||||||
{ key: 'statistics', icon: '/images/Container5.svg', label: '数据统计', path: '/statistics' },
|
{ key: 'statistics', icon: '/images/Container5.svg', label: '数据统计', path: '/statistics' },
|
||||||
|
// [2026-02-17] 新增:预种计划管理(3171 USDT/份预种开关 + 订单/持仓/合并查询)
|
||||||
|
{ key: 'pre-planting', icon: '/images/Container3.svg', label: '预种管理', path: '/pre-planting' },
|
||||||
{ key: 'maintenance', icon: '/images/Container6.svg', label: '系统维护', path: '/maintenance' },
|
{ key: 'maintenance', icon: '/images/Container6.svg', label: '系统维护', path: '/maintenance' },
|
||||||
{ key: 'settings', icon: '/images/Container6.svg', label: '系统设置', path: '/settings' },
|
{ key: 'settings', icon: '/images/Container6.svg', label: '系统设置', path: '/settings' },
|
||||||
];
|
];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue