rwadurian/frontend/mining-admin-web/DEVELOPMENT_GUIDE.md

810 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 示例
```typescript
// 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 示例
```typescript
// 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 示例
```typescript
// 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 仪表盘页面
```typescript
// 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 统计卡片组件
```typescript
// 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 用户详情页面
```typescript
// 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 实例配置
```typescript
// 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
```typescript
// 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线图
```typescript
// 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. 环境变量
```bash
# .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. 启动命令
```bash
# 安装依赖
npm install
# 开发环境
npm run dev
# 构建生产版本
npm run build
# 启动生产版本
npm run start
# 代码检查
npm run lint
# 类型检查
npm run type-check
```