810 lines
24 KiB
Markdown
810 lines
24 KiB
Markdown
# 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
|
||
```
|