parent
5410a0b8ab
commit
c56ccf5fb2
@ -0,0 +1,60 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Database
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
*.test.ts
|
||||
*.spec.ts
|
||||
|
||||
# Misc
|
||||
.cache/
|
||||
temp/
|
||||
tmp/
|
||||
|
||||
# Docker
|
||||
docker-compose*.yml
|
||||
Dockerfile*
|
||||
.dockerignore
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
*.md
|
||||
STARTUP_GUIDE.md
|
||||
|
||||
# Init scripts (will be mounted as volume)
|
||||
init-scripts/
|
||||
@ -0,0 +1,88 @@
|
||||
# Docker 快速参考卡片
|
||||
|
||||
## 🎯 常用命令速查
|
||||
|
||||
### 开发模式(热重载)
|
||||
|
||||
```bash
|
||||
# 启动(代码修改自动同步)
|
||||
./dev-start.sh # Linux/Mac
|
||||
-dev-start.bat # Windows
|
||||
|
||||
# 查看日志
|
||||
docker-compose -f docker-compose.dev.yml logs -f app
|
||||
|
||||
# 重启
|
||||
docker-compose -f docker-compose.dev.yml restart app
|
||||
|
||||
# 停止
|
||||
docker-compose -f docker-compose.dev.yml stop
|
||||
```
|
||||
|
||||
### 生产模式
|
||||
|
||||
```bash
|
||||
# 启动(重建镜像)
|
||||
./docker-start.sh # Linux/Mac
|
||||
docker-start.bat # Windows
|
||||
|
||||
# 或手动
|
||||
docker-compose up --build -d
|
||||
|
||||
# 查看状态
|
||||
docker-compose ps
|
||||
|
||||
# 停止
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### 代码同步
|
||||
|
||||
| 场景 | 命令 | 时间 |
|
||||
|------|------|------|
|
||||
| 修改代码 | 自动同步 | 1秒 |
|
||||
| 修改配置 | `docker-compose restart app` | 10秒 |
|
||||
| 修改依赖 | `docker-compose up --build -d` | 2-3分钟 |
|
||||
| 修改数据库 | `docker-compose down -v && up -d` | 2-3分钟 |
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ 文件说明
|
||||
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `docker-compose.yml` | 生产环境配置 |
|
||||
| `docker-compose.dev.yml` | 开发环境配置(热重载) |
|
||||
| `dev-start.sh/bat` | 开发模式启动脚本 |
|
||||
| `docker-start.sh/bat` | 生产模式启动脚本 |
|
||||
| `verify-docker.js` | 部署验证脚本 |
|
||||
| `DOCKER_SYNC.md` | 完整同步指南 |
|
||||
|
||||
---
|
||||
|
||||
## 🔗 访问地址
|
||||
|
||||
| 服务 | 地址 |
|
||||
|------|------|
|
||||
| API | http://localhost:3000/api/v1 |
|
||||
| Health | http://localhost:3000/api/v1/health |
|
||||
| MySQL | localhost:3306 (root/1qazse42W3) |
|
||||
| Redis | localhost:6379 |
|
||||
|
||||
---
|
||||
|
||||
## 💡 快速诊断
|
||||
|
||||
```bash
|
||||
# 查看所有服务状态
|
||||
docker-compose ps
|
||||
|
||||
# 查看应用日志
|
||||
docker-compose logs -f app
|
||||
|
||||
# 进入容器调试
|
||||
docker-compose exec app sh
|
||||
|
||||
# 测试 API
|
||||
curl http://localhost:3000/api/v1/health
|
||||
```
|
||||
@ -0,0 +1,75 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
echo ==============================================
|
||||
echo A股智投分析平台 - 开发模式启动
|
||||
echo 特点: 代码修改自动同步,无需重建镜像
|
||||
echo ==============================================
|
||||
|
||||
REM 检查 Docker
|
||||
docker --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo ❌ Docker 未安装
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
docker-compose --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo ❌ Docker Compose 未安装
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo ✓ Docker 环境检查通过
|
||||
|
||||
REM 创建必要目录
|
||||
if not exist logs mkdir logs
|
||||
|
||||
echo.
|
||||
echo ==============================================
|
||||
echo 启动开发环境...
|
||||
echo ==============================================
|
||||
|
||||
REM 使用开发配置启动
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
|
||||
echo.
|
||||
echo ⏳ 等待服务启动...
|
||||
timeout /t 5 /nobreak >nul
|
||||
|
||||
REM 检查状态
|
||||
echo.
|
||||
echo ==============================================
|
||||
echo 服务状态
|
||||
echo ==============================================
|
||||
docker-compose -f docker-compose.dev.yml ps
|
||||
|
||||
echo.
|
||||
echo ==============================================
|
||||
echo ✅ 开发环境已启动!
|
||||
echo ==============================================
|
||||
echo.
|
||||
echo 访问地址:
|
||||
echo • API: http://localhost:3000/api/v1
|
||||
echo • Health: http://localhost:3000/api/v1/health
|
||||
echo.
|
||||
echo 开发特性:
|
||||
echo ✓ 代码修改自动同步(无需重启容器)
|
||||
echo ✓ 支持热重载(自动重启服务)
|
||||
echo ✓ 调试日志实时输出
|
||||
echo.
|
||||
echo 常用命令:
|
||||
echo 查看日志: docker-compose -f docker-compose.dev.yml logs -f app
|
||||
echo 停止服务: docker-compose -f docker-compose.dev.yml stop
|
||||
echo 重启服务: docker-compose -f docker-compose.dev.yml restart app
|
||||
echo 完全删除: docker-compose -f docker-compose.dev.yml down -v
|
||||
echo.
|
||||
echo 💡 提示:修改 src/ 目录下的代码会立即生效!
|
||||
echo ==============================================
|
||||
|
||||
REM 询问是否查看日志
|
||||
echo.
|
||||
set /p show_logs=是否查看实时日志?(y/n):
|
||||
if /i "%show_logs%"=="y" (
|
||||
docker-compose -f docker-compose.dev.yml logs -f app
|
||||
)
|
||||
|
||||
pause
|
||||
@ -0,0 +1,77 @@
|
||||
#!/bin/bash
|
||||
# ============================================
|
||||
# A股智投分析平台 - 开发模式启动脚本
|
||||
# 支持代码热重载,修改后自动同步
|
||||
# ============================================
|
||||
|
||||
set -e
|
||||
|
||||
echo "=============================================="
|
||||
echo "A股智投分析平台 - 开发模式启动"
|
||||
echo "特点: 代码修改自动同步,无需重建镜像"
|
||||
echo "=============================================="
|
||||
|
||||
# 检查 Docker
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "❌ Docker 未安装"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker-compose &> /dev/null; then
|
||||
echo "❌ Docker Compose 未安装"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Docker 环境检查通过"
|
||||
|
||||
# 创建必要目录
|
||||
mkdir -p logs
|
||||
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
echo "启动开发环境..."
|
||||
echo "=============================================="
|
||||
|
||||
# 使用开发配置启动
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
|
||||
echo ""
|
||||
echo "⏳ 等待服务启动..."
|
||||
sleep 5
|
||||
|
||||
# 检查状态
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
echo "服务状态"
|
||||
echo "=============================================="
|
||||
docker-compose -f docker-compose.dev.yml ps
|
||||
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
echo "✅ 开发环境已启动!"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
echo "访问地址:"
|
||||
echo " • API: http://localhost:3000/api/v1"
|
||||
echo " • Health: http://localhost:3000/api/v1/health"
|
||||
echo ""
|
||||
echo "开发特性:"
|
||||
echo " ✓ 代码修改自动同步(无需重启容器)"
|
||||
echo " ✓ 支持热重载(自动重启服务)"
|
||||
echo " ✓ 调试日志实时输出"
|
||||
echo ""
|
||||
echo "常用命令:"
|
||||
echo " 查看日志: docker-compose -f docker-compose.dev.yml logs -f app"
|
||||
echo " 停止服务: docker-compose -f docker-compose.dev.yml stop"
|
||||
echo " 重启服务: docker-compose -f docker-compose.dev.yml restart app"
|
||||
echo " 完全删除: docker-compose -f docker-compose.dev.yml down -v"
|
||||
echo ""
|
||||
echo "💡 提示:修改 src/ 目录下的代码会立即生效!"
|
||||
echo "=============================================="
|
||||
|
||||
# 显示实时日志
|
||||
echo ""
|
||||
read -p "是否查看实时日志?(y/n): " show_logs
|
||||
if [ "$show_logs" = "y" ]; then
|
||||
docker-compose -f docker-compose.dev.yml logs -f app
|
||||
fi
|
||||
@ -0,0 +1,63 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
echo ==============================================
|
||||
echo A股智投分析平台 - Docker 快速启动
|
||||
echo ==============================================
|
||||
|
||||
REM 检查 Docker 是否安装
|
||||
docker --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo ❌ Docker 未安装,请先安装 Docker
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
docker-compose --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo ❌ Docker Compose 未安装,请先安装 Docker Compose
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo ✓ Docker 环境检查通过
|
||||
|
||||
REM 创建必要目录
|
||||
if not exist logs mkdir logs
|
||||
|
||||
echo.
|
||||
echo ==============================================
|
||||
echo 启动服务...
|
||||
echo ==============================================
|
||||
|
||||
REM 使用 .env.docker 覆盖默认配置
|
||||
copy /Y .env.docker .env.production >nul
|
||||
|
||||
REM 构建并启动服务
|
||||
docker-compose -f docker-compose.yml up --build -d
|
||||
|
||||
echo.
|
||||
echo ==============================================
|
||||
echo 等待服务启动...
|
||||
echo ==============================================
|
||||
|
||||
timeout /t 10 /nobreak >nul
|
||||
|
||||
echo.
|
||||
echo ==============================================
|
||||
echo ✅ 所有服务启动成功!
|
||||
echo ==============================================
|
||||
echo.
|
||||
echo 服务访问地址:
|
||||
echo • API 接口: http://localhost:3000/api/v1
|
||||
echo • 健康检查: http://localhost:3000/api/v1/health
|
||||
echo • MySQL: localhost:3306
|
||||
echo • Redis: localhost:6379
|
||||
echo.
|
||||
echo 默认账号:
|
||||
echo • MySQL: root / 1qazse42W3
|
||||
echo.
|
||||
echo 常用命令:
|
||||
echo • 查看日志: docker-compose logs -f app
|
||||
echo • 停止服务: docker-compose down
|
||||
echo • 完全重置: docker-compose down -v
|
||||
echo.
|
||||
echo ==============================================
|
||||
pause
|
||||
@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
# ============================================
|
||||
# A股智投分析平台 - Docker 快速启动脚本
|
||||
# ============================================
|
||||
|
||||
set -e
|
||||
|
||||
echo "=============================================="
|
||||
echo "A股智投分析平台 - Docker 一键启动"
|
||||
echo "=============================================="
|
||||
|
||||
# 检查 Docker 是否安装
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "❌ Docker 未安装,请先安装 Docker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker-compose &> /dev/null; then
|
||||
echo "❌ Docker Compose 未安装,请先安装 Docker Compose"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Docker 环境检查通过"
|
||||
|
||||
# 创建必要目录
|
||||
mkdir -p logs
|
||||
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
echo "启动服务..."
|
||||
echo "=============================================="
|
||||
|
||||
# 使用 .env.docker 覆盖默认配置
|
||||
cp .env.docker .env.production
|
||||
|
||||
# 构建并启动服务
|
||||
docker-compose -f docker-compose.yml up --build -d
|
||||
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
echo "等待服务启动..."
|
||||
echo "=============================================="
|
||||
|
||||
# 等待 MySQL 启动
|
||||
for i in {1..30}; do
|
||||
if docker-compose exec -T mysql mysqladmin ping -h localhost -u root -p1qazse42W3 --silent 2>/dev/null; then
|
||||
echo "✓ MySQL 已启动"
|
||||
break
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# 等待应用启动
|
||||
for i in {1..30}; do
|
||||
if curl -s http://localhost:3000/api/v1/health | grep -q "healthy"; then
|
||||
echo ""
|
||||
echo "✓ 后端服务已启动"
|
||||
break
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
echo "✅ 所有服务启动成功!"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
echo "服务访问地址:"
|
||||
echo " • API 接口: http://localhost:3000/api/v1"
|
||||
echo " • 健康检查: http://localhost:3000/api/v1/health"
|
||||
echo " • MySQL: localhost:3306"
|
||||
echo " • Redis: localhost:6379"
|
||||
echo ""
|
||||
echo "默认账号:"
|
||||
echo " • MySQL: root / 1qazse42W3"
|
||||
echo ""
|
||||
echo "常用命令:"
|
||||
echo " • 查看日志: docker-compose logs -f app"
|
||||
echo " • 停止服务: docker-compose down"
|
||||
echo " • 完全重置: docker-compose down -v"
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
@ -0,0 +1,146 @@
|
||||
-- ============================================
|
||||
-- A股智投分析平台 - 种子数据
|
||||
-- 包含基础版块、股票、市场指数数据
|
||||
-- ============================================
|
||||
|
||||
USE aguzhitou;
|
||||
|
||||
-- ============================================
|
||||
-- 1. 初始化版块数据
|
||||
-- ============================================
|
||||
INSERT INTO `sectors` (`name`, `code`) VALUES
|
||||
('半导体', '880491'),
|
||||
('新能源', '880952'),
|
||||
('医药生物', '880122'),
|
||||
('白酒', '880381'),
|
||||
('银行', '880471'),
|
||||
('证券', '880472'),
|
||||
('保险', '880473'),
|
||||
('房地产', '880482'),
|
||||
('汽车', '880391'),
|
||||
('电子', '880494'),
|
||||
('计算机', '880498'),
|
||||
('通信', '880495'),
|
||||
('传媒', '880499'),
|
||||
('军工', '880954'),
|
||||
('有色金属', '880324'),
|
||||
('钢铁', '880318'),
|
||||
('煤炭', '880952'),
|
||||
('化工', '880336'),
|
||||
('建筑材料', '880344'),
|
||||
('机械设备', '880440')
|
||||
ON DUPLICATE KEY UPDATE `name` = VALUES(`name`);
|
||||
|
||||
-- ============================================
|
||||
-- 2. 初始化股票数据
|
||||
-- ============================================
|
||||
INSERT INTO `stocks` (`code`, `name`, `sector_code`, `market_cap`, `pe`, `pb`) VALUES
|
||||
-- 银行板块
|
||||
('600000', '浦发银行', '880471', 350000000000, 4.5, 0.45),
|
||||
('600016', '民生银行', '880471', 280000000000, 4.2, 0.42),
|
||||
('600036', '招商银行', '880471', 850000000000, 6.8, 1.05),
|
||||
('601166', '兴业银行', '880471', 420000000000, 4.8, 0.58),
|
||||
('601288', '农业银行', '880471', 1200000000000, 4.5, 0.55),
|
||||
('601398', '工商银行', '880471', 1800000000000, 4.8, 0.58),
|
||||
('601988', '中国银行', '880471', 950000000000, 4.6, 0.52),
|
||||
|
||||
-- 白酒板块
|
||||
('000568', '泸州老窖', '880381', 320000000000, 25.5, 6.8),
|
||||
('000858', '五粮液', '880381', 650000000000, 22.3, 5.8),
|
||||
('600519', '贵州茅台', '880381', 2100000000000, 32.5, 9.8),
|
||||
('600809', '山西汾酒', '880381', 280000000000, 28.5, 8.5),
|
||||
|
||||
-- 半导体板块
|
||||
('688008', '澜起科技', '880491', 85000000000, 65.2, 8.5),
|
||||
('688012', '中微公司', '880491', 120000000000, 78.5, 9.2),
|
||||
('688036', '传音控股', '880491', 95000000000, 22.5, 6.8),
|
||||
('688981', '中芯国际', '880491', 420000000000, 85.5, 3.8),
|
||||
|
||||
-- 新能源板块
|
||||
('601012', '隆基绿能', '880952', 180000000000, 15.5, 2.8),
|
||||
('002594', '比亚迪', '880952', 650000000000, 32.5, 5.8),
|
||||
('300274', '阳光电源', '880952', 120000000000, 28.5, 8.5),
|
||||
('603659', '璞泰来', '880952', 45000000000, 25.5, 4.2),
|
||||
|
||||
-- 医药生物板块
|
||||
('600196', '复星医药', '880122', 68000000000, 22.5, 2.1),
|
||||
('600276', '恒瑞医药', '880122', 380000000000, 65.8, 8.5),
|
||||
('603259', '药明康德', '880122', 280000000000, 35.5, 5.8),
|
||||
|
||||
-- 证券板块
|
||||
('600030', '中信证券', '880472', 320000000000, 18.5, 1.35),
|
||||
('600837', '海通证券', '880472', 120000000000, 22.5, 0.95),
|
||||
('601688', '华泰证券', '880472', 85000000000, 15.8, 0.88),
|
||||
|
||||
-- 汽车板块
|
||||
('600104', '上汽集团', '880391', 180000000000, 12.5, 0.85),
|
||||
('601633', '长城汽车', '880391', 220000000000, 18.5, 2.8),
|
||||
('601238', '广汽集团', '880391', 95000000000, 12.8, 0.95),
|
||||
|
||||
-- 电子板块
|
||||
('000725', '京东方A', '880494', 165000000000, 45.5, 1.35),
|
||||
('002415', '海康威视', '880494', 320000000000, 22.5, 4.8),
|
||||
('601138', '工业富联', '880494', 280000000000, 15.5, 2.1),
|
||||
|
||||
-- 有色金属板块
|
||||
('601899', '紫金矿业', '880324', 320000000000, 15.8, 3.2),
|
||||
('603993', '洛阳钼业', '880324', 120000000000, 18.5, 2.5),
|
||||
|
||||
-- 化工板块
|
||||
('600309', '万华化学', '880336', 280000000000, 18.5, 3.8),
|
||||
('002001', '新和成', '880336', 65000000000, 22.5, 2.8)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`name` = VALUES(`name`),
|
||||
`sector_code` = VALUES(`sector_code`),
|
||||
`market_cap` = VALUES(`market_cap`),
|
||||
`pe` = VALUES(`pe`),
|
||||
`pb` = VALUES(`pb`);
|
||||
|
||||
-- ============================================
|
||||
-- 3. 初始化市场指数数据
|
||||
-- ============================================
|
||||
INSERT INTO `market_indices` (`name`, `code`, `current`, `change`, `changePercent`, `volume`, `turnover`, `sortOrder`) VALUES
|
||||
('上证指数', '000001', 3050.32, 15.23, 0.50, 450000000, 4200000000, 1),
|
||||
('深证成指', '399001', 9850.15, -25.60, -0.26, 520000000, 5100000000, 2),
|
||||
('创业板指', '399006', 1950.45, 8.75, 0.45, 180000000, 2100000000, 3),
|
||||
('科创50', '000688', 850.32, -5.23, -0.61, 65000000, 950000000, 4)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`current` = VALUES(`current`),
|
||||
`change` = VALUES(`change`),
|
||||
`changePercent` = VALUES(`changePercent`),
|
||||
`volume` = VALUES(`volume`),
|
||||
`turnover` = VALUES(`turnover`);
|
||||
|
||||
-- ============================================
|
||||
-- 4. 初始化版块行情数据(当前)
|
||||
-- ============================================
|
||||
INSERT INTO `sector_quotes` (`sector_code`, `current`, `change`, `changePercent`, `volume`, `turnover`, `momentumScore`, `rank`, `previous_rank`, `quote_time`) VALUES
|
||||
('880491', 2850.50, 45.25, 1.61, 850000000, 12500000000, 85.5, 1, 3, NOW()),
|
||||
('880952', 3250.80, 38.50, 1.20, 1200000000, 18500000000, 82.3, 2, 1, NOW()),
|
||||
('880122', 2150.35, 22.15, 1.04, 650000000, 9800000000, 78.5, 3, 2, NOW()),
|
||||
('880381', 4850.60, 35.80, 0.74, 450000000, 7200000000, 75.2, 4, 4, NOW()),
|
||||
('880472', 1250.25, 8.50, 0.68, 380000000, 5200000000, 72.8, 5, 6, NOW()),
|
||||
('880471', 1850.40, 10.20, 0.55, 520000000, 6800000000, 68.5, 6, 5, NOW()),
|
||||
('880391', 2250.75, 12.35, 0.55, 480000000, 6500000000, 65.3, 7, 8, NOW()),
|
||||
('880494', 1650.90, 7.80, 0.47, 420000000, 5800000000, 62.5, 8, 7, NOW()),
|
||||
('880498', 2850.15, 12.50, 0.44, 380000000, 5200000000, 58.2, 9, 10, NOW()),
|
||||
('880324', 1950.45, 6.25, 0.32, 320000000, 4800000000, 55.8, 10, 9, NOW()),
|
||||
('880473', 1450.60, 3.80, 0.26, 280000000, 3800000000, 52.5, 11, 11, NOW()),
|
||||
('880495', 1250.35, 2.15, 0.17, 250000000, 3500000000, 48.2, 12, 13, NOW()),
|
||||
('880954', 1850.80, 2.80, 0.15, 220000000, 3200000000, 45.8, 13, 12, NOW()),
|
||||
('880336', 2150.25, 2.50, 0.12, 290000000, 4200000000, 42.5, 14, 15, NOW()),
|
||||
('880440', 1750.90, 1.85, 0.11, 260000000, 3600000000, 38.2, 15, 14, NOW()),
|
||||
('880318', 850.45, 0.75, 0.09, 180000000, 2200000000, 35.8, 16, 16, NOW()),
|
||||
('880344', 950.60, 0.60, 0.06, 150000000, 1800000000, 32.5, 17, 18, NOW()),
|
||||
('880482', 1250.35, -2.50, -0.20, 200000000, 2800000000, 28.2, 18, 17, NOW()),
|
||||
('880499', 850.25, -2.15, -0.25, 120000000, 1500000000, 25.8, 19, 19, NOW()),
|
||||
('880952', 450.80, -3.25, -0.72, 95000000, 1200000000, 18.5, 20, 20, NOW());
|
||||
|
||||
-- ============================================
|
||||
-- 5. 初始化完成
|
||||
-- ============================================
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM sectors) AS sector_count,
|
||||
(SELECT COUNT(*) FROM stocks) AS stock_count,
|
||||
(SELECT COUNT(*) FROM market_indices) AS index_count,
|
||||
'Seed data loaded successfully' AS status;
|
||||
@ -0,0 +1,166 @@
|
||||
-- ============================================
|
||||
-- A股智投分析平台 - 分区维护工具
|
||||
-- 包含分区查询、添加、删除等操作
|
||||
-- ============================================
|
||||
|
||||
USE aguzhitou;
|
||||
|
||||
-- ============================================
|
||||
-- 1. 查看所有分区表信息
|
||||
-- ============================================
|
||||
DROP VIEW IF EXISTS `partition_info`;
|
||||
CREATE VIEW `partition_info` AS
|
||||
SELECT
|
||||
TABLE_NAME,
|
||||
PARTITION_NAME,
|
||||
PARTITION_METHOD,
|
||||
PARTITION_EXPRESSION,
|
||||
TABLE_ROWS,
|
||||
ROUND(DATA_LENGTH / 1024 / 1024, 2) AS DATA_SIZE_MB,
|
||||
ROUND(INDEX_LENGTH / 1024 / 1024, 2) AS INDEX_SIZE_MB,
|
||||
ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS TOTAL_SIZE_MB
|
||||
FROM information_schema.PARTITIONS
|
||||
WHERE TABLE_SCHEMA = 'aguzhitou'
|
||||
AND PARTITION_NAME IS NOT NULL
|
||||
ORDER BY TABLE_NAME, PARTITION_NAME;
|
||||
|
||||
-- ============================================
|
||||
-- 2. 创建添加新分区的存储过程
|
||||
-- ============================================
|
||||
DELIMITER $$
|
||||
|
||||
DROP PROCEDURE IF EXISTS `AddNewPartition`$$
|
||||
CREATE PROCEDURE `AddNewPartition`(
|
||||
IN p_table_name VARCHAR(64),
|
||||
IN p_partition_name VARCHAR(64),
|
||||
IN p_less_than_date VARCHAR(10)
|
||||
)
|
||||
BEGIN
|
||||
DECLARE v_sql VARCHAR(500);
|
||||
SET v_sql = CONCAT(
|
||||
'ALTER TABLE ', p_table_name,
|
||||
' ADD PARTITION (PARTITION ', p_partition_name,
|
||||
' VALUES LESS THAN (\'', p_less_than_date, '\'))'
|
||||
);
|
||||
SET @sql = v_sql;
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
SELECT CONCAT('Partition ', p_partition_name, ' added to ', p_table_name) AS result;
|
||||
END$$
|
||||
|
||||
-- ============================================
|
||||
-- 3. 创建删除旧分区的存储过程
|
||||
-- ============================================
|
||||
DROP PROCEDURE IF EXISTS `DropOldPartition`$$
|
||||
CREATE PROCEDURE `DropOldPartition`(
|
||||
IN p_table_name VARCHAR(64),
|
||||
IN p_partition_name VARCHAR(64)
|
||||
)
|
||||
BEGIN
|
||||
DECLARE v_sql VARCHAR(500);
|
||||
SET v_sql = CONCAT(
|
||||
'ALTER TABLE ', p_table_name,
|
||||
' DROP PARTITION ', p_partition_name
|
||||
);
|
||||
SET @sql = v_sql;
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
SELECT CONCAT('Partition ', p_partition_name, ' dropped from ', p_table_name) AS result;
|
||||
END$$
|
||||
|
||||
-- ============================================
|
||||
-- 4. 创建自动添加未来分区的存储过程
|
||||
-- ============================================
|
||||
DROP PROCEDURE IF EXISTS `AddFuturePartitions`$$
|
||||
CREATE PROCEDURE `AddFuturePartitions`()
|
||||
BEGIN
|
||||
DECLARE v_next_month DATE;
|
||||
DECLARE v_year INT;
|
||||
DECLARE v_month INT;
|
||||
DECLARE v_partition_name VARCHAR(20);
|
||||
DECLARE v_less_than VARCHAR(10);
|
||||
|
||||
-- 计算下个月
|
||||
SET v_next_month = DATE_ADD(DATE_FORMAT(CURDATE(), '%Y-%m-01'), INTERVAL 1 MONTH);
|
||||
SET v_year = YEAR(v_next_month);
|
||||
SET v_month = MONTH(v_next_month);
|
||||
SET v_partition_name = CONCAT('p_', v_year, '_', LPAD(v_month, 2, '0'));
|
||||
SET v_less_than = DATE_FORMAT(DATE_ADD(v_next_month, INTERVAL 1 MONTH), '%Y-%m-%d');
|
||||
|
||||
-- 为股票行情历史表添加分区
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.PARTITIONS
|
||||
WHERE TABLE_SCHEMA = 'aguzhitou'
|
||||
AND TABLE_NAME = 'stock_quotes_history'
|
||||
AND PARTITION_NAME = v_partition_name
|
||||
) THEN
|
||||
CALL AddNewPartition('stock_quotes_history', v_partition_name, v_less_than);
|
||||
END IF;
|
||||
|
||||
-- 为版块行情表添加分区
|
||||
SET v_partition_name = CONCAT('p_sector_', v_year, '_', LPAD(v_month, 2, '0'));
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.PARTITIONS
|
||||
WHERE TABLE_SCHEMA = 'aguzhitou'
|
||||
AND TABLE_NAME = 'sector_quotes'
|
||||
AND PARTITION_NAME = v_partition_name
|
||||
) THEN
|
||||
CALL AddNewPartition('sector_quotes', v_partition_name, v_less_than);
|
||||
END IF;
|
||||
|
||||
-- 为新高新低表添加分区
|
||||
SET v_partition_name = CONCAT('p_hl_', v_year, '_', LPAD(v_month, 2, '0'));
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.PARTITIONS
|
||||
WHERE TABLE_SCHEMA = 'aguzhitou'
|
||||
AND TABLE_NAME = 'high_low_stocks'
|
||||
AND PARTITION_NAME = v_partition_name
|
||||
) THEN
|
||||
CALL AddNewPartition('high_low_stocks', v_partition_name, v_less_than);
|
||||
END IF;
|
||||
|
||||
-- 为动量股票表添加分区
|
||||
SET v_partition_name = CONCAT('p_ms_', v_year, '_', LPAD(v_month, 2, '0'));
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.PARTITIONS
|
||||
WHERE TABLE_SCHEMA = 'aguzhitou'
|
||||
AND TABLE_NAME = 'momentum_stocks'
|
||||
AND PARTITION_NAME = v_partition_name
|
||||
) THEN
|
||||
CALL AddNewPartition('momentum_stocks', v_partition_name, v_less_than);
|
||||
END IF;
|
||||
|
||||
SELECT CONCAT('Future partitions for ', DATE_FORMAT(v_next_month, '%Y-%m'), ' added successfully') AS result;
|
||||
END$$
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
-- ============================================
|
||||
-- 5. 创建分区统计视图
|
||||
-- ============================================
|
||||
DROP VIEW IF EXISTS `partition_summary`;
|
||||
CREATE VIEW `partition_summary` AS
|
||||
SELECT
|
||||
TABLE_NAME,
|
||||
COUNT(*) AS partition_count,
|
||||
SUM(TABLE_ROWS) AS total_rows,
|
||||
ROUND(SUM(DATA_LENGTH) / 1024 / 1024, 2) AS total_data_mb,
|
||||
ROUND(SUM(INDEX_LENGTH) / 1024 / 1024, 2) AS total_index_mb,
|
||||
ROUND(SUM(DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS total_size_mb
|
||||
FROM information_schema.PARTITIONS
|
||||
WHERE TABLE_SCHEMA = 'aguzhitou'
|
||||
AND PARTITION_NAME IS NOT NULL
|
||||
GROUP BY TABLE_NAME
|
||||
ORDER BY total_size_mb DESC;
|
||||
|
||||
-- ============================================
|
||||
-- 6. 显示当前分区状态
|
||||
-- ============================================
|
||||
SELECT '分区表创建完成' AS status;
|
||||
SELECT '当前分区统计:' AS info;
|
||||
SELECT * FROM partition_summary;
|
||||
|
||||
SELECT '分区详细信息:' AS info;
|
||||
SELECT * FROM partition_info;
|
||||
@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Docker 部署验证脚本
|
||||
* 检查所有服务是否正常运行
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
const { exec } = require('child_process');
|
||||
const util = require('util');
|
||||
const execPromise = util.promisify(exec);
|
||||
|
||||
const DELAY = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
async function checkDocker() {
|
||||
console.log('[1] 检查 Docker 服务...');
|
||||
try {
|
||||
await execPromise('docker ps');
|
||||
console.log(' ✓ Docker 运行正常');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(' ✗ Docker 未运行');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkContainers() {
|
||||
console.log('[2] 检查容器状态...');
|
||||
try {
|
||||
const { stdout } = await execPromise('docker-compose ps');
|
||||
console.log(stdout);
|
||||
|
||||
if (stdout.includes('aguzhitou-mysql') && stdout.includes('aguzhitou-redis') && stdout.includes('aguzhitou-app')) {
|
||||
console.log(' ✓ 所有容器已启动');
|
||||
return true;
|
||||
} else {
|
||||
console.log(' ✗ 部分容器未启动');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(' ✗ 无法获取容器状态');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAPI() {
|
||||
console.log('[3] 检查 API 服务...');
|
||||
return new Promise((resolve) => {
|
||||
const req = http.get('http://localhost:3000/api/v1/health', (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const result = JSON.parse(data);
|
||||
if (result.code === 200) {
|
||||
console.log(' ✓ API 服务正常');
|
||||
console.log(` ✓ 服务时间: ${result.data.timestamp}`);
|
||||
resolve(true);
|
||||
} else {
|
||||
console.log(' ✗ API 返回异常');
|
||||
resolve(false);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(' ✗ API 响应解析失败');
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
console.log(' ✗ 无法连接 API');
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
req.setTimeout(5000, () => {
|
||||
console.log(' ✗ API 连接超时');
|
||||
req.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function checkMySQL() {
|
||||
console.log('[4] 检查 MySQL 数据库...');
|
||||
try {
|
||||
const { stdout } = await execPromise(
|
||||
'docker-compose exec -T mysql mysql -u root -p1qazse42W3 -e "SELECT COUNT(*) as tables FROM information_schema.tables WHERE table_schema=\'aguzhitou\';"'
|
||||
);
|
||||
|
||||
const match = stdout.match(/(\d+)/);
|
||||
if (match && parseInt(match[1]) >= 12) {
|
||||
console.log(` ✓ MySQL 数据库正常 (${match[1]} 张表)`);
|
||||
return true;
|
||||
} else {
|
||||
console.log(' ✗ 数据库表数量异常');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(' ✗ 无法连接 MySQL');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkPartitions() {
|
||||
console.log('[5] 检查数据库分区...');
|
||||
try {
|
||||
const { stdout } = await execPromise(
|
||||
'docker-compose exec -T mysql mysql -u root -p1qazse42W3 -e "SELECT COUNT(DISTINCT table_name) as tables, COUNT(*) as partitions FROM information_schema.partitions WHERE table_schema=\'aguzhitou\' AND partition_name IS NOT NULL;"'
|
||||
);
|
||||
|
||||
const match = stdout.match(/(\d+)\s*\|\s*(\d+)/);
|
||||
if (match) {
|
||||
console.log(` ✓ 分区表: ${match[1]} 张`);
|
||||
console.log(` ✓ 总分区: ${match[2]} 个`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.log(' ✗ 无法获取分区信息');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkRedis() {
|
||||
console.log('[6] 检查 Redis 缓存...');
|
||||
try {
|
||||
const { stdout } = await execPromise('docker-compose exec -T redis redis-cli ping');
|
||||
if (stdout.includes('PONG')) {
|
||||
console.log(' ✓ Redis 运行正常');
|
||||
return true;
|
||||
}
|
||||
console.log(' ✗ Redis 响应异常');
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.log(' ✗ 无法连接 Redis');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testAPIEndpoints() {
|
||||
console.log('[7] 测试 API 端点...');
|
||||
const endpoints = [
|
||||
{ path: '/api/v1/market/indices', name: '市场指数' },
|
||||
{ path: '/api/v1/sectors', name: '版块列表' },
|
||||
{ path: '/api/v1/stocks/search?keyword=茅台', name: '股票搜索' }
|
||||
];
|
||||
|
||||
let success = 0;
|
||||
for (const endpoint of endpoints) {
|
||||
try {
|
||||
const result = await new Promise((resolve) => {
|
||||
http.get(`http://localhost:3000${endpoint.path}`, (res) => {
|
||||
resolve(res.statusCode === 200);
|
||||
}).on('error', () => resolve(false));
|
||||
});
|
||||
|
||||
if (result) {
|
||||
console.log(` ✓ ${endpoint.name}`);
|
||||
success++;
|
||||
} else {
|
||||
console.log(` ✗ ${endpoint.name}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(` ✗ ${endpoint.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
return success === endpoints.length;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('='.repeat(60));
|
||||
console.log('A股智投分析平台 - Docker 部署验证');
|
||||
console.log('='.repeat(60));
|
||||
console.log();
|
||||
|
||||
const results = {
|
||||
docker: await checkDocker(),
|
||||
containers: await checkContainers(),
|
||||
api: await checkAPI(),
|
||||
mysql: await checkMySQL(),
|
||||
partitions: await checkPartitions(),
|
||||
redis: await checkRedis(),
|
||||
endpoints: await testAPIEndpoints()
|
||||
};
|
||||
|
||||
console.log();
|
||||
console.log('='.repeat(60));
|
||||
|
||||
const allPassed = Object.values(results).every(r => r === true);
|
||||
|
||||
if (allPassed) {
|
||||
console.log('✅ 所有检查通过!部署成功!');
|
||||
console.log('='.repeat(60));
|
||||
console.log();
|
||||
console.log('访问地址:');
|
||||
console.log(' • API 文档: http://localhost:3000/api/v1/health');
|
||||
console.log(' • MySQL: localhost:3306 (root/1qazse42W3)');
|
||||
console.log(' • Redis: localhost:6379');
|
||||
console.log();
|
||||
} else {
|
||||
console.log('❌ 部分检查未通过,请查看日志:');
|
||||
console.log(' docker-compose logs');
|
||||
console.log('='.repeat(60));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@ -0,0 +1,215 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Routes, Route, NavLink, useNavigate } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Users,
|
||||
Brain,
|
||||
Database,
|
||||
Activity,
|
||||
Upload,
|
||||
Settings,
|
||||
LogOut,
|
||||
Shield,
|
||||
Menu,
|
||||
X
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import UserManagement from './pages/UserManagement';
|
||||
import AIConfig from './pages/AIConfig';
|
||||
import DataSourceConfig from './pages/DataSourceConfig';
|
||||
import DataCheck from './pages/DataCheck';
|
||||
import DataImport from './pages/DataImport';
|
||||
|
||||
const sidebarItems = [
|
||||
{ path: '/admin', icon: LayoutDashboard, label: '总览' },
|
||||
{ path: '/admin/users', icon: Users, label: '用户管理' },
|
||||
{ path: '/admin/ai-config', icon: Brain, label: 'AI模型配置' },
|
||||
{ path: '/admin/data-source', icon: Database, label: '数据源配置' },
|
||||
{ path: '/admin/data-check', icon: Activity, label: '数据检测' },
|
||||
{ path: '/admin/data-import', icon: Upload, label: '数据导入' },
|
||||
];
|
||||
|
||||
export default function Admin() {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const { user, isAuthenticated, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
setIsMobile(window.innerWidth < 1024);
|
||||
if (window.innerWidth < 1024) {
|
||||
setSidebarOpen(false);
|
||||
} else {
|
||||
setSidebarOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
}, []);
|
||||
|
||||
// 简单的管理员权限检查
|
||||
useEffect(() => {
|
||||
// 这里应该检查用户是否有管理员权限
|
||||
// 暂时注释掉,方便测试
|
||||
// if (!isAuthenticated) {
|
||||
// navigate('/');
|
||||
// }
|
||||
}, [isAuthenticated, navigate]);
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] flex">
|
||||
{/* Mobile Overlay */}
|
||||
{isMobile && sidebarOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Sidebar */}
|
||||
<motion.aside
|
||||
initial={false}
|
||||
animate={{ width: sidebarOpen ? 260 : isMobile ? 0 : 70 }}
|
||||
className={`fixed lg:static inset-y-0 left-0 z-50 bg-[#111111] border-r border-[#2a2a2a] flex flex-col ${
|
||||
isMobile && !sidebarOpen ? 'hidden' : ''
|
||||
}`}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div className="h-16 flex items-center px-4 border-b border-[#2a2a2a]">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-[#ff6b35] to-[#ff9f43] rounded-lg flex items-center justify-center">
|
||||
<Shield className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
{sidebarOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="ml-3"
|
||||
>
|
||||
<h1 className="text-white font-bold">管理后台</h1>
|
||||
<p className="text-xs text-[#666]">Admin Panel</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 py-4 px-2 space-y-1 overflow-y-auto">
|
||||
{sidebarItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
end={item.path === '/admin'}
|
||||
onClick={() => isMobile && setSidebarOpen(false)}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-3 px-3 py-3 rounded-lg transition-all ${
|
||||
isActive
|
||||
? 'bg-gradient-to-r from-[#ff6b35]/20 to-transparent text-[#ff6b35] border-l-2 border-[#ff6b35]'
|
||||
: 'text-[#b0b0b0] hover:bg-[#1a1a1a] hover:text-white'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<item.icon className="w-5 h-5 flex-shrink-0" />
|
||||
{sidebarOpen && (
|
||||
<motion.span
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="text-sm font-medium whitespace-nowrap"
|
||||
>
|
||||
{item.label}
|
||||
</motion.span>
|
||||
)}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* User Info */}
|
||||
<div className="p-4 border-t border-[#2a2a2a]">
|
||||
{sidebarOpen ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-[#2a2a2a] rounded-full flex items-center justify-center">
|
||||
<span className="text-white font-medium">
|
||||
{user?.username?.[0]?.toUpperCase() || 'A'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-white text-sm font-medium truncate">
|
||||
{user?.username || '管理员'}
|
||||
</p>
|
||||
<p className="text-xs text-[#666] truncate">
|
||||
{user?.email || 'admin@example.com'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="p-2 hover:bg-[#2a2a2a] rounded-lg transition-colors"
|
||||
title="退出登录"
|
||||
>
|
||||
<LogOut className="w-4 h-4 text-[#b0b0b0]" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex justify-center p-2 hover:bg-[#2a2a2a] rounded-lg transition-colors"
|
||||
title="退出登录"
|
||||
>
|
||||
<LogOut className="w-5 h-5 text-[#b0b0b0]" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</motion.aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
{/* Header */}
|
||||
<header className="h-16 bg-[#111111] border-b border-[#2a2a2a] flex items-center justify-between px-4 lg:px-6">
|
||||
<button
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
className="p-2 hover:bg-[#2a2a2a] rounded-lg transition-colors"
|
||||
>
|
||||
{sidebarOpen ? (
|
||||
<X className="w-5 h-5 text-[#b0b0b0]" />
|
||||
) : (
|
||||
<Menu className="w-5 h-5 text-[#b0b0b0]" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<a
|
||||
href="/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-[#b0b0b0] hover:text-[#ff6b35] transition-colors"
|
||||
>
|
||||
访问前台 →
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Page Content */}
|
||||
<main className="flex-1 overflow-auto p-4 lg:p-6">
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/users" element={<UserManagement />} />
|
||||
<Route path="/ai-config" element={<AIConfig />} />
|
||||
<Route path="/data-source" element={<DataSourceConfig />} />
|
||||
<Route path="/data-check" element={<DataCheck />} />
|
||||
<Route path="/data-import" element={<DataImport />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,223 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Users,
|
||||
Database,
|
||||
Activity,
|
||||
TrendingUp,
|
||||
Server,
|
||||
Clock
|
||||
} from 'lucide-react';
|
||||
|
||||
interface SystemStats {
|
||||
totalUsers: number;
|
||||
totalStocks: number;
|
||||
totalSectors: number;
|
||||
dataCompleteness: number;
|
||||
lastSync: string;
|
||||
apiStatus: {
|
||||
akshare: boolean;
|
||||
database: boolean;
|
||||
redis: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const [stats, setStats] = useState<SystemStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// 模拟获取系统统计信息
|
||||
const fetchStats = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 这里应该调用后端 API
|
||||
// const response = await fetch('/api/v1/admin/stats');
|
||||
// const data = await response.json();
|
||||
|
||||
// 模拟数据
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
setStats({
|
||||
totalUsers: 128,
|
||||
totalStocks: 5234,
|
||||
totalSectors: 86,
|
||||
dataCompleteness: 87.5,
|
||||
lastSync: '2024-03-07 14:30:00',
|
||||
apiStatus: {
|
||||
akshare: true,
|
||||
database: true,
|
||||
redis: true,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch stats:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchStats();
|
||||
}, []);
|
||||
|
||||
const statCards = [
|
||||
{
|
||||
label: '总用户数',
|
||||
value: stats?.totalUsers || 0,
|
||||
icon: Users,
|
||||
color: 'from-blue-500 to-blue-600',
|
||||
trend: '+12%'
|
||||
},
|
||||
{
|
||||
label: '股票总数',
|
||||
value: stats?.totalStocks || 0,
|
||||
icon: TrendingUp,
|
||||
color: 'from-green-500 to-green-600',
|
||||
trend: '+3.2%'
|
||||
},
|
||||
{
|
||||
label: '版块总数',
|
||||
value: stats?.totalSectors || 0,
|
||||
icon: Database,
|
||||
color: 'from-purple-500 to-purple-600',
|
||||
trend: '0%'
|
||||
},
|
||||
{
|
||||
label: '数据完整度',
|
||||
value: `${stats?.dataCompleteness || 0}%`,
|
||||
icon: Activity,
|
||||
color: 'from-orange-500 to-orange-600',
|
||||
trend: '-2.1%'
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">管理总览</h1>
|
||||
<p className="text-[#b0b0b0] mt-1">系统运行状态和数据概况</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{statCards.map((card, index) => (
|
||||
<motion.div
|
||||
key={card.label}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
className="bg-[#111111] border border-[#2a2a2a] rounded-xl p-5"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-[#b0b0b0] text-sm">{card.label}</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">{card.value}</p>
|
||||
<span className={`text-xs mt-2 inline-block ${
|
||||
card.trend.startsWith('+') ? 'text-green-400' :
|
||||
card.trend.startsWith('-') ? 'text-red-400' : 'text-[#666]'
|
||||
}`}>
|
||||
{card.trend} 较上周
|
||||
</span>
|
||||
</div>
|
||||
<div className={`w-12 h-12 rounded-xl bg-gradient-to-br ${card.color} flex items-center justify-center`}>
|
||||
<card.icon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* System Status */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* API Status */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className="bg-[#111111] border border-[#2a2a2a] rounded-xl p-5"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Server className="w-5 h-5 text-[#ff6b35]" />
|
||||
<h2 className="text-white font-semibold">服务状态</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ name: 'AKShare 数据服务', status: stats?.apiStatus.akshare },
|
||||
{ name: 'MySQL 数据库', status: stats?.apiStatus.database },
|
||||
{ name: 'Redis 缓存', status: stats?.apiStatus.redis },
|
||||
].map((service) => (
|
||||
<div key={service.name} className="flex items-center justify-between py-2">
|
||||
<span className="text-[#b0b0b0]">{service.name}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full ${
|
||||
service.status ? 'bg-green-500' : 'bg-red-500'
|
||||
}`} />
|
||||
<span className={`text-sm ${
|
||||
service.status ? 'text-green-400' : 'text-red-400'
|
||||
}`}>
|
||||
{service.status ? '正常' : '异常'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="bg-[#111111] border border-[#2a2a2a] rounded-xl p-5"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Clock className="w-5 h-5 text-[#ff6b35]" />
|
||||
<h2 className="text-white font-semibold">最近活动</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ time: '14:30', action: '数据同步完成', detail: '共更新 5234 只股票数据' },
|
||||
{ time: '12:15', action: '新用户注册', detail: '用户 user_128 注册成功' },
|
||||
{ time: '10:00', action: '系统备份', detail: '数据库自动备份完成' },
|
||||
{ time: '08:30', action: 'AI模型更新', detail: '动量计算模型已更新' },
|
||||
].map((activity, index) => (
|
||||
<div key={index} className="flex gap-3 py-2 border-b border-[#2a2a2a] last:border-0">
|
||||
<span className="text-[#666] text-sm w-12">{activity.time}</span>
|
||||
<div>
|
||||
<p className="text-white text-sm">{activity.action}</p>
|
||||
<p className="text-[#666] text-xs">{activity.detail}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.6 }}
|
||||
className="bg-[#111111] border border-[#2a2a2a] rounded-xl p-5"
|
||||
>
|
||||
<h2 className="text-white font-semibold mb-4">快速操作</h2>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button className="px-4 py-2 bg-[#ff6b35] text-white rounded-lg hover:bg-[#ff6b35]/90 transition-colors text-sm">
|
||||
立即同步数据
|
||||
</button>
|
||||
<button className="px-4 py-2 bg-[#2a2a2a] text-white rounded-lg hover:bg-[#333] transition-colors text-sm">
|
||||
导出用户列表
|
||||
</button>
|
||||
<button className="px-4 py-2 bg-[#2a2a2a] text-white rounded-lg hover:bg-[#333] transition-colors text-sm">
|
||||
清理缓存
|
||||
</button>
|
||||
<button className="px-4 py-2 bg-[#2a2a2a] text-white rounded-lg hover:bg-[#333] transition-colors text-sm">
|
||||
系统设置
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,409 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Activity,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
XCircle,
|
||||
RefreshCw,
|
||||
Play,
|
||||
Database,
|
||||
TrendingUp,
|
||||
Calendar,
|
||||
Clock
|
||||
} from 'lucide-react';
|
||||
|
||||
interface DataCheckItem {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'stock' | 'sector' | 'index' | 'kline';
|
||||
total: number;
|
||||
current: number;
|
||||
lastUpdate: string;
|
||||
status: 'complete' | 'incomplete' | 'missing';
|
||||
details?: string;
|
||||
}
|
||||
|
||||
interface CheckProgress {
|
||||
isChecking: boolean;
|
||||
isBuffering: boolean;
|
||||
progress: number;
|
||||
currentTask: string;
|
||||
}
|
||||
|
||||
export default function DataCheck() {
|
||||
const [dataStatus, setDataStatus] = useState<DataCheckItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [progress, setProgress] = useState<CheckProgress>({
|
||||
isChecking: false,
|
||||
isBuffering: false,
|
||||
progress: 0,
|
||||
currentTask: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchDataStatus();
|
||||
}, []);
|
||||
|
||||
const fetchDataStatus = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 模拟 API 调用
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
|
||||
const mockData: DataCheckItem[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: '股票基础数据',
|
||||
type: 'stock',
|
||||
total: 5234,
|
||||
current: 5234,
|
||||
lastUpdate: '2024-03-07 14:30:00',
|
||||
status: 'complete',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '版块数据',
|
||||
type: 'sector',
|
||||
total: 86,
|
||||
current: 86,
|
||||
lastUpdate: '2024-03-07 14:30:00',
|
||||
status: 'complete',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '市场指数',
|
||||
type: 'index',
|
||||
total: 4,
|
||||
current: 4,
|
||||
lastUpdate: '2024-03-07 14:30:00',
|
||||
status: 'complete',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: '股票K线数据',
|
||||
type: 'kline',
|
||||
total: 5234 * 60,
|
||||
current: 4800 * 60,
|
||||
lastUpdate: '2024-03-07 10:00:00',
|
||||
status: 'incomplete',
|
||||
details: '部分股票缺少近期K线数据',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: '版块K线数据',
|
||||
type: 'kline',
|
||||
total: 86 * 60,
|
||||
current: 80 * 60,
|
||||
lastUpdate: '2024-03-07 10:00:00',
|
||||
status: 'incomplete',
|
||||
details: '6个版块数据待更新',
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: '实时行情数据',
|
||||
type: 'stock',
|
||||
total: 5234,
|
||||
current: 0,
|
||||
lastUpdate: '-',
|
||||
status: 'missing',
|
||||
details: '今日行情数据未获取',
|
||||
},
|
||||
];
|
||||
|
||||
setDataStatus(mockData);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch data status:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckData = async () => {
|
||||
setProgress({
|
||||
isChecking: true,
|
||||
isBuffering: false,
|
||||
progress: 0,
|
||||
currentTask: '正在检查数据完整性...',
|
||||
});
|
||||
|
||||
// 模拟检查进度
|
||||
for (let i = 0; i <= 100; i += 10) {
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
setProgress(prev => ({ ...prev, progress: i }));
|
||||
}
|
||||
|
||||
setProgress(prev => ({ ...prev, isChecking: false }));
|
||||
await fetchDataStatus();
|
||||
};
|
||||
|
||||
const handleBufferData = async () => {
|
||||
setProgress({
|
||||
isChecking: false,
|
||||
isBuffering: true,
|
||||
progress: 0,
|
||||
currentTask: '正在获取缺失数据...',
|
||||
});
|
||||
|
||||
const tasks = [
|
||||
'获取股票基础数据...',
|
||||
'获取版块数据...',
|
||||
'获取市场指数...',
|
||||
'获取K线数据...',
|
||||
'计算动量指标...',
|
||||
'更新缓存...',
|
||||
];
|
||||
|
||||
for (let i = 0; i < tasks.length; i++) {
|
||||
setProgress(prev => ({
|
||||
...prev,
|
||||
currentTask: tasks[i],
|
||||
progress: Math.round((i / tasks.length) * 100),
|
||||
}));
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
}
|
||||
|
||||
setProgress(prev => ({ ...prev, progress: 100 }));
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
setProgress({
|
||||
isChecking: false,
|
||||
isBuffering: false,
|
||||
progress: 0,
|
||||
currentTask: '',
|
||||
});
|
||||
|
||||
await fetchDataStatus();
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: DataCheckItem['status']) => {
|
||||
switch (status) {
|
||||
case 'complete':
|
||||
return <CheckCircle className="w-5 h-5 text-green-400" />;
|
||||
case 'incomplete':
|
||||
return <AlertCircle className="w-5 h-5 text-yellow-400" />;
|
||||
case 'missing':
|
||||
return <XCircle className="w-5 h-5 text-red-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status: DataCheckItem['status']) => {
|
||||
switch (status) {
|
||||
case 'complete':
|
||||
return '完整';
|
||||
case 'incomplete':
|
||||
return '部分缺失';
|
||||
case 'missing':
|
||||
return '缺失';
|
||||
}
|
||||
};
|
||||
|
||||
const completionRate = dataStatus.length > 0
|
||||
? Math.round((dataStatus.filter(d => d.status === 'complete').length / dataStatus.length) * 100)
|
||||
: 0;
|
||||
|
||||
const totalMissing = dataStatus.reduce((acc, item) => {
|
||||
if (item.status !== 'complete') {
|
||||
return acc + (item.total - item.current);
|
||||
}
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">数据检测</h1>
|
||||
<p className="text-[#b0b0b0] mt-1">检查数据完整性并一键缓冲缺失数据</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleCheckData}
|
||||
disabled={progress.isChecking || progress.isBuffering}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-[#2a2a2a] text-white rounded-lg hover:bg-[#333] transition-colors disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${progress.isChecking ? 'animate-spin' : ''}`} />
|
||||
检查数据
|
||||
</button>
|
||||
<button
|
||||
onClick={handleBufferData}
|
||||
disabled={progress.isChecking || progress.isBuffering || totalMissing === 0}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-[#ff6b35] text-white rounded-lg hover:bg-[#ff6b35]/90 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Play className="w-4 h-4" />
|
||||
一键缓冲
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
{(progress.isChecking || progress.isBuffering) && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="bg-[#111111] border border-[#2a2a2a] rounded-xl p-5"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-white">{progress.currentTask}</span>
|
||||
<span className="text-[#ff6b35] font-medium">{progress.progress}%</span>
|
||||
</div>
|
||||
<div className="h-2 bg-[#2a2a2a] rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
className="h-full bg-gradient-to-r from-[#ff6b35] to-[#ff9f43]"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${progress.progress}%` }}
|
||||
transition={{ duration: 0.3 }}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="bg-[#111111] border border-[#2a2a2a] rounded-xl p-5"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[#b0b0b0] text-sm">数据完整度</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">{completionRate}%</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 bg-green-500/20 rounded-xl flex items-center justify-center">
|
||||
<CheckCircle className="w-6 h-6 text-green-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 h-2 bg-[#2a2a2a] rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-green-500 rounded-full"
|
||||
style={{ width: `${completionRate}%` }}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="bg-[#111111] border border-[#2a2a2a] rounded-xl p-5"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[#b0b0b0] text-sm">缺失数据条数</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">{totalMissing.toLocaleString()}</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 bg-red-500/20 rounded-xl flex items-center justify-center">
|
||||
<AlertCircle className="w-6 h-6 text-red-400" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-[#666] mt-3">
|
||||
需要缓冲补充
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="bg-[#111111] border border-[#2a2a2a] rounded-xl p-5"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[#b0b0b0] text-sm">上次检查</p>
|
||||
<p className="text-lg font-bold text-white mt-1">
|
||||
{new Date().toLocaleTimeString('zh-CN')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 bg-blue-500/20 rounded-xl flex items-center justify-center">
|
||||
<Clock className="w-6 h-6 text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-[#666] mt-3">
|
||||
今天
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Data Status Table */}
|
||||
<div className="bg-[#111111] border border-[#2a2a2a] rounded-xl overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[#2a2a2a] flex items-center gap-3">
|
||||
<Database className="w-5 h-5 text-[#ff6b35]" />
|
||||
<h2 className="text-white font-semibold">数据状态详情</h2>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-[#2a2a2a]">
|
||||
<th className="px-5 py-3 text-left text-sm font-medium text-[#b0b0b0]">数据项</th>
|
||||
<th className="px-5 py-3 text-left text-sm font-medium text-[#b0b0b0]">类型</th>
|
||||
<th className="px-5 py-3 text-left text-sm font-medium text-[#b0b0b0]">完整度</th>
|
||||
<th className="px-5 py-3 text-left text-sm font-medium text-[#b0b0b0]">状态</th>
|
||||
<th className="px-5 py-3 text-left text-sm font-medium text-[#b0b0b0]">最后更新</th>
|
||||
<th className="px-5 py-3 text-left text-sm font-medium text-[#b0b0b0]">备注</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-5 py-8 text-center text-[#b0b0b0]">
|
||||
<RefreshCw className="w-6 h-6 animate-spin mx-auto mb-2" />
|
||||
加载中...
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
dataStatus.map((item) => (
|
||||
<tr key={item.id} className="border-b border-[#2a2a2a] last:border-0 hover:bg-[#1a1a1a]">
|
||||
<td className="px-5 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{item.type === 'stock' && <TrendingUp className="w-4 h-4 text-blue-400" />}
|
||||
{item.type === 'sector' && <Database className="w-4 h-4 text-purple-400" />}
|
||||
{item.type === 'index' && <Activity className="w-4 h-4 text-green-400" />}
|
||||
{item.type === 'kline' && <Calendar className="w-4 h-4 text-orange-400" />}
|
||||
<span className="text-white">{item.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-5 py-4 text-[#b0b0b0] capitalize">{item.type}</td>
|
||||
<td className="px-5 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-24 h-2 bg-[#2a2a2a] rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${
|
||||
item.status === 'complete' ? 'bg-green-500' :
|
||||
item.status === 'incomplete' ? 'bg-yellow-500' :
|
||||
'bg-red-500'
|
||||
}`}
|
||||
style={{ width: `${(item.current / item.total) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-[#b0b0b0]">
|
||||
{((item.current / item.total) * 100).toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-5 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
{getStatusIcon(item.status)}
|
||||
<span className={`text-sm ${
|
||||
item.status === 'complete' ? 'text-green-400' :
|
||||
item.status === 'incomplete' ? 'text-yellow-400' :
|
||||
'text-red-400'
|
||||
}`}>
|
||||
{getStatusText(item.status)}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-5 py-4 text-[#b0b0b0] text-sm">{item.lastUpdate}</td>
|
||||
<td className="px-5 py-4 text-[#666] text-sm">{item.details || '-'}</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,374 @@
|
||||
import { useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
Database,
|
||||
Save,
|
||||
RefreshCw,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
Globe,
|
||||
Clock,
|
||||
Shield
|
||||
} from 'lucide-react';
|
||||
|
||||
interface DataSource {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'akshare' | 'tushare' | 'custom';
|
||||
url: string;
|
||||
apiKey: string;
|
||||
enabled: boolean;
|
||||
syncInterval: number;
|
||||
lastSync: string;
|
||||
status: 'connected' | 'disconnected' | 'error';
|
||||
}
|
||||
|
||||
interface MarketConfig {
|
||||
tradingHours: {
|
||||
preMarket: string;
|
||||
open: string;
|
||||
close: string;
|
||||
postMarket: string;
|
||||
};
|
||||
dataRetention: number;
|
||||
enablePreMarket: boolean;
|
||||
enableAfterHours: boolean;
|
||||
}
|
||||
|
||||
export default function DataSourceConfig() {
|
||||
const [sources, setSources] = useState<DataSource[]>([
|
||||
{
|
||||
id: '1',
|
||||
name: 'AKShare 官方',
|
||||
type: 'akshare',
|
||||
url: 'http://localhost:8000',
|
||||
apiKey: '',
|
||||
enabled: true,
|
||||
syncInterval: 5,
|
||||
lastSync: '2024-03-07 14:30:00',
|
||||
status: 'connected',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Tushare Pro',
|
||||
type: 'tushare',
|
||||
url: 'https://api.tushare.pro',
|
||||
apiKey: 'ts_xxxxxxxxxxxxxxxx',
|
||||
enabled: false,
|
||||
syncInterval: 15,
|
||||
lastSync: '-',
|
||||
status: 'disconnected',
|
||||
},
|
||||
]);
|
||||
|
||||
const [marketConfig, setMarketConfig] = useState<MarketConfig>({
|
||||
tradingHours: {
|
||||
preMarket: '09:15',
|
||||
open: '09:30',
|
||||
close: '15:00',
|
||||
postMarket: '15:30',
|
||||
},
|
||||
dataRetention: 365,
|
||||
enablePreMarket: true,
|
||||
enableAfterHours: false,
|
||||
});
|
||||
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [testing, setTesting] = useState<string | null>(null);
|
||||
|
||||
const handleTestConnection = async (sourceId: string) => {
|
||||
setTesting(sourceId);
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
setSources(prev => prev.map(s =>
|
||||
s.id === sourceId
|
||||
? { ...s, status: Math.random() > 0.3 ? 'connected' : 'error' }
|
||||
: s
|
||||
));
|
||||
setTesting(null);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
const handleToggleSource = (sourceId: string) => {
|
||||
setSources(prev => prev.map(s =>
|
||||
s.id === sourceId ? { ...s, enabled: !s.enabled } : s
|
||||
));
|
||||
};
|
||||
|
||||
const handleUpdateSource = (sourceId: string, updates: Partial<DataSource>) => {
|
||||
setSources(prev => prev.map(s =>
|
||||
s.id === sourceId ? { ...s, ...updates } : s
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">数据源配置</h1>
|
||||
<p className="text-[#b0b0b0] mt-1">配置数据来源和同步参数</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-[#ff6b35] text-white rounded-lg hover:bg-[#ff6b35]/90 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{saving ? (
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Save className="w-4 h-4" />
|
||||
)}
|
||||
保存配置
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Data Sources */}
|
||||
<div className="space-y-4">
|
||||
{sources.map((source, index) => (
|
||||
<motion.div
|
||||
key={source.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
className="bg-[#111111] border border-[#2a2a2a] rounded-xl p-5"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
|
||||
source.type === 'akshare' ? 'bg-green-500/20' :
|
||||
source.type === 'tushare' ? 'bg-blue-500/20' :
|
||||
'bg-purple-500/20'
|
||||
}`}>
|
||||
<Database className={`w-5 h-5 ${
|
||||
source.type === 'akshare' ? 'text-green-400' :
|
||||
source.type === 'tushare' ? 'text-blue-400' :
|
||||
'text-purple-400'
|
||||
}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-white font-medium">{source.name}</h3>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${
|
||||
source.status === 'connected' ? 'bg-green-500/20 text-green-400' :
|
||||
source.status === 'error' ? 'bg-red-500/20 text-red-400' :
|
||||
'bg-gray-500/20 text-gray-400'
|
||||
}`}>
|
||||
{source.status === 'connected' ? '已连接' :
|
||||
source.status === 'error' ? '连接错误' : '未连接'}
|
||||
</span>
|
||||
<span className="text-xs text-[#666]">
|
||||
最后同步: {source.lastSync}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleTestConnection(source.id)}
|
||||
disabled={testing === source.id}
|
||||
className="px-3 py-1.5 text-sm bg-[#2a2a2a] text-white rounded-lg hover:bg-[#333] transition-colors disabled:opacity-50"
|
||||
>
|
||||
{testing === source.id ? (
|
||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
'测试连接'
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleToggleSource(source.id)}
|
||||
className={`w-12 h-6 rounded-full transition-colors relative ${
|
||||
source.enabled ? 'bg-[#ff6b35]' : 'bg-[#2a2a2a]'
|
||||
}`}
|
||||
>
|
||||
<span className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-all ${
|
||||
source.enabled ? 'left-7' : 'left-1'
|
||||
}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-[#b0b0b0] mb-2">API URL</label>
|
||||
<div className="relative">
|
||||
<Globe className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#666]" />
|
||||
<input
|
||||
type="text"
|
||||
value={source.url}
|
||||
onChange={(e) => handleUpdateSource(source.id, { url: e.target.value })}
|
||||
className="w-full bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg pl-10 pr-4 py-2 text-white outline-none focus:border-[#ff6b35]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-[#b0b0b0] mb-2">API Key</label>
|
||||
<div className="relative">
|
||||
<Shield className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#666]" />
|
||||
<input
|
||||
type="password"
|
||||
value={source.apiKey}
|
||||
onChange={(e) => handleUpdateSource(source.id, { apiKey: e.target.value })}
|
||||
placeholder={source.type === 'akshare' ? '无需 API Key' : '输入 API Key'}
|
||||
className="w-full bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg pl-10 pr-4 py-2 text-white placeholder-[#666] outline-none focus:border-[#ff6b35]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-[#b0b0b0] mb-2">同步间隔 (分钟)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={source.syncInterval}
|
||||
onChange={(e) => handleUpdateSource(source.id, { syncInterval: parseInt(e.target.value) })}
|
||||
min="1"
|
||||
max="60"
|
||||
className="w-full bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-4 py-2 text-white outline-none focus:border-[#ff6b35]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-[#b0b0b0] mb-2">数据源类型</label>
|
||||
<select
|
||||
value={source.type}
|
||||
onChange={(e) => handleUpdateSource(source.id, { type: e.target.value as DataSource['type'] })}
|
||||
className="w-full bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-4 py-2 text-white outline-none focus:border-[#ff6b35]"
|
||||
>
|
||||
<option value="akshare">AKShare</option>
|
||||
<option value="tushare">Tushare</option>
|
||||
<option value="custom">自定义</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
{/* Add New Source Button */}
|
||||
<button className="w-full py-4 border-2 border-dashed border-[#2a2a2a] rounded-xl text-[#b0b0b0] hover:border-[#ff6b35] hover:text-[#ff6b35] transition-colors flex items-center justify-center gap-2">
|
||||
<Database className="w-5 h-5" />
|
||||
添加新数据源
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Market Config */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="bg-[#111111] border border-[#2a2a2a] rounded-xl p-5"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Clock className="w-5 h-5 text-[#ff6b35]" />
|
||||
<h2 className="text-white font-semibold">市场配置</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Trading Hours */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-white font-medium">交易时间</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-[#b0b0b0] mb-2">开盘时间</label>
|
||||
<input
|
||||
type="time"
|
||||
value={marketConfig.tradingHours.open}
|
||||
onChange={(e) => setMarketConfig(prev => ({
|
||||
...prev,
|
||||
tradingHours: { ...prev.tradingHours, open: e.target.value }
|
||||
}))}
|
||||
className="w-full bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-4 py-2 text-white outline-none focus:border-[#ff6b35]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-[#b0b0b0] mb-2">收盘时间</label>
|
||||
<input
|
||||
type="time"
|
||||
value={marketConfig.tradingHours.close}
|
||||
onChange={(e) => setMarketConfig(prev => ({
|
||||
...prev,
|
||||
tradingHours: { ...prev.tradingHours, close: e.target.value }
|
||||
}))}
|
||||
className="w-full bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-4 py-2 text-white outline-none focus:border-[#ff6b35]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 bg-[#0a0a0a] rounded-lg">
|
||||
<span className="text-white">启用盘前数据</span>
|
||||
<button
|
||||
onClick={() => setMarketConfig(prev => ({ ...prev, enablePreMarket: !prev.enablePreMarket }))}
|
||||
className={`w-12 h-6 rounded-full transition-colors relative ${
|
||||
marketConfig.enablePreMarket ? 'bg-[#ff6b35]' : 'bg-[#2a2a2a]'
|
||||
}`}
|
||||
>
|
||||
<span className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-all ${
|
||||
marketConfig.enablePreMarket ? 'left-7' : 'left-1'
|
||||
}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-3 bg-[#0a0a0a] rounded-lg">
|
||||
<span className="text-white">启用盘后数据</span>
|
||||
<button
|
||||
onClick={() => setMarketConfig(prev => ({ ...prev, enableAfterHours: !prev.enableAfterHours }))}
|
||||
className={`w-12 h-6 rounded-full transition-colors relative ${
|
||||
marketConfig.enableAfterHours ? 'bg-[#ff6b35]' : 'bg-[#2a2a2a]'
|
||||
}`}
|
||||
>
|
||||
<span className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-all ${
|
||||
marketConfig.enableAfterHours ? 'left-7' : 'left-1'
|
||||
}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Data Retention */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-white font-medium">数据保留</h3>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-[#b0b0b0] mb-2">
|
||||
历史数据保留天数: {marketConfig.dataRetention} 天
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="30"
|
||||
max="1095"
|
||||
step="30"
|
||||
value={marketConfig.dataRetention}
|
||||
onChange={(e) => setMarketConfig(prev => ({
|
||||
...prev,
|
||||
dataRetention: parseInt(e.target.value)
|
||||
}))}
|
||||
className="w-full accent-[#ff6b35]"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-[#666] mt-1">
|
||||
<span>30天</span>
|
||||
<span>1年</span>
|
||||
<span>3年</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-blue-500/10 border border-blue-500/30 rounded-lg">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-blue-400 font-medium">数据保留策略</p>
|
||||
<p className="text-sm text-blue-300/80 mt-1">
|
||||
超过保留期的历史数据将被自动归档或删除。建议至少保留 1 年的数据用于分析。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,113 @@
|
||||
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
|
||||
import { userApi } from '@/services/api';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
login: (email: string, password: string) => Promise<void>;
|
||||
register: (username: string, email: string, password: string) => Promise<void>;
|
||||
logout: () => void;
|
||||
fetchProfile: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// 检查本地存储的 token 并获取用户信息
|
||||
useEffect(() => {
|
||||
const initAuth = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
try {
|
||||
await fetchProfile();
|
||||
} catch (error) {
|
||||
// Token 无效,清除存储
|
||||
localStorage.removeItem('token');
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
initAuth();
|
||||
}, []);
|
||||
|
||||
const fetchProfile = async () => {
|
||||
try {
|
||||
const profile = await userApi.getProfile();
|
||||
setUser(profile);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch profile:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const login = async (email: string, password: string) => {
|
||||
try {
|
||||
const response = await userApi.login({ email, password });
|
||||
localStorage.setItem('token', response.token);
|
||||
setUser({
|
||||
id: response.id,
|
||||
username: response.username,
|
||||
email: response.email,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const register = async (username: string, email: string, password: string) => {
|
||||
try {
|
||||
const response = await userApi.register({ username, email, password });
|
||||
localStorage.setItem('token', response.token);
|
||||
setUser({
|
||||
id: response.id,
|
||||
username: response.username,
|
||||
email: response.email,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Register failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('token');
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
user,
|
||||
isAuthenticated: !!user,
|
||||
isLoading,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
fetchProfile,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@ -1,10 +1,20 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom'
|
||||
import { AuthProvider } from '@/contexts/AuthContext'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
import Admin from './admin/Admin.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<BrowserRouter>
|
||||
<AuthProvider>
|
||||
<Routes>
|
||||
<Route path="/" element={<App />} />
|
||||
<Route path="/admin/*" element={<Admin />} />
|
||||
</Routes>
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
Loading…
Reference in new issue