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: 'system-transfer', icon: '/images/Container5.svg', label: '系统划转', path: '/system-transfer' },
|
||||
{ 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: 'settings', icon: '/images/Container6.svg', label: '系统设置', path: '/settings' },
|
||||
];
|
||||
|
|
|
|||
Loading…
Reference in New Issue