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

24 KiB
Raw Permalink Blame History

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