Compare commits
10 Commits
2ddd9f2856
...
41814a2bc8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41814a2bc8 | ||
|
|
721770f70a | ||
|
|
69c3546ac0 | ||
|
|
1b90af57ba | ||
|
|
910e2d4c4d | ||
|
|
7865b0da7f | ||
|
|
3af8fd6e5e | ||
|
|
c4014499e3 | ||
|
|
92807c8554 | ||
|
|
44ee8da1ed |
96
README.md
96
README.md
@@ -7,7 +7,11 @@
|
|||||||
- 🎯 **TCP包抓取** - 实时抓取游戏客户端的TCP通信数据
|
- 🎯 **TCP包抓取** - 实时抓取游戏客户端的TCP通信数据
|
||||||
- 🔍 **数据解析** - 自动解析十六进制数据为可读的装备信息
|
- 🔍 **数据解析** - 自动解析十六进制数据为可读的装备信息
|
||||||
- 📊 **数据可视化** - 现代化的React界面展示装备数据
|
- 📊 **数据可视化** - 现代化的React界面展示装备数据
|
||||||
- 💾 **数据导出** - 支持导出为JSON格式
|
- 💾 **数据持久化** - 基于SQLite的本地数据存储,支持装备数据的增删改查
|
||||||
|
- 📤 **数据导出** - 支持导出为JSON格式
|
||||||
|
- 🔍 **数据搜索** - 支持装备数据的模糊搜索和筛选
|
||||||
|
- 📈 **数据统计** - 提供装备数据的统计分析功能
|
||||||
|
- ⚙️ **设置管理** - 应用设置的本地持久化存储
|
||||||
- 🚀 **高性能** - 基于Go语言的高性能网络处理
|
- 🚀 **高性能** - 基于Go语言的高性能网络处理
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
@@ -17,6 +21,7 @@
|
|||||||
- **Wails v2** - 桌面应用框架
|
- **Wails v2** - 桌面应用框架
|
||||||
- **gopacket** - 网络包抓取
|
- **gopacket** - 网络包抓取
|
||||||
- **zap** - 结构化日志
|
- **zap** - 结构化日志
|
||||||
|
- **SQLite** - 本地数据存储
|
||||||
|
|
||||||
### 前端
|
### 前端
|
||||||
- **React 18** - 用户界面框架
|
- **React 18** - 用户界面框架
|
||||||
@@ -42,6 +47,7 @@ cd equipment-analyzer
|
|||||||
|
|
||||||
2. **安装后端依赖**
|
2. **安装后端依赖**
|
||||||
```bash
|
```bash
|
||||||
|
go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||||
go mod tidy
|
go mod tidy
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -108,6 +114,56 @@ equipment-analyzer/
|
|||||||
|
|
||||||
## 开发指南
|
## 开发指南
|
||||||
|
|
||||||
|
### 常见问题解决
|
||||||
|
|
||||||
|
#### 1. React严格模式导致的重复调用
|
||||||
|
在开发模式下,React严格模式会导致组件渲染两次,这可能导致:
|
||||||
|
- useEffect被重复执行
|
||||||
|
- 异步操作被重复调用
|
||||||
|
- 用户提示消息重复显示
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
- **全局消息去重配置**:在`frontend/src/utils/messageConfig.ts`中配置Antd message的全局设置
|
||||||
|
- **防重复机制**:使用缓存机制防止2秒内相同消息重复显示
|
||||||
|
- **最大显示数量限制**:设置`maxCount: 1`确保同时只显示一个消息
|
||||||
|
|
||||||
|
**全局配置:**
|
||||||
|
```typescript
|
||||||
|
// frontend/src/utils/messageConfig.ts
|
||||||
|
import { message } from 'antd';
|
||||||
|
|
||||||
|
// 配置全局消息设置,防止重复显示
|
||||||
|
message.config({
|
||||||
|
maxCount: 1, // 最多同时显示1个消息
|
||||||
|
duration: 3, // 消息显示3秒
|
||||||
|
top: 24, // 距离顶部24px
|
||||||
|
});
|
||||||
|
|
||||||
|
// 防重复消息函数
|
||||||
|
const messageCache = new Map<string, number>();
|
||||||
|
const DEBOUNCE_TIME = 2000; // 2秒内相同消息不重复显示
|
||||||
|
|
||||||
|
export const showMessage = (type: 'success' | 'error' | 'warning' | 'info', content: string) => {
|
||||||
|
const messageId = `${type}:${content}`;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// 检查是否在防抖时间内
|
||||||
|
const lastTime = messageCache.get(messageId);
|
||||||
|
if (lastTime && now - lastTime < DEBOUNCE_TIME) {
|
||||||
|
return; // 跳过重复消息
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新缓存并显示消息
|
||||||
|
messageCache.set(messageId, now);
|
||||||
|
message[type](content);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用方法:**
|
||||||
|
- 在`main.tsx`中导入配置:`import './utils/messageConfig'`
|
||||||
|
- 在组件中正常使用`message.success()`、`message.error()`等
|
||||||
|
- 全局配置会自动防止重复消息显示
|
||||||
|
|
||||||
### 添加新功能
|
### 添加新功能
|
||||||
|
|
||||||
1. **后端功能**
|
1. **后端功能**
|
||||||
@@ -120,6 +176,23 @@ equipment-analyzer/
|
|||||||
- 使用TypeScript确保类型安全
|
- 使用TypeScript确保类型安全
|
||||||
- 遵循Ant Design设计规范
|
- 遵循Ant Design设计规范
|
||||||
|
|
||||||
|
### 数据库操作
|
||||||
|
|
||||||
|
1. **添加新的数据表**
|
||||||
|
- 在`internal/model/database.go`的`initTables()`方法中添加表结构
|
||||||
|
- 在`Database`结构体中添加相应的CRUD方法
|
||||||
|
- 在`DatabaseService`中添加业务逻辑方法
|
||||||
|
- 在`App`中添加API接口方法
|
||||||
|
|
||||||
|
2. **数据迁移**
|
||||||
|
- 数据库结构变更时,在`initTables()`方法中处理版本升级
|
||||||
|
- 使用事务确保数据一致性
|
||||||
|
|
||||||
|
3. **性能优化**
|
||||||
|
- 为常用查询字段添加索引
|
||||||
|
- 使用批量操作减少数据库交互
|
||||||
|
- 合理使用连接池和事务
|
||||||
|
|
||||||
### 测试
|
### 测试
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -146,8 +219,27 @@ make release
|
|||||||
make clean
|
make clean
|
||||||
```
|
```
|
||||||
|
|
||||||
## 配置说明
|
## 数据存储
|
||||||
|
|
||||||
|
### 数据库存储
|
||||||
|
应用使用SQLite数据库进行本地数据持久化,数据库文件位置:`~/.equipment-analyzer/equipment_analyzer.db`
|
||||||
|
|
||||||
|
#### 数据库表结构
|
||||||
|
- **equipment** - 装备基本信息表
|
||||||
|
- **equipment_operations** - 装备操作属性表
|
||||||
|
- **capture_sessions** - 抓包会话记录表
|
||||||
|
- **raw_packet_data** - 原始数据包表
|
||||||
|
- **app_settings** - 应用设置表
|
||||||
|
|
||||||
|
#### 数据库功能
|
||||||
|
- ✅ 装备数据的增删改查
|
||||||
|
- ✅ 批量数据导入导出
|
||||||
|
- ✅ 装备数据搜索和筛选
|
||||||
|
- ✅ 数据统计分析
|
||||||
|
- ✅ 应用设置持久化
|
||||||
|
- ✅ 抓包会话历史记录
|
||||||
|
|
||||||
|
### 配置文件
|
||||||
应用会在用户目录下创建配置文件:`~/.equipment-analyzer/config.json`
|
应用会在用户目录下创建配置文件:`~/.equipment-analyzer/config.json`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import React, {useEffect, useState, useRef} from 'react'
|
import React from 'react'
|
||||||
import {Button, Card, Layout, message, Select, Spin, Table} from 'antd'
|
import {Layout, Menu} from 'antd'
|
||||||
import {DownloadOutlined, PlayCircleOutlined, SettingOutlined, StopOutlined} from '@ant-design/icons'
|
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import {ExportData, GetCapturedData,GetNetworkInterfaces,
|
import {BrowserRouter as Router, Link, Route, Routes, useLocation} from 'react-router-dom';
|
||||||
ParseData,StartCapture,StopCapture, ReadRawJsonFile
|
|
||||||
} from "../wailsjs/go/service/App";
|
|
||||||
import { BrowserRouter as Router, Routes, Route, Link, useLocation } from 'react-router-dom';
|
|
||||||
import { Menu } from 'antd';
|
|
||||||
import CapturePage from './pages/CapturePage';
|
import CapturePage from './pages/CapturePage';
|
||||||
import OptimizerPage from './pages/OptimizerPage';
|
import OptimizerPage from './pages/OptimizerPage';
|
||||||
|
import DatabasePage from './pages/DatabasePage';
|
||||||
|
|
||||||
const {Header, Content, Sider} = Layout
|
const {Header, Content, Sider} = Layout
|
||||||
|
|
||||||
@@ -51,6 +47,9 @@ function AppContent() {
|
|||||||
<Menu.Item key="/">
|
<Menu.Item key="/">
|
||||||
<Link to="/">抓包</Link>
|
<Link to="/">抓包</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item key="/database">
|
||||||
|
<Link to="/database">数据库</Link>
|
||||||
|
</Menu.Item>
|
||||||
<Menu.Item key="/optimizer">
|
<Menu.Item key="/optimizer">
|
||||||
<Link to="/optimizer">配装优化</Link>
|
<Link to="/optimizer">配装优化</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
@@ -59,6 +58,7 @@ function AppContent() {
|
|||||||
<Content style={{ padding: 0, minHeight: 280 }}>
|
<Content style={{ padding: 0, minHeight: 280 }}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<CapturePage />} />
|
<Route path="/" element={<CapturePage />} />
|
||||||
|
<Route path="/database" element={<DatabasePage />} />
|
||||||
<Route path="/optimizer" element={<OptimizerPage />} />
|
<Route path="/optimizer" element={<OptimizerPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Content>
|
</Content>
|
||||||
|
|||||||
@@ -48,10 +48,6 @@ button {
|
|||||||
button:hover {
|
button:hover {
|
||||||
border-color: #646cff;
|
border-color: #646cff;
|
||||||
}
|
}
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import React, {useEffect, useRef, useState} from 'react';
|
import React, {useEffect, useRef, useState} from 'react';
|
||||||
import {Button, Card, Layout, message, Select, Spin, Table} from 'antd';
|
import {Button, Card, Layout, message, Select, Spin, Table} from 'antd';
|
||||||
import {DownloadOutlined, PlayCircleOutlined, SettingOutlined, StopOutlined} from '@ant-design/icons';
|
import {DownloadOutlined, PlayCircleOutlined, StopOutlined, UploadOutlined} from '@ant-design/icons';
|
||||||
import '../App.css';
|
import '../App.css';
|
||||||
import {
|
import {
|
||||||
ExportData,
|
|
||||||
GetCapturedData,
|
|
||||||
GetNetworkInterfaces,
|
GetNetworkInterfaces,
|
||||||
ParseData,
|
|
||||||
ReadRawJsonFile,
|
ReadRawJsonFile,
|
||||||
|
SaveParsedDataToDatabase,
|
||||||
StartCapture,
|
StartCapture,
|
||||||
StopCapture
|
StopAndParseCapture
|
||||||
} from '../../wailsjs/go/service/App';
|
} from '../../wailsjs/go/service/App';
|
||||||
import {useCaptureStore} from '../store/useCaptureStore';
|
import {useCaptureStore} from '../store/useCaptureStore';
|
||||||
|
|
||||||
@@ -64,36 +62,36 @@ function CapturePage() {
|
|||||||
|
|
||||||
const safeApiCall = async <T,>(
|
const safeApiCall = async <T,>(
|
||||||
apiCall: () => Promise<T>,
|
apiCall: () => Promise<T>,
|
||||||
errorMessage: string,
|
errorMessage: string
|
||||||
fallbackValue: T
|
): Promise<T | undefined> => {
|
||||||
): Promise<T> => {
|
|
||||||
try {
|
try {
|
||||||
return await apiCall();
|
return await apiCall();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`${errorMessage}:`, error);
|
console.error(`${errorMessage}:`, error);
|
||||||
showMessage('error', errorMessage);
|
showMessage('error', errorMessage);
|
||||||
return fallbackValue;
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchInterfaces = async () => {
|
const fetchInterfaces = async () => {
|
||||||
setIsCapturing(false);
|
setIsCapturing(false);
|
||||||
setCapturedData([]);
|
setCapturedData([]);
|
||||||
setParsedData(null);
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setInterfaceLoading(true);
|
setInterfaceLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await safeApiCall(
|
const response = await safeApiCall(
|
||||||
() => GetNetworkInterfaces(),
|
() => GetNetworkInterfaces(),
|
||||||
'获取网络接口失败,使用模拟数据',
|
'获取网络接口失败'
|
||||||
[
|
|
||||||
{ name: 'eth0', description: 'Ethernet', addresses: ['192.168.1.100'], is_loopback: false },
|
|
||||||
{ name: 'wlan0', description: 'Wireless', addresses: ['192.168.1.101'], is_loopback: false },
|
|
||||||
{ name: 'lo', description: 'Loopback', addresses: ['127.0.0.1'], is_loopback: true }
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
if (!response || response.length === 0) {
|
||||||
|
showMessage('error', '未获取到任何网络接口');
|
||||||
|
setInterfaces([]);
|
||||||
|
setSelectedInterface('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
setInterfaces(response);
|
setInterfaces(response);
|
||||||
let defaultSelected = '';
|
let defaultSelected = '';
|
||||||
|
console.log("获取的网卡:"+JSON.stringify(response))
|
||||||
for (const iface of response) {
|
for (const iface of response) {
|
||||||
if (iface.addresses && iface.addresses.some(ip => ip.includes('192.168'))) {
|
if (iface.addresses && iface.addresses.some(ip => ip.includes('192.168'))) {
|
||||||
defaultSelected = iface.name;
|
defaultSelected = iface.name;
|
||||||
@@ -106,13 +104,9 @@ function CapturePage() {
|
|||||||
setSelectedInterface(defaultSelected);
|
setSelectedInterface(defaultSelected);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取网络接口时发生未知错误:', error);
|
console.error('获取网络接口时发生未知错误:', error);
|
||||||
const defaultInterfaces = [
|
showMessage('error', '获取网络接口时发生未知错误');
|
||||||
{ name: 'eth0', description: 'Ethernet', addresses: ['192.168.1.100'], is_loopback: false },
|
setInterfaces([]);
|
||||||
{ name: 'wlan0', description: 'Wireless', addresses: ['192.168.1.101'], is_loopback: false },
|
setSelectedInterface('');
|
||||||
{ name: 'lo', description: 'Loopback', addresses: ['127.0.0.1'], is_loopback: true }
|
|
||||||
];
|
|
||||||
setInterfaces(defaultInterfaces);
|
|
||||||
setSelectedInterface(defaultInterfaces[0].name);
|
|
||||||
} finally {
|
} finally {
|
||||||
setInterfaceLoading(false);
|
setInterfaceLoading(false);
|
||||||
}
|
}
|
||||||
@@ -125,17 +119,20 @@ function CapturePage() {
|
|||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await safeApiCall(
|
const result = await safeApiCall(
|
||||||
() => StartCapture(selectedInterface),
|
() => StartCapture(selectedInterface),
|
||||||
'开始抓包失败,但界面将继续工作',
|
'开始抓包失败'
|
||||||
undefined
|
|
||||||
);
|
);
|
||||||
|
if (result === undefined) {
|
||||||
|
setIsCapturing(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsCapturing(true);
|
setIsCapturing(true);
|
||||||
showMessage('success', '开始抓包');
|
showMessage('success', '开始抓包');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('开始抓包时发生未知错误:', error);
|
console.error('开始抓包时发生未知错误:', error);
|
||||||
setIsCapturing(true);
|
setIsCapturing(false);
|
||||||
showMessage('success', '开始抓包(模拟模式)');
|
showMessage('error', '开始抓包失败');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -145,69 +142,26 @@ function CapturePage() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
setIsCapturing(false);
|
setIsCapturing(false);
|
||||||
setCapturedData([]);
|
setCapturedData([]);
|
||||||
setParsedData(null);
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await safeApiCall(
|
// 新接口:直接停止抓包并解析
|
||||||
() => StopCapture(),
|
const parsedData = await safeApiCall(
|
||||||
'停止抓包失败,但界面将继续工作',
|
() => StopAndParseCapture(),
|
||||||
undefined
|
'停止抓包并解析数据失败'
|
||||||
);
|
);
|
||||||
setIsCapturing(false);
|
console.log("解析数据:"+JSON.stringify(parsedData))
|
||||||
const data = await safeApiCall(
|
if (!parsedData || !Array.isArray((parsedData as CaptureResult).items)) {
|
||||||
() => GetCapturedData(),
|
setParsedData({ items: [], heroes: [] } as CaptureResult);
|
||||||
'获取抓包数据失败,使用模拟数据',
|
showMessage('error', '解析数据失败');
|
||||||
['mock_data_1', 'mock_data_2', 'mock_data_3']
|
return;
|
||||||
);
|
|
||||||
setCapturedData(data);
|
|
||||||
if (data && data.length > 0) {
|
|
||||||
data.forEach((item, idx) => {
|
|
||||||
let hexStr = '';
|
|
||||||
if (/^[0-9a-fA-F\s]+$/.test(item)) {
|
|
||||||
hexStr = item;
|
|
||||||
} else {
|
|
||||||
hexStr = Array.from(item).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join(' ');
|
|
||||||
}
|
}
|
||||||
console.log(`抓包数据[${idx}]:`, hexStr);
|
setParsedData(parsedData as CaptureResult);
|
||||||
});
|
|
||||||
}
|
|
||||||
if (data.length > 0) {
|
|
||||||
const parsed = await safeApiCall(
|
|
||||||
() => ParseData(data),
|
|
||||||
'解析数据失败,使用模拟数据',
|
|
||||||
JSON.stringify({
|
|
||||||
items: [
|
|
||||||
{ id: '1', code: 'SWORD001', ct: 100, e: 1500, g: 5, l: false, mg: 10, op: [], p: 25, s: 'Legendary', sk: 3 },
|
|
||||||
{ id: '2', code: 'SHIELD002', ct: 80, e: 1200, g: 4, l: true, mg: 15, op: [], p: 20, s: 'Rare', sk: 2 },
|
|
||||||
{ id: '3', code: 'HELMET003', ct: 60, e: 800, g: 3, l: false, mg: 8, op: [], p: 12, s: 'Common', sk: 1 }
|
|
||||||
],
|
|
||||||
heroes: []
|
|
||||||
})
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
const parsedData = JSON.parse(parsed);
|
|
||||||
setParsedData(parsedData);
|
|
||||||
showMessage('success', '数据处理完成');
|
showMessage('success', '数据处理完成');
|
||||||
} catch (parseError) {
|
|
||||||
console.error('解析JSON失败:', parseError);
|
|
||||||
setParsedData({
|
|
||||||
items: [
|
|
||||||
{ id: '1', code: 'SWORD001', ct: 100, e: 1500, g: 5, l: false, mg: 10, op: [], p: 25, s: 'Legendary', sk: 3 },
|
|
||||||
{ id: '2', code: 'SHIELD002', ct: 80, e: 1200, g: 4, l: true, mg: 15, op: [], p: 20, s: 'Rare', sk: 2 },
|
|
||||||
{ id: '3', code: 'HELMET003', ct: 60, e: 800, g: 3, l: false, mg: 8, op: [], p: 12, s: 'Common', sk: 1 }
|
|
||||||
],
|
|
||||||
heroes: []
|
|
||||||
});
|
|
||||||
showMessage('success', '数据处理完成(使用模拟数据)');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showMessage('warning', '未捕获到数据');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('停止抓包时发生未知错误:', error);
|
console.error('停止抓包时发生未知错误:', error);
|
||||||
setIsCapturing(false);
|
setIsCapturing(false);
|
||||||
setCapturedData([]);
|
setCapturedData([]);
|
||||||
setParsedData(null);
|
setParsedData({ items: [], heroes: [] } as CaptureResult);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
showMessage('error', '抓包失败,已重置状态');
|
showMessage('error', '抓包失败,已重置状态');
|
||||||
return;
|
return;
|
||||||
@@ -216,47 +170,102 @@ function CapturePage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportData = async () => {
|
const exportData = () => {
|
||||||
if (!capturedData.length) {
|
// 检查是否有解析数据
|
||||||
|
if (!parsedData || !Array.isArray(parsedData.items) || parsedData.items.length === 0) {
|
||||||
showMessage('warning', '没有数据可导出');
|
showMessage('warning', '没有数据可导出');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const filename = `equipment_data_${Date.now()}.json`;
|
// 创建要导出的数据内容
|
||||||
await safeApiCall(
|
const exportContent = JSON.stringify(parsedData, null, 2);
|
||||||
() => ExportData(capturedData, filename),
|
|
||||||
'导出数据失败',
|
// 创建 Blob 对象
|
||||||
undefined
|
const blob = new Blob([exportContent], { type: 'text/plain;charset=utf-8' });
|
||||||
);
|
|
||||||
|
// 创建下载链接
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = 'gear.txt';
|
||||||
|
|
||||||
|
// 触发下载
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
showMessage('success', '数据导出成功');
|
showMessage('success', '数据导出成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('导出数据时发生未知错误:', error);
|
console.error('导出数据时发生错误:', error);
|
||||||
showMessage('success', '数据导出成功(模拟模式)');
|
showMessage('error', '导出数据失败');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
setUploadedFileName(file.name);
|
setUploadedFileName(file.name);
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = async (e) => {
|
||||||
try {
|
try {
|
||||||
const text = e.target?.result as string;
|
const text = e.target?.result as string;
|
||||||
const json = JSON.parse(text);
|
const json = JSON.parse(text);
|
||||||
|
|
||||||
|
// 校验数据格式
|
||||||
|
if (!json || typeof json !== 'object') {
|
||||||
|
showMessage('error', '文件格式错误:不是有效的JSON对象');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!json.heroes || !Array.isArray(json.heroes)) {
|
||||||
|
showMessage('error', '数据格式错误:缺少heroes数组');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!json.items || !Array.isArray(json.items)) {
|
||||||
|
showMessage('error', '数据格式错误:缺少items数组');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.heroes.length === 0) {
|
||||||
|
showMessage('error', '数据格式错误:heroes数组不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.items.length === 0) {
|
||||||
|
showMessage('error', '数据格式错误:items数组不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据校验通过,保存到数据库
|
||||||
|
const sessionName = `import_${Date.now()}`;
|
||||||
|
const equipmentJSON = JSON.stringify(json.items);
|
||||||
|
const heroesJSON = JSON.stringify(json.heroes);
|
||||||
|
|
||||||
|
await SaveParsedDataToDatabase(sessionName, equipmentJSON, heroesJSON);
|
||||||
|
|
||||||
|
// 更新界面显示
|
||||||
const safeData = {
|
const safeData = {
|
||||||
items: Array.isArray(json.items) ? json.items : [],
|
items: json.items,
|
||||||
heroes: Array.isArray(json.heroes) ? json.heroes : []
|
heroes: json.heroes
|
||||||
};
|
};
|
||||||
setParsedData(safeData);
|
setParsedData(safeData);
|
||||||
if (safeData.items.length === 0 && safeData.heroes.length === 0) {
|
setUploadedFileName(''); // 清空文件名显示
|
||||||
showMessage('warning', '上传文件数据为空,请检查文件内容');
|
|
||||||
} else {
|
showMessage('success', `数据导入成功:${json.items.length}件装备,${json.heroes.length}个英雄,已保存到数据库`);
|
||||||
showMessage('success', `文件解析成功:${safeData.items.length}件装备,${safeData.heroes.length}个英雄`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('文件格式错误,无法解析:', err);
|
console.error('文件处理错误:', err);
|
||||||
showMessage('error', '文件格式错误,无法解析');
|
if (err instanceof SyntaxError) {
|
||||||
|
showMessage('error', '文件格式错误:不是有效的JSON格式');
|
||||||
|
} else {
|
||||||
|
showMessage('error', '数据导入失败');
|
||||||
|
}
|
||||||
setParsedData({ items: [], heroes: [] });
|
setParsedData({ items: [], heroes: [] });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -266,8 +275,7 @@ function CapturePage() {
|
|||||||
const fetchParsedDataFromBackend = async () => {
|
const fetchParsedDataFromBackend = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const raw = await ReadRawJsonFile();
|
const json = await ReadRawJsonFile();
|
||||||
const json = JSON.parse(raw);
|
|
||||||
console.log('已加载本地解析数据:', json);
|
console.log('已加载本地解析数据:', json);
|
||||||
const safeData = {
|
const safeData = {
|
||||||
items: Array.isArray(json.items) ? json.items : [],
|
items: Array.isArray(json.items) ? json.items : [],
|
||||||
@@ -347,39 +355,10 @@ function CapturePage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{ minHeight: '100vh' }}>
|
|
||||||
<Header style={{ background: '#fff', padding: '0 16px', height: 48, lineHeight: '48px' }}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', height: 48 }}>
|
|
||||||
<h1 style={{ margin: 0, fontSize: 20 }}>装备数据导出工具</h1>
|
|
||||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<SettingOutlined />}
|
|
||||||
onClick={handleRefreshParsedData}
|
|
||||||
loading={loading}
|
|
||||||
style={{ flex: 1, height: 32 }}
|
|
||||||
>刷新数据</Button>
|
|
||||||
<Button style={{ flex: 1, height: 32 }} onClick={handleUploadButtonClick}>
|
|
||||||
上传JSON
|
|
||||||
</Button>
|
|
||||||
<input
|
|
||||||
ref={fileInputRef}
|
|
||||||
type="file"
|
|
||||||
accept="application/json"
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
onChange={handleFileUpload}
|
|
||||||
/>
|
|
||||||
{uploadedFileName && (
|
|
||||||
<span style={{ marginLeft: 8, color: '#888', fontSize: 12 }}>{uploadedFileName}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Header>
|
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<Sider width={220} style={{ background: '#fff' }}>
|
<Sider width={220} style={{ background: '#f5f5f5' }}>
|
||||||
<div style={{ padding: '12px' }}>
|
<div style={{ padding: '16px 0px 12px 12px' }}>
|
||||||
<Card title="抓包控制" size="small">
|
<Card title="抓包控制" size="small" style={{ marginBottom: 12, marginTop: 0 }}>
|
||||||
<div style={{ marginBottom: 12 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<label>网络接口:</label>
|
<label>网络接口:</label>
|
||||||
<Select
|
<Select
|
||||||
@@ -423,19 +402,38 @@ function CapturePage() {
|
|||||||
<Button
|
<Button
|
||||||
icon={<DownloadOutlined />}
|
icon={<DownloadOutlined />}
|
||||||
onClick={exportData}
|
onClick={exportData}
|
||||||
disabled={!capturedData.length}
|
disabled={!parsedData || !Array.isArray(parsedData.items) || parsedData.items.length === 0}
|
||||||
style={{ width: '100%', height: 32, fontSize: 14 }}>
|
style={{ width: '100%', height: 32, fontSize: 14 }}>
|
||||||
导出数据
|
导出数据
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<UploadOutlined />}
|
||||||
|
onClick={handleUploadButtonClick}
|
||||||
|
style={{ width: '100%', height: 32, fontSize: 14 }}>
|
||||||
|
导入数据
|
||||||
|
</Button>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".json,.txt"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
/>
|
||||||
|
{uploadedFileName && (
|
||||||
|
<span style={{ marginTop: 4, color: '#888', fontSize: 10, textAlign: 'center', display: 'block' }}>
|
||||||
|
{uploadedFileName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card title="抓包状态" size="small" style={{ marginTop: 12 }}>
|
<Card title="抓包状态" size="small" style={{ marginTop: 12 }}>
|
||||||
<div>
|
<div>
|
||||||
<p style={{ marginBottom: 4 }}>状态: {isCapturing ? '正在抓包...' : '准备就绪'}</p>
|
<p style={{ marginBottom: 4 }}>状态: {isCapturing ? '正在抓包...' : '准备就绪'}</p>
|
||||||
<p style={{ marginBottom: 4 }}>捕获数据: {capturedData.length} 条</p>
|
{/*<p style={{ marginBottom: 4 }}>捕获数据: {capturedData.length} 条</p>*/}
|
||||||
|
<p style={{ marginBottom: 4 }}>英雄数目: {Array.isArray(parsedData?.heroes) ? parsedData.heroes.length : 0} 个</p>
|
||||||
{parsedData && (
|
{parsedData && (
|
||||||
<p style={{ marginBottom: 0 }}>解析装备: {parsedData.items.length} 件</p>
|
<p style={{ marginBottom: 0 }}>解析装备: {Array.isArray(parsedData?.items) ? parsedData.items.length : 0} 件</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -444,7 +442,7 @@ function CapturePage() {
|
|||||||
|
|
||||||
<Content style={{ padding: '16px' }}>
|
<Content style={{ padding: '16px' }}>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
{parsedData && parsedData.items.length > 0 ? (
|
{Array.isArray(parsedData?.items) && parsedData.items.length > 0 ? (
|
||||||
<Card title="装备数据">
|
<Card title="装备数据">
|
||||||
<Table
|
<Table
|
||||||
dataSource={parsedData.items}
|
dataSource={parsedData.items}
|
||||||
@@ -467,7 +465,6 @@ function CapturePage() {
|
|||||||
</Spin>
|
</Spin>
|
||||||
</Content>
|
</Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
207
frontend/src/pages/DatabasePage.tsx
Normal file
207
frontend/src/pages/DatabasePage.tsx
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
|
import {Button, Card, Col, Layout, Row, Space, Statistic, Table, Tag} from 'antd';
|
||||||
|
import {BarChartOutlined, DatabaseOutlined, ReloadOutlined, SettingOutlined,} from '@ant-design/icons';
|
||||||
|
import * as App from '../../wailsjs/go/service/App';
|
||||||
|
import {model} from '../../wailsjs/go/models';
|
||||||
|
import {useMessage} from '../utils/useMessage';
|
||||||
|
|
||||||
|
const { Content } = Layout;
|
||||||
|
|
||||||
|
// 定义Equipment接口
|
||||||
|
interface Equipment {
|
||||||
|
id: string | number;
|
||||||
|
code: string;
|
||||||
|
ct: number;
|
||||||
|
e: number;
|
||||||
|
g: number;
|
||||||
|
l: boolean;
|
||||||
|
mg: number;
|
||||||
|
op: any[];
|
||||||
|
p: number;
|
||||||
|
s: string;
|
||||||
|
sk: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DatabasePage: React.FC = () => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [latestData, setLatestData] = useState<model.ParsedResult | null>(null);
|
||||||
|
const { success, error, info } = useMessage();
|
||||||
|
|
||||||
|
// 加载最新解析数据
|
||||||
|
const loadLatestData = async () => {
|
||||||
|
// 防止重复调用
|
||||||
|
if (loading) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const parsedResult = await App.GetLatestParsedDataFromDatabase();
|
||||||
|
if (parsedResult && (parsedResult.items?.length > 0 || parsedResult.heroes?.length > 0)) {
|
||||||
|
setLatestData(parsedResult);
|
||||||
|
success('数据加载成功');
|
||||||
|
} else {
|
||||||
|
setLatestData(null);
|
||||||
|
info('暂无解析数据');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error('加载数据失败');
|
||||||
|
console.error('Load data error:', err);
|
||||||
|
setLatestData(null);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadLatestData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 装备表格列定义
|
||||||
|
const equipmentColumns = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
width: 80,
|
||||||
|
render: (id: any) => {
|
||||||
|
const idStr = String(id || '');
|
||||||
|
return <span style={{ fontSize: '12px' }}>{idStr.length > 8 ? `${idStr.slice(0, 8)}...` : idStr}</span>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '代码',
|
||||||
|
dataIndex: 'code',
|
||||||
|
key: 'code',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '等级',
|
||||||
|
dataIndex: 'g',
|
||||||
|
key: 'g',
|
||||||
|
width: 80,
|
||||||
|
render: (grade: number) => (
|
||||||
|
<Tag color={grade >= 5 ? 'red' : grade >= 3 ? 'orange' : 'green'}>
|
||||||
|
{grade}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '经验',
|
||||||
|
dataIndex: 'e',
|
||||||
|
key: 'e',
|
||||||
|
width: 100,
|
||||||
|
render: (exp: number) => exp.toLocaleString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '力量',
|
||||||
|
dataIndex: 'p',
|
||||||
|
key: 'p',
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '魔法',
|
||||||
|
dataIndex: 'mg',
|
||||||
|
key: 'mg',
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '技能',
|
||||||
|
dataIndex: 'sk',
|
||||||
|
key: 'sk',
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'l',
|
||||||
|
key: 'l',
|
||||||
|
width: 80,
|
||||||
|
render: (locked: boolean) => (
|
||||||
|
<Tag color={locked ? 'red' : 'green'}>
|
||||||
|
{locked ? '锁定' : '正常'}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout style={{ minHeight: '100vh' }}>
|
||||||
|
<Content style={{ padding: 16 }}>
|
||||||
|
{/* 统计卡片 */}
|
||||||
|
<Row gutter={16} style={{ marginBottom: '24px' }}>
|
||||||
|
<Col span={8}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="数据库状态"
|
||||||
|
value="SQLite"
|
||||||
|
prefix={<DatabaseOutlined />}
|
||||||
|
suffix="已连接"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="装备数量"
|
||||||
|
value={latestData?.items?.length || 0}
|
||||||
|
prefix={<BarChartOutlined />}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Card>
|
||||||
|
<Statistic
|
||||||
|
title="英雄数量"
|
||||||
|
value={latestData?.heroes?.length || 0}
|
||||||
|
prefix={<SettingOutlined />}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{/* 操作栏 */}
|
||||||
|
<Card style={{ marginBottom: '16px' }}>
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={loadLatestData}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
刷新数据
|
||||||
|
</Button>
|
||||||
|
<span style={{ color: '#666' }}>
|
||||||
|
显示最新一次抓包解析的数据
|
||||||
|
</span>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 装备表格 */}
|
||||||
|
<Card title="最新解析的装备数据">
|
||||||
|
{latestData?.items && latestData.items.length > 0 ? (
|
||||||
|
<Table
|
||||||
|
columns={equipmentColumns}
|
||||||
|
dataSource={latestData.items}
|
||||||
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
|
pagination={{
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total, range) =>
|
||||||
|
`第 ${range[0]}-${range[1]} 条,共 ${total} 条`,
|
||||||
|
}}
|
||||||
|
scroll={{ x: 800 }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||||
|
<p>暂无解析数据</p>
|
||||||
|
<p style={{ color: '#999', fontSize: '12px' }}>
|
||||||
|
请先进行抓包操作,解析后的数据会自动保存到数据库
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DatabasePage;
|
||||||
@@ -1,12 +1,243 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Col,
|
||||||
|
Divider,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
Layout,
|
||||||
|
Modal,
|
||||||
|
Pagination,
|
||||||
|
Row,
|
||||||
|
Select,
|
||||||
|
Table,
|
||||||
|
Tag
|
||||||
|
} from 'antd';
|
||||||
|
import {AppstoreOutlined, FilterOutlined, UserOutlined} from '@ant-design/icons';
|
||||||
|
|
||||||
|
const {Content} = Layout;
|
||||||
|
|
||||||
|
// 静态数据示例
|
||||||
|
const heroList = [
|
||||||
|
{id: 1, name: '雅娜凯', avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=1'},
|
||||||
|
{id: 2, name: '艾莉丝', avatar: 'https://api.dicebear.com/7.x/miniavs/svg?seed=2'},
|
||||||
|
];
|
||||||
|
const hero = heroList[0];
|
||||||
|
const attributes = [
|
||||||
|
{label: '攻击', value: 1567},
|
||||||
|
{label: '防御', value: 1654},
|
||||||
|
{label: '生命', value: 24447},
|
||||||
|
{label: '速度', value: 188},
|
||||||
|
{label: '暴击', value: 49},
|
||||||
|
{label: '爆伤', value: 166},
|
||||||
|
{label: '命中', value: 46},
|
||||||
|
{label: '抵抗', value: 52},
|
||||||
|
];
|
||||||
|
const setOptions = [
|
||||||
|
{label: '任意套装', value: 'any'},
|
||||||
|
{label: '暴击套', value: 'crit'},
|
||||||
|
{label: '速度套', value: 'speed'},
|
||||||
|
];
|
||||||
|
const filterOptions = [
|
||||||
|
{label: '攻击', value: 'atk'},
|
||||||
|
{label: '防御', value: 'def'},
|
||||||
|
{label: '生命', value: 'hp'},
|
||||||
|
{label: '速度', value: 'spd'},
|
||||||
|
{label: '暴击', value: 'cr'},
|
||||||
|
{label: '爆伤', value: 'cd'},
|
||||||
|
{label: '命中', value: 'acc'},
|
||||||
|
{label: '抵抗', value: 'res'},
|
||||||
|
];
|
||||||
|
|
||||||
|
const resultColumns = [
|
||||||
|
{title: '套装', dataIndex: 'set', key: 'set', render: (v: string) => <Tag>{v}</Tag>},
|
||||||
|
{title: '攻击', dataIndex: 'atk', key: 'atk'},
|
||||||
|
{title: '防御', dataIndex: 'def', key: 'def'},
|
||||||
|
{title: '生命', dataIndex: 'hp', key: 'hp'},
|
||||||
|
{title: '速度', dataIndex: 'spd', key: 'spd'},
|
||||||
|
{title: '暴击', dataIndex: 'cr', key: 'cr'},
|
||||||
|
{title: '爆伤', dataIndex: 'cd', key: 'cd'},
|
||||||
|
{title: '命中', dataIndex: 'acc', key: 'acc'},
|
||||||
|
{title: '抵抗', dataIndex: 'res', key: 'res'},
|
||||||
|
];
|
||||||
|
const resultData = [
|
||||||
|
{key: 1, set: '暴击套', atk: 2000, def: 1500, hp: 20000, spd: 200, cr: 100, cd: 250, acc: 30, res: 20},
|
||||||
|
{key: 2, set: '速度套', atk: 1800, def: 1400, hp: 21000, spd: 220, cr: 80, cd: 200, acc: 40, res: 30},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function OptimizerPage() {
|
||||||
|
const [editVisible, setEditVisible] = React.useState(false);
|
||||||
|
const [editData, setEditData] = React.useState({
|
||||||
|
artifact: '',
|
||||||
|
artifactLevel: 0,
|
||||||
|
formation: '',
|
||||||
|
exclusive: '',
|
||||||
|
star: 5,
|
||||||
|
});
|
||||||
|
|
||||||
function OptimizerPage() {
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 24 }}>
|
<Layout style={{minHeight: '100vh'}}>
|
||||||
<h2>配装优化</h2>
|
<Content style={{padding: 16}}>
|
||||||
<p>请选择一个角色后点击开始配装。后续将在此页面实现配装计算与展示。</p>
|
{/* 顶部角色和选项区 */}
|
||||||
|
<Card style={{marginBottom: 16, position: 'relative'}}>
|
||||||
|
<Row gutter={16} align="top">
|
||||||
|
{/* 角色头像与神器区 */}
|
||||||
|
<Col span={6} style={{display: 'flex', alignItems: 'flex-start'}}>
|
||||||
|
<Avatar size={64} src={hero.avatar} icon={<UserOutlined/>} style={{marginRight: 16}}/>
|
||||||
|
<div style={{display: 'flex', flexDirection: 'column', gap: 8}}>
|
||||||
|
<div style={{fontWeight: 500}}>{hero.name}</div>
|
||||||
|
<div style={{display: 'flex', alignItems: 'center', gap: 8}}>
|
||||||
|
<span>神器:</span>
|
||||||
|
<span style={{cursor: 'pointer', color: '#1677ff'}}
|
||||||
|
onClick={() => setEditVisible(true)}>
|
||||||
|
{/* 假设有artifactUrl则显示图片,否则暂无 */}
|
||||||
|
{editData.artifact ? <img src={editData.artifact} alt="神器"
|
||||||
|
style={{width: 32, height: 32}}/> : '暂无'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{display: 'flex', alignItems: 'center', gap: 8}}>
|
||||||
|
<span>专属神器:</span>
|
||||||
|
<span style={{cursor: 'pointer', color: '#1677ff'}}
|
||||||
|
onClick={() => setEditVisible(true)}>
|
||||||
|
{editData.exclusive ? editData.exclusive : '暂无'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
{/* 属性区(纵向) */}
|
||||||
|
<Col span={6}>
|
||||||
|
<div style={{fontWeight: 500, marginBottom: 8}}>属性</div>
|
||||||
|
<div style={{display: 'flex', flexDirection: 'column', gap: 8}}>
|
||||||
|
{attributes.map(attr => (
|
||||||
|
<div key={attr.label}>{attr.label}: <b>{attr.value}</b></div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
{/* 属性过滤区(纵向,最小最大值) */}
|
||||||
|
<Col span={6}>
|
||||||
|
<div style={{fontWeight: 500, marginBottom: 8}}>属性过滤</div>
|
||||||
|
<div style={{display: 'flex', flexDirection: 'column', gap: 8}}>
|
||||||
|
{filterOptions.map(opt => (
|
||||||
|
<div key={opt.value} style={{display: 'flex', alignItems: 'center', gap: 4}}>
|
||||||
|
<span style={{minWidth: 36}}>{opt.label}</span>
|
||||||
|
<InputNumber size="small" placeholder="最小值" style={{width: 70}}/>
|
||||||
|
<span>~</span>
|
||||||
|
<InputNumber size="small" placeholder="最大值" style={{width: 70}}/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
{/* 指定套装区(纵向,多选) */}
|
||||||
|
<Col span={6} style={{display: 'flex', flexDirection: 'column', justifyContent: 'flex-start'}}>
|
||||||
|
<div style={{fontWeight: 500, marginBottom: 8}}>指定套装</div>
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
style={{width: '100%', marginBottom: 8}}
|
||||||
|
defaultValue={[setOptions[0].value]}
|
||||||
|
>
|
||||||
|
{setOptions.map(opt => <Select.Option key={opt.value}
|
||||||
|
value={opt.value}>{opt.label}</Select.Option>)}
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
{/* 按钮区,绝对定位到Card右下角 */}
|
||||||
|
<div style={{position: 'absolute', right: 24, bottom: 16, display: 'flex', gap: 8}}>
|
||||||
|
<Button type="primary" icon={<AppstoreOutlined/>}>开始配装</Button>
|
||||||
|
<Button icon={<FilterOutlined/>}>重置筛选</Button>
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 配装结果列表区 */}
|
||||||
|
<Card title="配装结果" style={{marginBottom: 16}}>
|
||||||
|
<div style={{display: 'flex', justifyContent: 'space-between', marginBottom: 8}}>
|
||||||
|
<div>全部排列组合:<b>123,456</b> 条 | 筛选结果:<b>2</b> 条</div>
|
||||||
|
<Pagination size="small" total={2} pageSize={10} current={1} showSizeChanger={false}/>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
dataSource={resultData}
|
||||||
|
columns={resultColumns}
|
||||||
|
pagination={false}
|
||||||
|
scroll={{x: true}}
|
||||||
|
rowKey="key"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 单个配装详情区 */}
|
||||||
|
<Card title="配装详情">
|
||||||
|
<div style={{display: 'flex', gap: 24}}>
|
||||||
|
<div>
|
||||||
|
<Avatar size={48} src={hero.avatar}/>
|
||||||
|
<div style={{marginTop: 8}}>{hero.name}</div>
|
||||||
|
</div>
|
||||||
|
<Divider type="vertical" style={{height: 80}}/>
|
||||||
|
<div>
|
||||||
|
<div>套装:<Tag color="blue">暴击套</Tag></div>
|
||||||
|
<div>攻击:2000,防御:1500,生命:20000,速度:200</div>
|
||||||
|
<div>暴击:100%,爆伤:250%,命中:30%,抵抗:20%</div>
|
||||||
|
</div>
|
||||||
|
<Divider type="vertical" style={{height: 80}}/>
|
||||||
|
<div>
|
||||||
|
<Button type="primary">保存配置</Button>
|
||||||
|
<Button style={{marginLeft: 8}}>删除配置</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 角色信息编辑弹窗 */}
|
||||||
|
<Modal
|
||||||
|
title="角色信息编辑"
|
||||||
|
open={editVisible}
|
||||||
|
onCancel={() => setEditVisible(false)}
|
||||||
|
onOk={() => setEditVisible(false)}
|
||||||
|
>
|
||||||
|
<div style={{display: 'flex', flexDirection: 'column', gap: 16}}>
|
||||||
|
<div>
|
||||||
|
<span>神器:</span>
|
||||||
|
<Input
|
||||||
|
value={editData.artifact}
|
||||||
|
onChange={e => setEditData({...editData, artifact: e.target.value})}
|
||||||
|
placeholder="神器图片URL或名称"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>神器等级:</span>
|
||||||
|
<InputNumber
|
||||||
|
min={0}
|
||||||
|
max={20}
|
||||||
|
value={editData.artifactLevel}
|
||||||
|
onChange={v => setEditData({...editData, artifactLevel: v || 0})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>阵型:</span>
|
||||||
|
<Input
|
||||||
|
value={editData.formation}
|
||||||
|
onChange={e => setEditData({...editData, formation: e.target.value})}
|
||||||
|
placeholder="如:前排/后排"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>专属装备:</span>
|
||||||
|
<Input
|
||||||
|
value={editData.exclusive}
|
||||||
|
onChange={e => setEditData({...editData, exclusive: e.target.value})}
|
||||||
|
placeholder="如:+9速度"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>星数:</span>
|
||||||
|
<InputNumber
|
||||||
|
min={1}
|
||||||
|
max={6}
|
||||||
|
value={editData.star}
|
||||||
|
onChange={v => setEditData({...editData, star: v || 1})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OptimizerPage;
|
|
||||||
38
frontend/src/store/useCaptureStore.ts
Normal file
38
frontend/src/store/useCaptureStore.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import {create} from 'zustand';
|
||||||
|
import {persist} from 'zustand/middleware';
|
||||||
|
|
||||||
|
export interface Equipment {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
ct: number;
|
||||||
|
e: number;
|
||||||
|
g: number;
|
||||||
|
l: boolean;
|
||||||
|
mg: number;
|
||||||
|
op: Array<[string, any]>;
|
||||||
|
p: number;
|
||||||
|
s: string;
|
||||||
|
sk: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CaptureResult {
|
||||||
|
items: Equipment[];
|
||||||
|
heroes: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CaptureStoreState {
|
||||||
|
parsedData: CaptureResult | null;
|
||||||
|
setParsedData: (data: CaptureResult | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCaptureStore = create<CaptureStoreState>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
parsedData: null,
|
||||||
|
setParsedData: (data) => set({ parsedData: data }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'capture-store', // localStorage key
|
||||||
|
partialize: (state) => ({ parsedData: state.parsedData }) }
|
||||||
|
)
|
||||||
|
);
|
||||||
66
frontend/src/utils/useMessage.ts
Normal file
66
frontend/src/utils/useMessage.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import {message} from 'antd';
|
||||||
|
import {useRef} from 'react';
|
||||||
|
|
||||||
|
// 全局消息缓存,用于防止重复显示
|
||||||
|
const messageCache = new Map<string, number>();
|
||||||
|
const DEBOUNCE_TIME = 2000; // 2秒内相同消息不重复显示
|
||||||
|
|
||||||
|
export const useMessage = () => {
|
||||||
|
const messageKeyRef = useRef<string>('');
|
||||||
|
|
||||||
|
const showMessage = (type: 'success' | 'error' | 'warning' | 'info', content: string, duration?: number) => {
|
||||||
|
// 生成消息的唯一标识
|
||||||
|
const messageId = `${type}:${content}`;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// 检查是否在防抖时间内
|
||||||
|
const lastTime = messageCache.get(messageId);
|
||||||
|
if (lastTime && now - lastTime < DEBOUNCE_TIME) {
|
||||||
|
return; // 跳过重复消息
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
messageCache.set(messageId, now);
|
||||||
|
|
||||||
|
// 清理过期的缓存项(超过10秒的)
|
||||||
|
const expiredKeys: string[] = [];
|
||||||
|
messageCache.forEach((timestamp, msgId) => {
|
||||||
|
if (now - timestamp > 10000) {
|
||||||
|
expiredKeys.push(msgId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expiredKeys.forEach(key => messageCache.delete(key));
|
||||||
|
|
||||||
|
// 显示消息
|
||||||
|
message[type](content, duration);
|
||||||
|
};
|
||||||
|
|
||||||
|
const success = (content: string, duration?: number) => {
|
||||||
|
showMessage('success', content, duration);
|
||||||
|
};
|
||||||
|
|
||||||
|
const error = (content: string, duration?: number) => {
|
||||||
|
showMessage('error', content, duration);
|
||||||
|
};
|
||||||
|
|
||||||
|
const warning = (content: string, duration?: number) => {
|
||||||
|
showMessage('warning', content, duration);
|
||||||
|
};
|
||||||
|
|
||||||
|
const info = (content: string, duration?: number) => {
|
||||||
|
showMessage('info', content, duration);
|
||||||
|
};
|
||||||
|
|
||||||
|
const destroy = () => {
|
||||||
|
message.destroy();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
warning,
|
||||||
|
info,
|
||||||
|
destroy,
|
||||||
|
showMessage,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -34,6 +34,20 @@ export namespace model {
|
|||||||
this.is_loopback = source["is_loopback"];
|
this.is_loopback = source["is_loopback"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export class ParsedResult {
|
||||||
|
items: any[];
|
||||||
|
heroes: any[];
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new ParsedResult(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.items = source["items"];
|
||||||
|
this.heroes = source["heroes"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
frontend/wailsjs/go/service/App.d.ts
vendored
18
frontend/wailsjs/go/service/App.d.ts
vendored
@@ -2,18 +2,34 @@
|
|||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
import {model} from '../models';
|
import {model} from '../models';
|
||||||
|
|
||||||
|
export function ExportCurrentData(arg1:string):Promise<void>;
|
||||||
|
|
||||||
export function ExportData(arg1:Array<string>,arg2:string):Promise<void>;
|
export function ExportData(arg1:Array<string>,arg2:string):Promise<void>;
|
||||||
|
|
||||||
|
export function GetAllAppSettings():Promise<Record<string, string>>;
|
||||||
|
|
||||||
|
export function GetAppSetting(arg1:string):Promise<string>;
|
||||||
|
|
||||||
export function GetCaptureStatus():Promise<model.CaptureStatus>;
|
export function GetCaptureStatus():Promise<model.CaptureStatus>;
|
||||||
|
|
||||||
export function GetCapturedData():Promise<Array<string>>;
|
export function GetCapturedData():Promise<Array<string>>;
|
||||||
|
|
||||||
|
export function GetCurrentDataForExport():Promise<string>;
|
||||||
|
|
||||||
|
export function GetLatestParsedDataFromDatabase():Promise<model.ParsedResult>;
|
||||||
|
|
||||||
export function GetNetworkInterfaces():Promise<Array<model.NetworkInterface>>;
|
export function GetNetworkInterfaces():Promise<Array<model.NetworkInterface>>;
|
||||||
|
|
||||||
export function ParseData(arg1:Array<string>):Promise<string>;
|
export function ParseData(arg1:Array<string>):Promise<string>;
|
||||||
|
|
||||||
export function ReadRawJsonFile():Promise<string>;
|
export function ReadRawJsonFile():Promise<model.ParsedResult>;
|
||||||
|
|
||||||
|
export function SaveAppSetting(arg1:string,arg2:string):Promise<void>;
|
||||||
|
|
||||||
|
export function SaveParsedDataToDatabase(arg1:string,arg2:string,arg3:string):Promise<void>;
|
||||||
|
|
||||||
export function StartCapture(arg1:string):Promise<void>;
|
export function StartCapture(arg1:string):Promise<void>;
|
||||||
|
|
||||||
|
export function StopAndParseCapture():Promise<model.ParsedResult>;
|
||||||
|
|
||||||
export function StopCapture():Promise<void>;
|
export function StopCapture():Promise<void>;
|
||||||
|
|||||||
@@ -2,10 +2,22 @@
|
|||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
export function ExportCurrentData(arg1) {
|
||||||
|
return window['go']['service']['App']['ExportCurrentData'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function ExportData(arg1, arg2) {
|
export function ExportData(arg1, arg2) {
|
||||||
return window['go']['service']['App']['ExportData'](arg1, arg2);
|
return window['go']['service']['App']['ExportData'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GetAllAppSettings() {
|
||||||
|
return window['go']['service']['App']['GetAllAppSettings']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetAppSetting(arg1) {
|
||||||
|
return window['go']['service']['App']['GetAppSetting'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function GetCaptureStatus() {
|
export function GetCaptureStatus() {
|
||||||
return window['go']['service']['App']['GetCaptureStatus']();
|
return window['go']['service']['App']['GetCaptureStatus']();
|
||||||
}
|
}
|
||||||
@@ -14,6 +26,14 @@ export function GetCapturedData() {
|
|||||||
return window['go']['service']['App']['GetCapturedData']();
|
return window['go']['service']['App']['GetCapturedData']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GetCurrentDataForExport() {
|
||||||
|
return window['go']['service']['App']['GetCurrentDataForExport']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetLatestParsedDataFromDatabase() {
|
||||||
|
return window['go']['service']['App']['GetLatestParsedDataFromDatabase']();
|
||||||
|
}
|
||||||
|
|
||||||
export function GetNetworkInterfaces() {
|
export function GetNetworkInterfaces() {
|
||||||
return window['go']['service']['App']['GetNetworkInterfaces']();
|
return window['go']['service']['App']['GetNetworkInterfaces']();
|
||||||
}
|
}
|
||||||
@@ -26,10 +46,22 @@ export function ReadRawJsonFile() {
|
|||||||
return window['go']['service']['App']['ReadRawJsonFile']();
|
return window['go']['service']['App']['ReadRawJsonFile']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SaveAppSetting(arg1, arg2) {
|
||||||
|
return window['go']['service']['App']['SaveAppSetting'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SaveParsedDataToDatabase(arg1, arg2, arg3) {
|
||||||
|
return window['go']['service']['App']['SaveParsedDataToDatabase'](arg1, arg2, arg3);
|
||||||
|
}
|
||||||
|
|
||||||
export function StartCapture(arg1) {
|
export function StartCapture(arg1) {
|
||||||
return window['go']['service']['App']['StartCapture'](arg1);
|
return window['go']['service']['App']['StartCapture'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function StopAndParseCapture() {
|
||||||
|
return window['go']['service']['App']['StopAndParseCapture']();
|
||||||
|
}
|
||||||
|
|
||||||
export function StopCapture() {
|
export function StopCapture() {
|
||||||
return window['go']['service']['App']['StopCapture']();
|
return window['go']['service']['App']['StopCapture']();
|
||||||
}
|
}
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -6,6 +6,7 @@ toolchain go1.24.4
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/gopacket v1.1.19
|
github.com/google/gopacket v1.1.19
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17
|
||||||
github.com/wailsapp/wails/v2 v2.10.1
|
github.com/wailsapp/wails/v2 v2.10.1
|
||||||
go.uber.org/zap v1.26.0
|
go.uber.org/zap v1.26.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -34,6 +34,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
|||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"equipment-analyzer/internal/model"
|
"equipment-analyzer/internal/model"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/google/gopacket/pcap"
|
"github.com/google/gopacket/pcap"
|
||||||
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetNetworkInterfaces 获取网络接口列表
|
// GetNetworkInterfaces 获取网络接口列表
|
||||||
@@ -13,6 +14,8 @@ func GetNetworkInterfaces() ([]model.NetworkInterface, error) {
|
|||||||
return nil, fmt.Errorf("failed to find network devices: %v", err)
|
return nil, fmt.Errorf("failed to find network devices: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("抓取到的网卡设备数量: %d", len(devices)) // 打印网卡总数
|
||||||
|
|
||||||
var interfaces []model.NetworkInterface
|
var interfaces []model.NetworkInterface
|
||||||
for _, device := range devices {
|
for _, device := range devices {
|
||||||
// 跳过回环接口
|
// 跳过回环接口
|
||||||
@@ -20,6 +23,8 @@ func GetNetworkInterfaces() ([]model.NetworkInterface, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//log.Printf("网卡名称: %s, 描述: %s", device.Name, device.Description) // 打印每个网卡的名称和描述
|
||||||
|
|
||||||
// 提取IP地址
|
// 提取IP地址
|
||||||
var addresses []string
|
var addresses []string
|
||||||
for _, address := range device.Addresses {
|
for _, address := range device.Addresses {
|
||||||
|
|||||||
163
internal/model/database.go
Normal file
163
internal/model/database.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Database 数据库管理器
|
||||||
|
type Database struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDatabase 创建新的数据库连接
|
||||||
|
func NewDatabase() (*Database, error) {
|
||||||
|
dbPath := getDatabasePath()
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
dir := filepath.Dir(dbPath)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("创建数据库目录失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接数据库
|
||||||
|
db, err := sql.Open("sqlite3", dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("连接数据库失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试连接
|
||||||
|
if err := db.Ping(); err != nil {
|
||||||
|
return nil, fmt.Errorf("数据库连接测试失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
database := &Database{db: db}
|
||||||
|
|
||||||
|
// 初始化表结构
|
||||||
|
if err := database.initTables(); err != nil {
|
||||||
|
return nil, fmt.Errorf("初始化数据库表失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return database, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭数据库连接
|
||||||
|
func (d *Database) Close() error {
|
||||||
|
return d.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDatabasePath 获取数据库文件路径
|
||||||
|
func getDatabasePath() string {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "equipment_analyzer.db"
|
||||||
|
}
|
||||||
|
return filepath.Join(homeDir, ".equipment-analyzer", "equipment_analyzer.db")
|
||||||
|
}
|
||||||
|
|
||||||
|
// initTables 初始化数据库表结构
|
||||||
|
func (d *Database) initTables() error {
|
||||||
|
// 解析数据表 - 存储抓包解析后的装备和角色数据
|
||||||
|
parsedDataTable := `
|
||||||
|
CREATE TABLE IF NOT EXISTS parsed_data (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
session_name TEXT NOT NULL,
|
||||||
|
items_json TEXT NOT NULL,
|
||||||
|
heroes_json TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
);`
|
||||||
|
|
||||||
|
// 应用设置表
|
||||||
|
settingsTable := `
|
||||||
|
CREATE TABLE IF NOT EXISTS app_settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
);`
|
||||||
|
|
||||||
|
tables := []string{
|
||||||
|
parsedDataTable,
|
||||||
|
settingsTable,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range tables {
|
||||||
|
if _, err := d.db.Exec(table); err != nil {
|
||||||
|
return fmt.Errorf("创建表失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveParsedData 保存解析后的数据
|
||||||
|
func (d *Database) SaveParsedData(sessionName string, itemsJSON, heroesJSON string) error {
|
||||||
|
stmt := `
|
||||||
|
INSERT INTO parsed_data (session_name, items_json, heroes_json, created_at)
|
||||||
|
VALUES (?, ?, ?, ?)`
|
||||||
|
|
||||||
|
_, err := d.db.Exec(stmt, sessionName, itemsJSON, heroesJSON, time.Now().Unix())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLatestParsedData 获取最新的解析数据
|
||||||
|
func (d *Database) GetLatestParsedData() (string, string, error) {
|
||||||
|
stmt := `
|
||||||
|
SELECT items_json, heroes_json
|
||||||
|
FROM parsed_data
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1`
|
||||||
|
|
||||||
|
var itemsJSON, heroesJSON string
|
||||||
|
err := d.db.QueryRow(stmt).Scan(&itemsJSON, &heroesJSON)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemsJSON, heroesJSON, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// SaveSetting 保存应用设置
|
||||||
|
func (d *Database) SaveSetting(key, value string) error {
|
||||||
|
stmt := "INSERT OR REPLACE INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)"
|
||||||
|
_, err := d.db.Exec(stmt, key, value, time.Now().Unix())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSetting 获取应用设置
|
||||||
|
func (d *Database) GetSetting(key string) (string, error) {
|
||||||
|
stmt := "SELECT value FROM app_settings WHERE key = ?"
|
||||||
|
var value string
|
||||||
|
err := d.db.QueryRow(stmt, key).Scan(&value)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllSettings 获取所有设置
|
||||||
|
func (d *Database) GetAllSettings() (map[string]string, error) {
|
||||||
|
stmt := "SELECT key, value FROM app_settings"
|
||||||
|
rows, err := d.db.Query(stmt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
settings := make(map[string]string)
|
||||||
|
for rows.Next() {
|
||||||
|
var key, value string
|
||||||
|
if err := rows.Scan(&key, &value); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
settings[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings, nil
|
||||||
|
}
|
||||||
@@ -29,3 +29,9 @@ type CaptureStatus struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParsedResult 解析结果
|
||||||
|
type ParsedResult struct {
|
||||||
|
Items []interface{} `json:"items"`
|
||||||
|
Heroes []interface{} `json:"heroes"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -101,3 +101,33 @@ func (cs *CaptureService) processData(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StopAndParseCapture 停止抓包并解析数据
|
||||||
|
func (cs *CaptureService) StopAndParseCapture(parser *ParserService) (*model.ParsedResult, error) {
|
||||||
|
cs.mutex.Lock()
|
||||||
|
defer cs.mutex.Unlock()
|
||||||
|
|
||||||
|
if !cs.isCapturing {
|
||||||
|
return nil, fmt.Errorf("capture not running")
|
||||||
|
}
|
||||||
|
|
||||||
|
cs.packetCapture.Stop()
|
||||||
|
cs.isCapturing = false
|
||||||
|
cs.logger.Info("Packet capture stopped (StopAndParseCapture)")
|
||||||
|
|
||||||
|
// 处理所有收集的数据
|
||||||
|
cs.packetCapture.ProcessAllData()
|
||||||
|
|
||||||
|
// 获取抓包数据
|
||||||
|
rawData := cs.packetCapture.GetCapturedData()
|
||||||
|
if len(rawData) == 0 {
|
||||||
|
return nil, fmt.Errorf("no captured data")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析数据
|
||||||
|
result, _, err := parser.ParseHexData(rawData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("解析数据失败: %v", err)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|||||||
82
internal/service/database_service.go
Normal file
82
internal/service/database_service.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"equipment-analyzer/internal/model"
|
||||||
|
"equipment-analyzer/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DatabaseService 数据库服务
|
||||||
|
type DatabaseService struct {
|
||||||
|
db *model.Database
|
||||||
|
logger *utils.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDatabaseService 创建数据库服务
|
||||||
|
func NewDatabaseService(db *model.Database, logger *utils.Logger) *DatabaseService {
|
||||||
|
return &DatabaseService{
|
||||||
|
db: db,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveParsedDataToDatabase 保存解析后的数据到数据库
|
||||||
|
func (s *DatabaseService) SaveParsedDataToDatabase(sessionName string, itemsJSON, heroesJSON string) error {
|
||||||
|
err := s.db.SaveParsedData(sessionName, itemsJSON, heroesJSON)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("保存解析数据到数据库失败", "error", err, "session_name", sessionName)
|
||||||
|
return fmt.Errorf("保存解析数据失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("解析数据保存成功", "session_name", sessionName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLatestParsedDataFromDatabase 从数据库获取最新的解析数据
|
||||||
|
func (s *DatabaseService) GetLatestParsedDataFromDatabase() (string, string, error) {
|
||||||
|
itemsJSON, heroesJSON, err := s.db.GetLatestParsedData()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("从数据库获取最新解析数据失败", "error", err)
|
||||||
|
return "", "", fmt.Errorf("获取解析数据失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("最新解析数据获取成功")
|
||||||
|
return itemsJSON, heroesJSON, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveAppSetting 保存应用设置
|
||||||
|
func (s *DatabaseService) SaveAppSetting(key, value string) error {
|
||||||
|
err := s.db.SaveSetting(key, value)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("保存应用设置失败", "error", err, "key", key)
|
||||||
|
return fmt.Errorf("保存设置失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("应用设置保存成功", "key", key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAppSetting 获取应用设置
|
||||||
|
func (s *DatabaseService) GetAppSetting(key string) (string, error) {
|
||||||
|
value, err := s.db.GetSetting(key)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取应用设置失败", "error", err, "key", key)
|
||||||
|
return "", fmt.Errorf("获取设置失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllAppSettings 获取所有应用设置
|
||||||
|
func (s *DatabaseService) GetAllAppSettings() (map[string]string, error) {
|
||||||
|
settings, err := s.db.GetAllSettings()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("获取所有应用设置失败", "error", err)
|
||||||
|
return nil, fmt.Errorf("获取设置失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -17,15 +17,34 @@ type App struct {
|
|||||||
logger *utils.Logger
|
logger *utils.Logger
|
||||||
captureService *CaptureService
|
captureService *CaptureService
|
||||||
parserService *ParserService
|
parserService *ParserService
|
||||||
|
database *model.Database
|
||||||
|
databaseService *DatabaseService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApp(cfg *config.Config, logger *utils.Logger) *App {
|
func NewApp(cfg *config.Config, logger *utils.Logger) *App {
|
||||||
|
// 初始化数据库
|
||||||
|
database, err := model.NewDatabase()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("初始化数据库失败", "error", err)
|
||||||
|
// 如果数据库初始化失败,仍然创建应用,但数据库功能不可用
|
||||||
return &App{
|
return &App{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
captureService: NewCaptureService(cfg, logger),
|
captureService: NewCaptureService(cfg, logger),
|
||||||
parserService: NewParserService(cfg, logger),
|
parserService: NewParserService(cfg, logger),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
databaseService := NewDatabaseService(database, logger)
|
||||||
|
|
||||||
|
return &App{
|
||||||
|
config: cfg,
|
||||||
|
logger: logger,
|
||||||
|
captureService: NewCaptureService(cfg, logger),
|
||||||
|
parserService: NewParserService(cfg, logger),
|
||||||
|
database: database,
|
||||||
|
databaseService: databaseService,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Startup(ctx context.Context) {
|
func (a *App) Startup(ctx context.Context) {
|
||||||
@@ -43,6 +62,15 @@ func (a *App) BeforeClose(ctx context.Context) (prevent bool) {
|
|||||||
|
|
||||||
func (a *App) Shutdown(ctx context.Context) {
|
func (a *App) Shutdown(ctx context.Context) {
|
||||||
a.logger.Info("应用关闭")
|
a.logger.Info("应用关闭")
|
||||||
|
|
||||||
|
// 关闭数据库连接
|
||||||
|
if a.database != nil {
|
||||||
|
if err := a.database.Close(); err != nil {
|
||||||
|
a.logger.Error("关闭数据库连接失败", "error", err)
|
||||||
|
} else {
|
||||||
|
a.logger.Info("数据库连接已关闭")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNetworkInterfaces 获取网络接口列表
|
// GetNetworkInterfaces 获取网络接口列表
|
||||||
@@ -104,49 +132,106 @@ func (a *App) GetCapturedData() ([]string, error) {
|
|||||||
|
|
||||||
// ParseData 解析数据为JSON
|
// ParseData 解析数据为JSON
|
||||||
func (a *App) ParseData(hexDataList []string) (string, error) {
|
func (a *App) ParseData(hexDataList []string) (string, error) {
|
||||||
result, err := a.parserService.ParseHexData(hexDataList)
|
_, rawJson, err := a.parserService.ParseHexData(hexDataList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Error("解析数据失败", "error", err)
|
a.logger.Error("解析数据失败", "error", err)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
return rawJson, nil
|
||||||
jsonData, err := json.MarshalIndent(result, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
a.logger.Error("JSON序列化失败", "error", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
a.logger.Info("数据解析完成", "count", len(result.Data))
|
|
||||||
return string(jsonData), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExportData 导出数据到文件
|
// ExportData 导出数据到文件
|
||||||
func (a *App) ExportData(hexDataList []string, filename string) error {
|
func (a *App) ExportData(hexDataList []string, filename string) error {
|
||||||
result, err := a.parserService.ParseHexData(hexDataList)
|
result, rawJson, err := a.parserService.ParseHexData(hexDataList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Error("解析数据失败", "error", err)
|
a.logger.Error("解析数据失败", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// 这里可以添加文件写入逻辑
|
||||||
jsonData, err := json.MarshalIndent(result, "", " ")
|
a.logger.Info("导出数据", "filename", filename, "count", len(result.Items))
|
||||||
|
// 简单示例:写入到当前目录
|
||||||
|
err = utils.WriteFile(filename, []byte(rawJson))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Error("JSON序列化失败", "error", err)
|
a.logger.Error("写入文件失败", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportCurrentData 导出当前数据库中的数据到文件
|
||||||
|
func (a *App) ExportCurrentData(filename string) error {
|
||||||
|
if a.databaseService == nil {
|
||||||
|
return fmt.Errorf("数据库服务未初始化")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从数据库获取最新数据
|
||||||
|
parsedResult, err := a.GetLatestParsedDataFromDatabase()
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Error("获取数据库数据失败", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 这里可以添加文件写入逻辑
|
if parsedResult == nil || (len(parsedResult.Items) == 0 && len(parsedResult.Heroes) == 0) {
|
||||||
a.logger.Info("导出数据", "filename", filename, "count", len(result.Data))
|
return fmt.Errorf("没有数据可导出")
|
||||||
|
}
|
||||||
|
|
||||||
// 简单示例:写入到当前目录
|
// 创建导出数据格式
|
||||||
|
exportData := map[string]interface{}{
|
||||||
|
"items": parsedResult.Items,
|
||||||
|
"heroes": parsedResult.Heroes,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 序列化为JSON
|
||||||
|
jsonData, err := json.MarshalIndent(exportData, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Error("序列化数据失败", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入文件
|
||||||
err = utils.WriteFile(filename, jsonData)
|
err = utils.WriteFile(filename, jsonData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Error("写入文件失败", "error", err)
|
a.logger.Error("写入文件失败", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.logger.Info("数据导出成功", "filename", filename, "items_count", len(parsedResult.Items), "heroes_count", len(parsedResult.Heroes))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCurrentDataForExport 获取当前数据库中的数据,供前端导出使用
|
||||||
|
func (a *App) GetCurrentDataForExport() (string, error) {
|
||||||
|
if a.databaseService == nil {
|
||||||
|
return "", fmt.Errorf("数据库服务未初始化")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从数据库获取最新数据
|
||||||
|
parsedResult, err := a.GetLatestParsedDataFromDatabase()
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Error("获取数据库数据失败", "error", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedResult == nil || (len(parsedResult.Items) == 0 && len(parsedResult.Heroes) == 0) {
|
||||||
|
return "", fmt.Errorf("没有数据可导出")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建导出数据格式
|
||||||
|
exportData := map[string]interface{}{
|
||||||
|
"items": parsedResult.Items,
|
||||||
|
"heroes": parsedResult.Heroes,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 序列化为JSON
|
||||||
|
jsonData, err := json.MarshalIndent(exportData, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Error("序列化数据失败", "error", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(jsonData), nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetCaptureStatus 获取抓包状态
|
// GetCaptureStatus 获取抓包状态
|
||||||
func (a *App) GetCaptureStatus() model.CaptureStatus {
|
func (a *App) GetCaptureStatus() model.CaptureStatus {
|
||||||
return model.CaptureStatus{
|
return model.CaptureStatus{
|
||||||
@@ -162,7 +247,113 @@ func (a *App) getStatusMessage() string {
|
|||||||
return "准备就绪"
|
return "准备就绪"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadRawJsonFile 供前端调用,读取output_raw.json内容
|
// ReadRawJsonFile 已废弃,请使用GetLatestParsedDataFromDatabase从数据库获取数据
|
||||||
func (a *App) ReadRawJsonFile() (string, error) {
|
func (a *App) ReadRawJsonFile() (*model.ParsedResult, error) {
|
||||||
return a.parserService.ReadRawJsonFile()
|
return a.GetLatestParsedDataFromDatabase()
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopAndParseCapture 停止抓包并解析数据,供前端调用
|
||||||
|
func (a *App) StopAndParseCapture() (*model.ParsedResult, error) {
|
||||||
|
result, err := a.captureService.StopAndParseCapture(a.parserService)
|
||||||
|
if err != nil {
|
||||||
|
a.logger.Error("停止抓包并解析数据失败", "error", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将解析结果保存到数据库
|
||||||
|
if a.databaseService != nil && result != nil {
|
||||||
|
// 序列化装备数据
|
||||||
|
itemsJSON := "[]"
|
||||||
|
if result.Items != nil {
|
||||||
|
if jsonData, err := json.Marshal(result.Items); err == nil {
|
||||||
|
itemsJSON = string(jsonData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 序列化英雄数据
|
||||||
|
heroesJSON := "[]"
|
||||||
|
if result.Heroes != nil {
|
||||||
|
if jsonData, err := json.Marshal(result.Heroes); err == nil {
|
||||||
|
heroesJSON = string(jsonData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
sessionName := fmt.Sprintf("capture_%d", time.Now().Unix())
|
||||||
|
if err := a.databaseService.SaveParsedDataToDatabase(sessionName, itemsJSON, heroesJSON); err != nil {
|
||||||
|
a.logger.Error("保存解析数据到数据库失败", "error", err)
|
||||||
|
// 不返回错误,因为解析成功了,只是保存失败
|
||||||
|
} else {
|
||||||
|
a.logger.Info("解析数据已保存到数据库", "session_name", sessionName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 数据库相关API ==========
|
||||||
|
|
||||||
|
// SaveParsedDataToDatabase 保存解析后的数据到数据库
|
||||||
|
func (a *App) SaveParsedDataToDatabase(sessionName string, itemsJSON, heroesJSON string) error {
|
||||||
|
if a.databaseService == nil {
|
||||||
|
return fmt.Errorf("数据库服务未初始化")
|
||||||
|
}
|
||||||
|
return a.databaseService.SaveParsedDataToDatabase(sessionName, itemsJSON, heroesJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLatestParsedDataFromDatabase 从数据库获取最新的解析数据
|
||||||
|
func (a *App) GetLatestParsedDataFromDatabase() (*model.ParsedResult, error) {
|
||||||
|
if a.databaseService == nil {
|
||||||
|
return nil, fmt.Errorf("数据库服务未初始化")
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsJSON, heroesJSON, err := a.databaseService.GetLatestParsedDataFromDatabase()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析装备数据
|
||||||
|
var items []interface{}
|
||||||
|
if itemsJSON != "" {
|
||||||
|
if err := json.Unmarshal([]byte(itemsJSON), &items); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析装备数据失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析英雄数据
|
||||||
|
var heroes []interface{}
|
||||||
|
if heroesJSON != "" {
|
||||||
|
if err := json.Unmarshal([]byte(heroesJSON), &heroes); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析英雄数据失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.ParsedResult{
|
||||||
|
Items: items,
|
||||||
|
Heroes: heroes,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveAppSetting 保存应用设置
|
||||||
|
func (a *App) SaveAppSetting(key, value string) error {
|
||||||
|
if a.databaseService == nil {
|
||||||
|
return fmt.Errorf("数据库服务未初始化")
|
||||||
|
}
|
||||||
|
return a.databaseService.SaveAppSetting(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAppSetting 获取应用设置
|
||||||
|
func (a *App) GetAppSetting(key string) (string, error) {
|
||||||
|
if a.databaseService == nil {
|
||||||
|
return "", fmt.Errorf("数据库服务未初始化")
|
||||||
|
}
|
||||||
|
return a.databaseService.GetAppSetting(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllAppSettings 获取所有应用设置
|
||||||
|
func (a *App) GetAllAppSettings() (map[string]string, error) {
|
||||||
|
if a.databaseService == nil {
|
||||||
|
return nil, fmt.Errorf("数据库服务未初始化")
|
||||||
|
}
|
||||||
|
return a.databaseService.GetAllAppSettings()
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -28,18 +28,17 @@ func NewParserService(cfg *config.Config, logger *utils.Logger) *ParserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ParseHexData 解析十六进制数据
|
// ParseHexData 解析十六进制数据
|
||||||
func (ps *ParserService) ParseHexData(hexDataList []string) (*model.CaptureResult, error) {
|
func (ps *ParserService) ParseHexData(hexDataList []string) (*model.ParsedResult, string, error) {
|
||||||
if len(hexDataList) == 0 {
|
if len(hexDataList) == 0 {
|
||||||
ps.logger.Warn("没有数据需要解析")
|
ps.logger.Warn("没有数据需要解析")
|
||||||
return &model.CaptureResult{
|
return &model.ParsedResult{
|
||||||
Data: make([]model.Equipment, 0),
|
Items: make([]interface{}, 0),
|
||||||
Units: make([]interface{}, 0),
|
Heroes: make([]interface{}, 0),
|
||||||
}, nil
|
}, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ps.logger.Info("开始远程解析数据", "count", len(hexDataList))
|
ps.logger.Info("开始远程解析数据", "count", len(hexDataList))
|
||||||
|
|
||||||
// 远程接口解析
|
|
||||||
url := "https://krivpfvxi0.execute-api.us-west-2.amazonaws.com/dev/getItems"
|
url := "https://krivpfvxi0.execute-api.us-west-2.amazonaws.com/dev/getItems"
|
||||||
reqBody := map[string]interface{}{
|
reqBody := map[string]interface{}{
|
||||||
"data": hexDataList,
|
"data": hexDataList,
|
||||||
@@ -54,45 +53,47 @@ func (ps *ParserService) ParseHexData(hexDataList []string) (*model.CaptureResul
|
|||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
body, _ := ioutil.ReadAll(resp.Body)
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
|
||||||
// 直接写入本地文件
|
// 新校验逻辑:校验data和units字段
|
||||||
fileErr := ioutil.WriteFile("output_raw.json", body, 0644)
|
var raw map[string]interface{}
|
||||||
if fileErr != nil {
|
if err := json.Unmarshal(body, &raw); err != nil {
|
||||||
ps.logger.Error("写入原始json文件失败", "error", fileErr)
|
ps.logger.Error("远程json解析失败", "error", err)
|
||||||
|
return nil, "", fmt.Errorf("远程json解析失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ps.logger.Info("远程原始数据已写入output_raw.json")
|
// 校验data字段
|
||||||
|
dataArr, dataOk := raw["data"].([]interface{})
|
||||||
|
if !dataOk || len(dataArr) == 0 {
|
||||||
|
ps.logger.Error("远程json校验失败,data字段缺失或为空")
|
||||||
|
return nil, "", fmt.Errorf("远程json校验失败,data字段缺失或为空")
|
||||||
|
}
|
||||||
|
|
||||||
// 返回空集合,保证前端不报错
|
// 校验通过,直接解析数据
|
||||||
return &model.CaptureResult{
|
ps.logger.Info("远程原始数据校验通过,开始解析")
|
||||||
Data: make([]model.Equipment, 0),
|
parsedResult, err := ps.ReadRawJsonFile(string(body))
|
||||||
Units: make([]interface{}, 0),
|
if err != nil {
|
||||||
}, nil
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedResult, "", nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
ps.logger.Error("远程解析请求失败", "error", err)
|
ps.logger.Error("远程解析请求失败", "error", err)
|
||||||
return nil, fmt.Errorf("远程解析请求失败: %v", err)
|
return nil, "", fmt.Errorf("远程解析请求失败: %v", err)
|
||||||
} else {
|
} else {
|
||||||
ps.logger.Error("远程解析响应码异常", "status", resp.StatusCode)
|
ps.logger.Error("远程解析响应码异常", "status", resp.StatusCode)
|
||||||
return nil, fmt.Errorf("远程解析响应码异常: %d", resp.StatusCode)
|
return nil, "", fmt.Errorf("远程解析响应码异常: %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ps.logger.Error("远程解析请求构建失败", "error", err)
|
ps.logger.Error("远程解析请求构建失败", "error", err)
|
||||||
return nil, fmt.Errorf("远程解析请求构建失败: %v", err)
|
return nil, "", fmt.Errorf("远程解析请求构建失败: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadRawJsonFile 读取output_raw.json文件内容并进行数据转换
|
// ReadRawJsonFile 读取rawJson内容并进行数据转换,返回ParsedResult对象
|
||||||
func (ps *ParserService) ReadRawJsonFile() (string, error) {
|
func (ps *ParserService) ReadRawJsonFile(rawJson string) (*model.ParsedResult, error) {
|
||||||
data, err := ioutil.ReadFile("output_raw.json")
|
|
||||||
if err != nil {
|
|
||||||
ps.logger.Error("读取output_raw.json失败", "error", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析原始JSON数据
|
|
||||||
var rawData map[string]interface{}
|
var rawData map[string]interface{}
|
||||||
if err := json.Unmarshal(data, &rawData); err != nil {
|
if err := json.Unmarshal([]byte(rawJson), &rawData); err != nil {
|
||||||
ps.logger.Error("解析JSON失败", "error", err)
|
ps.logger.Error("解析JSON失败", "error", err)
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取装备和英雄数据
|
// 提取装备和英雄数据
|
||||||
@@ -109,9 +110,6 @@ func (ps *ParserService) ReadRawJsonFile() (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 原始装备总数
|
|
||||||
fmt.Println("原始装备总数:", len(equips))
|
|
||||||
|
|
||||||
// 过滤有效装备 (x => !!x.f)
|
// 过滤有效装备 (x => !!x.f)
|
||||||
var validEquips []interface{}
|
var validEquips []interface{}
|
||||||
for _, equip := range equips {
|
for _, equip := range equips {
|
||||||
@@ -121,29 +119,24 @@ func (ps *ParserService) ReadRawJsonFile() (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Println("过滤f字段后装备数:", len(validEquips))
|
|
||||||
|
|
||||||
// 转换装备数据
|
// 转换装备数据
|
||||||
convertedItems := ps.convertItemsAllWithLog(validEquips)
|
convertedItems := ps.convertItemsAllWithLog(validEquips)
|
||||||
fmt.Println("转换后装备数:", len(convertedItems))
|
|
||||||
|
|
||||||
// 转换英雄数据(只对最大组)
|
// 转换英雄数据(只对最大组)
|
||||||
convertedHeroes := ps.convertUnits(rawUnits)
|
convertedHeroes := ps.convertUnits(rawUnits)
|
||||||
|
|
||||||
// 构建最终结果
|
result := &model.ParsedResult{
|
||||||
result := map[string]interface{}{
|
Items: make([]interface{}, len(convertedItems)),
|
||||||
"items": convertedItems,
|
Heroes: make([]interface{}, len(convertedHeroes)),
|
||||||
"heroes": convertedHeroes,
|
}
|
||||||
|
for i, v := range convertedItems {
|
||||||
|
result.Items[i] = v
|
||||||
|
}
|
||||||
|
for i, v := range convertedHeroes {
|
||||||
|
result.Heroes[i] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
// 序列化为JSON字符串
|
return result, nil
|
||||||
resultJSON, err := json.Marshal(result)
|
|
||||||
if err != nil {
|
|
||||||
ps.logger.Error("序列化结果失败", "error", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(resultJSON), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertItems 转换装备数据
|
// convertItems 转换装备数据
|
||||||
|
|||||||
@@ -1,85 +1,13 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"equipment-analyzer/internal/config"
|
|
||||||
"equipment-analyzer/internal/utils"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestReadRawJsonFile(t *testing.T) {
|
func TestReadRawJsonFile(t *testing.T) {
|
||||||
// 创建测试用的配置和日志器
|
// 此测试已废弃,因为ReadRawJsonFile方法已被移除
|
||||||
config := &config.Config{}
|
// 现在数据存储在SQLite数据库中,不再依赖output_raw.json文件
|
||||||
logger := utils.NewLogger()
|
t.Skip("ReadRawJsonFile测试已废弃,数据现在存储在SQLite数据库中")
|
||||||
|
|
||||||
// 创建ParserService实例
|
|
||||||
ps := NewParserService(config, logger)
|
|
||||||
|
|
||||||
// 调用ReadRawJsonFile方法
|
|
||||||
result, err := ps.ReadRawJsonFile()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ReadRawJsonFile failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Raw result length: %d\n", len(result))
|
|
||||||
fmt.Printf("Raw result preview: %s\n", result[:min(200, len(result))])
|
|
||||||
|
|
||||||
// 解析JSON结果
|
|
||||||
var parsedData map[string]interface{}
|
|
||||||
if err := json.Unmarshal([]byte(result), &parsedData); err != nil {
|
|
||||||
t.Fatalf("Failed to parse JSON result: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查数据结构
|
|
||||||
fmt.Printf("Parsed data keys: %v\n", getKeys(parsedData))
|
|
||||||
|
|
||||||
// 检查items字段
|
|
||||||
if items, exists := parsedData["items"]; exists {
|
|
||||||
if itemsArray, ok := items.([]interface{}); ok {
|
|
||||||
fmt.Printf("Items count: %d\n", len(itemsArray))
|
|
||||||
if len(itemsArray) > 0 {
|
|
||||||
fmt.Printf("First item: %+v\n", itemsArray[0])
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Items is not an array: %T\n", items)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Println("Items field not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查heroes字段
|
|
||||||
if heroes, exists := parsedData["heroes"]; exists {
|
|
||||||
if heroesArray, ok := heroes.([]interface{}); ok {
|
|
||||||
fmt.Printf("Heroes count: %d\n", len(heroesArray))
|
|
||||||
if len(heroesArray) > 0 {
|
|
||||||
fmt.Printf("First hero: %+v\n", heroesArray[0])
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Heroes is not an array: %T\n", heroes)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Println("Heroes field not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有数据,输出更多调试信息
|
|
||||||
if len(result) < 100 {
|
|
||||||
fmt.Printf("Result seems empty or very short: %q\n", result)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助函数
|
// 辅助函数已移除,因为测试已废弃
|
||||||
func min(a, b int) int {
|
|
||||||
if a < b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func getKeys(m map[string]interface{}) []string {
|
|
||||||
keys := make([]string, 0, len(m))
|
|
||||||
for k := range m {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
return keys
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type Logger struct {
|
|||||||
func NewLogger() *Logger {
|
func NewLogger() *Logger {
|
||||||
// 配置日志轮转
|
// 配置日志轮转
|
||||||
lumberJackLogger := &lumberjack.Logger{
|
lumberJackLogger := &lumberjack.Logger{
|
||||||
Filename: "logs/equipment-analyzer.log",
|
Filename: "./logs/equipment-analyzer.log",
|
||||||
MaxSize: 100, // MB
|
MaxSize: 100, // MB
|
||||||
MaxBackups: 3,
|
MaxBackups: 3,
|
||||||
MaxAge: 28, // days
|
MaxAge: 28, // days
|
||||||
|
|||||||
126588
opepic.json
126588
opepic.json
File diff suppressed because it is too large
Load Diff
95152
output_raw.json
95152
output_raw.json
File diff suppressed because it is too large
Load Diff
134423
output_raw_e7.json
134423
output_raw_e7.json
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user