24 KiB
24 KiB
Mining Admin Web (挖矿管理后台) 开发指导
1. 项目概述
1.1 核心职责
Mining Admin Web 是挖矿系统的管理后台,供运营人员使用,用于配置管理、监控、数据查询等功能。
主要功能:
- 仪表盘(实时数据监控)
- 用户管理(算力/积分股查询)
- 配置管理(系统参数配置)
- 系统账户管理
- 报表与统计
- 审计日志查看
- 初始化任务管理
1.2 技术栈
框架: Next.js 14 (App Router)
语言: TypeScript
状态管理: Zustand + Redux Toolkit (混合模式)
UI 组件: Shadcn/ui + Tailwind CSS
图表: ECharts / Recharts
表格: TanStack Table
表单: React Hook Form + Zod
请求: TanStack Query + Axios
1.3 架构模式
Clean Architecture + Feature-Sliced Design
2. 目录结构
mining-admin-web/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── (auth)/ # 认证相关页面组
│ │ │ ├── login/
│ │ │ │ └── page.tsx
│ │ │ └── layout.tsx
│ │ ├── (dashboard)/ # 主应用页面组
│ │ │ ├── dashboard/
│ │ │ │ └── page.tsx # 仪表盘首页
│ │ │ ├── users/
│ │ │ │ ├── page.tsx # 用户列表
│ │ │ │ └── [accountSequence]/
│ │ │ │ └── page.tsx # 用户详情
│ │ │ ├── configs/
│ │ │ │ └── page.tsx # 配置管理
│ │ │ ├── system-accounts/
│ │ │ │ └── page.tsx # 系统账户
│ │ │ ├── reports/
│ │ │ │ └── page.tsx # 报表
│ │ │ ├── audit-logs/
│ │ │ │ └── page.tsx # 审计日志
│ │ │ ├── initialization/
│ │ │ │ └── page.tsx # 初始化任务
│ │ │ └── layout.tsx # 主布局(侧边栏+顶栏)
│ │ ├── api/ # API Routes (BFF)
│ │ │ └── [...path]/
│ │ │ └── route.ts # 代理到后端服务
│ │ ├── layout.tsx # 根布局
│ │ ├── page.tsx # 根页面(重定向)
│ │ └── globals.css
│ │
│ ├── components/ # 共享组件
│ │ ├── ui/ # 基础UI组件 (Shadcn)
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ ├── dialog.tsx
│ │ │ ├── input.tsx
│ │ │ ├── table.tsx
│ │ │ ├── tabs.tsx
│ │ │ ├── toast.tsx
│ │ │ └── ...
│ │ ├── charts/ # 图表组件
│ │ │ ├── price-chart.tsx # 价格K线图
│ │ │ ├── contribution-chart.tsx # 算力趋势图
│ │ │ ├── distribution-chart.tsx # 分配统计图
│ │ │ └── realtime-gauge.tsx # 实时仪表盘
│ │ ├── tables/ # 表格组件
│ │ │ ├── data-table.tsx # 通用数据表格
│ │ │ ├── user-table.tsx
│ │ │ ├── config-table.tsx
│ │ │ └── audit-log-table.tsx
│ │ ├── forms/ # 表单组件
│ │ │ ├── config-form.tsx
│ │ │ └── system-account-form.tsx
│ │ └── layout/ # 布局组件
│ │ ├── sidebar.tsx
│ │ ├── header.tsx
│ │ ├── breadcrumb.tsx
│ │ └── page-header.tsx
│ │
│ ├── features/ # 功能模块 (Feature-Sliced)
│ │ ├── dashboard/
│ │ │ ├── components/
│ │ │ │ ├── stats-cards.tsx
│ │ │ │ ├── realtime-panel.tsx
│ │ │ │ ├── price-overview.tsx
│ │ │ │ └── activity-feed.tsx
│ │ │ ├── hooks/
│ │ │ │ ├── use-dashboard-stats.ts
│ │ │ │ └── use-realtime-data.ts
│ │ │ ├── api/
│ │ │ │ └── dashboard.api.ts
│ │ │ └── store/
│ │ │ └── dashboard.slice.ts # RTK Slice
│ │ │
│ │ ├── users/
│ │ │ ├── components/
│ │ │ │ ├── user-search.tsx
│ │ │ │ ├── user-detail-card.tsx
│ │ │ │ ├── contribution-breakdown.tsx
│ │ │ │ ├── mining-records-list.tsx
│ │ │ │ └── trade-orders-list.tsx
│ │ │ ├── hooks/
│ │ │ │ ├── use-users.ts
│ │ │ │ └── use-user-detail.ts
│ │ │ ├── api/
│ │ │ │ └── users.api.ts
│ │ │ └── store/
│ │ │ └── users.slice.ts
│ │ │
│ │ ├── configs/
│ │ │ ├── components/
│ │ │ │ ├── config-list.tsx
│ │ │ │ ├── config-edit-dialog.tsx
│ │ │ │ └── transfer-switch.tsx
│ │ │ ├── hooks/
│ │ │ │ └── use-configs.ts
│ │ │ ├── api/
│ │ │ │ └── configs.api.ts
│ │ │ └── store/
│ │ │ └── configs.slice.ts
│ │ │
│ │ ├── system-accounts/
│ │ │ ├── components/
│ │ │ ├── hooks/
│ │ │ ├── api/
│ │ │ └── store/
│ │ │
│ │ ├── reports/
│ │ │ ├── components/
│ │ │ │ ├── report-filters.tsx
│ │ │ │ ├── contribution-report.tsx
│ │ │ │ ├── mining-report.tsx
│ │ │ │ └── trading-report.tsx
│ │ │ ├── hooks/
│ │ │ ├── api/
│ │ │ └── store/
│ │ │
│ │ └── audit-logs/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── api/
│ │ └── store/
│ │
│ ├── lib/ # 工具库
│ │ ├── api/
│ │ │ ├── client.ts # Axios 实例
│ │ │ ├── interceptors.ts # 请求拦截器
│ │ │ └── types.ts # API 类型
│ │ ├── utils/
│ │ │ ├── format.ts # 格式化工具
│ │ │ ├── decimal.ts # 高精度计算
│ │ │ └── date.ts # 日期处理
│ │ ├── hooks/
│ │ │ ├── use-auth.ts
│ │ │ ├── use-toast.ts
│ │ │ └── use-confirmation.ts
│ │ └── constants/
│ │ ├── routes.ts
│ │ └── config.ts
│ │
│ ├── store/ # 全局状态管理
│ │ ├── index.ts # Store 配置
│ │ ├── slices/
│ │ │ ├── auth.slice.ts # RTK: 认证状态
│ │ │ ├── ui.slice.ts # RTK: UI状态
│ │ │ └── realtime.slice.ts # RTK: 实时数据
│ │ ├── middleware/
│ │ │ └── logger.ts
│ │ └── zustand/
│ │ ├── use-sidebar.ts # Zustand: 侧边栏状态
│ │ └── use-theme.ts # Zustand: 主题状态
│ │
│ └── types/ # 全局类型定义
│ ├── api.ts
│ ├── user.ts
│ ├── config.ts
│ ├── dashboard.ts
│ └── common.ts
│
├── public/
│ └── images/
│
├── .env.local # 环境变量
├── .env.production
├── next.config.js
├── tailwind.config.js
├── tsconfig.json
├── package.json
└── README.md
3. 状态管理策略
3.1 混合模式:Zustand + Redux Toolkit
┌─────────────────────────────────────────────────────────────┐
│ 状态管理分层策略 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Redux Toolkit (RTK) │
│ ├── 复杂业务状态(用户列表、配置、报表等) │
│ ├── 需要 DevTools 调试的状态 │
│ ├── 需要中间件处理的状态 │
│ └── 跨页面共享的业务数据 │
│ │
│ Zustand │
│ ├── 简单UI状态(侧边栏、模态框、主题) │
│ ├── 临时状态(表单草稿、筛选条件) │
│ └── 组件级局部状态 │
│ │
│ TanStack Query │
│ ├── 服务端数据缓存 │
│ ├── 自动重新获取 │
│ └── 乐观更新 │
│ │
└─────────────────────────────────────────────────────────────┘
3.2 RTK Slice 示例
// store/slices/configs.slice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { configsApi } from '@/features/configs/api/configs.api';
import type { SystemConfig } from '@/types/config';
interface ConfigsState {
items: SystemConfig[];
loading: boolean;
error: string | null;
selectedKey: string | null;
}
const initialState: ConfigsState = {
items: [],
loading: false,
error: null,
selectedKey: null,
};
// Async Thunks
export const fetchConfigs = createAsyncThunk(
'configs/fetchConfigs',
async (_, { rejectWithValue }) => {
try {
const response = await configsApi.getAll();
return response.data;
} catch (error: any) {
return rejectWithValue(error.message);
}
}
);
export const updateConfig = createAsyncThunk(
'configs/updateConfig',
async ({ key, value }: { key: string; value: string }, { rejectWithValue }) => {
try {
const response = await configsApi.update(key, value);
return response.data;
} catch (error: any) {
return rejectWithValue(error.message);
}
}
);
// Slice
const configsSlice = createSlice({
name: 'configs',
initialState,
reducers: {
setSelectedKey: (state, action: PayloadAction<string | null>) => {
state.selectedKey = action.payload;
},
clearError: (state) => {
state.error = null;
},
},
extraReducers: (builder) => {
builder
.addCase(fetchConfigs.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchConfigs.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
})
.addCase(fetchConfigs.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
})
.addCase(updateConfig.fulfilled, (state, action) => {
const index = state.items.findIndex(
(item) => item.configKey === action.payload.configKey
);
if (index !== -1) {
state.items[index] = action.payload;
}
});
},
});
export const { setSelectedKey, clearError } = configsSlice.actions;
export default configsSlice.reducer;
3.3 Zustand Store 示例
// store/zustand/use-sidebar.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface SidebarState {
isCollapsed: boolean;
toggle: () => void;
setCollapsed: (collapsed: boolean) => void;
}
export const useSidebar = create<SidebarState>()(
persist(
(set) => ({
isCollapsed: false,
toggle: () => set((state) => ({ isCollapsed: !state.isCollapsed })),
setCollapsed: (collapsed) => set({ isCollapsed: collapsed }),
}),
{
name: 'sidebar-state',
}
)
);
3.4 TanStack Query 示例
// features/dashboard/hooks/use-dashboard-stats.ts
import { useQuery } from '@tanstack/react-query';
import { dashboardApi } from '../api/dashboard.api';
export function useDashboardStats() {
return useQuery({
queryKey: ['dashboard', 'stats'],
queryFn: () => dashboardApi.getStats(),
refetchInterval: 30000, // 30秒刷新
staleTime: 10000, // 10秒内不重新获取
});
}
export function useRealtimeData() {
return useQuery({
queryKey: ['dashboard', 'realtime'],
queryFn: () => dashboardApi.getRealtimeData(),
refetchInterval: 5000, // 5秒刷新
staleTime: 3000,
});
}
4. 页面开发示例
4.1 仪表盘页面
// app/(dashboard)/dashboard/page.tsx
'use client';
import { StatsCards } from '@/features/dashboard/components/stats-cards';
import { RealtimePanel } from '@/features/dashboard/components/realtime-panel';
import { PriceOverview } from '@/features/dashboard/components/price-overview';
import { ActivityFeed } from '@/features/dashboard/components/activity-feed';
import { PageHeader } from '@/components/layout/page-header';
export default function DashboardPage() {
return (
<div className="space-y-6">
<PageHeader
title="仪表盘"
description="挖矿系统实时数据概览"
/>
{/* 统计卡片 */}
<StatsCards />
{/* 主要内容区 */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 价格概览 */}
<div className="lg:col-span-2">
<PriceOverview />
</div>
{/* 实时数据面板 */}
<div>
<RealtimePanel />
</div>
</div>
{/* 活动动态 */}
<ActivityFeed />
</div>
);
}
4.2 统计卡片组件
// features/dashboard/components/stats-cards.tsx
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useDashboardStats } from '../hooks/use-dashboard-stats';
import { formatNumber, formatDecimal } from '@/lib/utils/format';
import { TrendingUp, Users, Coins, Activity } from 'lucide-react';
export function StatsCards() {
const { data: stats, isLoading } = useDashboardStats();
if (isLoading) {
return <StatsCardsSkeleton />;
}
const cards = [
{
title: '当前价格',
value: formatDecimal(stats?.currentPrice, 8),
unit: '绿积分/股',
icon: TrendingUp,
trend: '+12.5%',
trendUp: true,
},
{
title: '全网算力',
value: formatNumber(stats?.networkEffectiveContribution),
icon: Activity,
},
{
title: '已分配积分股',
value: formatNumber(stats?.totalDistributed),
icon: Coins,
},
{
title: '认种用户数',
value: formatNumber(stats?.adoptedUsers),
icon: Users,
},
];
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{cards.map((card) => (
<Card key={card.title}>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{card.title}
</CardTitle>
<card.icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{card.value}</div>
{card.unit && (
<p className="text-xs text-muted-foreground">{card.unit}</p>
)}
{card.trend && (
<p className={`text-xs ${card.trendUp ? 'text-green-500' : 'text-red-500'}`}>
{card.trend} 较昨日
</p>
)}
</CardContent>
</Card>
))}
</div>
);
}
4.3 用户详情页面
// app/(dashboard)/users/[accountSequence]/page.tsx
'use client';
import { useParams } from 'next/navigation';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { PageHeader } from '@/components/layout/page-header';
import { UserDetailCard } from '@/features/users/components/user-detail-card';
import { ContributionBreakdown } from '@/features/users/components/contribution-breakdown';
import { MiningRecordsList } from '@/features/users/components/mining-records-list';
import { TradeOrdersList } from '@/features/users/components/trade-orders-list';
import { useUserDetail } from '@/features/users/hooks/use-user-detail';
export default function UserDetailPage() {
const params = useParams();
const accountSequence = params.accountSequence as string;
const { data: user, isLoading } = useUserDetail(accountSequence);
if (isLoading) {
return <UserDetailSkeleton />;
}
return (
<div className="space-y-6">
<PageHeader
title="用户详情"
description={`账户序列: ${accountSequence}`}
backLink="/users"
/>
{/* 用户基本信息卡片 */}
<UserDetailCard user={user} />
{/* 详细信息标签页 */}
<Tabs defaultValue="contribution">
<TabsList>
<TabsTrigger value="contribution">算力明细</TabsTrigger>
<TabsTrigger value="mining">挖矿记录</TabsTrigger>
<TabsTrigger value="trading">交易记录</TabsTrigger>
</TabsList>
<TabsContent value="contribution">
<ContributionBreakdown accountSequence={accountSequence} />
</TabsContent>
<TabsContent value="mining">
<MiningRecordsList accountSequence={accountSequence} />
</TabsContent>
<TabsContent value="trading">
<TradeOrdersList accountSequence={accountSequence} />
</TabsContent>
</Tabs>
</div>
);
}
5. API 客户端
5.1 Axios 实例配置
// lib/api/client.ts
import axios from 'axios';
import { getSession } from 'next-auth/react';
export const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL || '/api',
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器
apiClient.interceptors.request.use(
async (config) => {
const session = await getSession();
if (session?.accessToken) {
config.headers.Authorization = `Bearer ${session.accessToken}`;
}
return config;
},
(error) => Promise.reject(error)
);
// 响应拦截器
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// 处理未授权
window.location.href = '/login';
}
return Promise.reject(error);
}
);
5.2 功能模块 API
// features/dashboard/api/dashboard.api.ts
import { apiClient } from '@/lib/api/client';
import type { DashboardStats, RealtimeData } from '@/types/dashboard';
export const dashboardApi = {
getStats: async (): Promise<DashboardStats> => {
const response = await apiClient.get('/admin/dashboard');
return response.data;
},
getRealtimeData: async (): Promise<RealtimeData> => {
const response = await apiClient.get('/admin/dashboard/realtime');
return response.data;
},
getChartData: async (type: string, period: string) => {
const response = await apiClient.get('/admin/dashboard/charts', {
params: { type, period },
});
return response.data;
},
};
6. 图表组件
6.1 价格K线图
// components/charts/price-chart.tsx
'use client';
import { useEffect, useRef } from 'react';
import * as echarts from 'echarts';
import type { KlineData } from '@/types/dashboard';
interface PriceChartProps {
data: KlineData[];
period: string;
}
export function PriceChart({ data, period }: PriceChartProps) {
const chartRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!chartRef.current || !data.length) return;
const chart = echarts.init(chartRef.current);
const option: echarts.EChartsOption = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' },
},
xAxis: {
type: 'category',
data: data.map((d) => d.time),
boundaryGap: false,
},
yAxis: {
type: 'value',
scale: true,
splitLine: { show: true },
},
series: [
{
name: '价格',
type: 'candlestick',
data: data.map((d) => [d.open, d.close, d.low, d.high]),
itemStyle: {
color: '#ef4444', // 上涨
color0: '#22c55e', // 下跌
borderColor: '#ef4444',
borderColor0: '#22c55e',
},
},
{
name: '成交量',
type: 'bar',
xAxisIndex: 0,
yAxisIndex: 1,
data: data.map((d) => d.volume),
itemStyle: {
color: (params: any) => {
const item = data[params.dataIndex];
return item.close >= item.open ? '#ef4444' : '#22c55e';
},
},
},
],
grid: [
{ left: '10%', right: '10%', height: '50%' },
{ left: '10%', right: '10%', top: '65%', height: '20%' },
],
dataZoom: [
{ type: 'inside', xAxisIndex: [0, 1], start: 50, end: 100 },
{ show: true, xAxisIndex: [0, 1], type: 'slider', bottom: 10 },
],
};
chart.setOption(option);
const handleResize = () => chart.resize();
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
chart.dispose();
};
}, [data, period]);
return <div ref={chartRef} className="w-full h-[400px]" />;
}
7. 环境变量
# .env.local
NEXT_PUBLIC_API_URL=http://localhost:3023
NEXT_PUBLIC_WS_URL=ws://localhost:3023
# 认证
NEXTAUTH_URL=http://localhost:3100
NEXTAUTH_SECRET=your-secret-key
# 其他
NEXT_PUBLIC_APP_NAME="挖矿管理后台"
8. 关键注意事项
8.1 数据精度
- 使用
Decimal.js或字符串处理大数值 - 显示时根据场景选择合适的小数位数
- 价格显示8位小数,数量显示4位小数
8.2 实时数据
- 使用 TanStack Query 的
refetchInterval实现轮询 - 考虑使用 WebSocket 获取实时推送
- 注意内存泄漏,组件卸载时清理订阅
8.3 性能优化
- 大表格使用虚拟滚动
- 图表数据分页加载
- 使用
React.memo优化重渲染
8.4 错误处理
- 全局错误边界捕获异常
- API 错误统一 toast 提示
- 表单验证使用 Zod
9. 开发检查清单
- 配置 Next.js 项目
- 集成 Shadcn/ui 组件库
- 配置 Redux Toolkit Store
- 配置 Zustand Stores
- 配置 TanStack Query
- 实现登录认证
- 实现仪表盘页面
- 实现用户列表/详情页面
- 实现配置管理页面
- 实现系统账户页面
- 实现报表页面
- 实现审计日志页面
- 实现K线图表
- 响应式布局适配
- 编写测试
10. 启动命令
# 安装依赖
npm install
# 开发环境
npm run dev
# 构建生产版本
npm run build
# 启动生产版本
npm run start
# 代码检查
npm run lint
# 类型检查
npm run type-check