feat(database): 实现数据库功能并优化数据导出
- 新增数据库相关 API 和服务 - 实现数据导出功能,支持导出到 JSON 文件 - 优化数据导入流程,增加数据校验 - 新增数据库页面,展示解析数据和统计信息 - 更新捕获页面,支持导入数据到数据库
This commit is contained in:
@@ -1,14 +1,10 @@
|
||||
import React, {useEffect, useState, useRef} from 'react'
|
||||
import {Button, Card, Layout, message, Select, Spin, Table} from 'antd'
|
||||
import {DownloadOutlined, PlayCircleOutlined, SettingOutlined, StopOutlined} from '@ant-design/icons'
|
||||
import React from 'react'
|
||||
import {Layout, Menu} from 'antd'
|
||||
import './App.css'
|
||||
import {ExportData, GetCapturedData,GetNetworkInterfaces,
|
||||
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 {BrowserRouter as Router, Link, Route, Routes, useLocation} from 'react-router-dom';
|
||||
import CapturePage from './pages/CapturePage';
|
||||
import OptimizerPage from './pages/OptimizerPage';
|
||||
import DatabasePage from './pages/DatabasePage';
|
||||
|
||||
const {Header, Content, Sider} = Layout
|
||||
|
||||
@@ -51,6 +47,9 @@ function AppContent() {
|
||||
<Menu.Item key="/">
|
||||
<Link to="/">抓包</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="/database">
|
||||
<Link to="/database">数据库</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="/optimizer">
|
||||
<Link to="/optimizer">配装优化</Link>
|
||||
</Menu.Item>
|
||||
@@ -59,6 +58,7 @@ function AppContent() {
|
||||
<Content style={{ padding: 0, minHeight: 280 }}>
|
||||
<Routes>
|
||||
<Route path="/" element={<CapturePage />} />
|
||||
<Route path="/database" element={<DatabasePage />} />
|
||||
<Route path="/optimizer" element={<OptimizerPage />} />
|
||||
</Routes>
|
||||
</Content>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {Button, Card, Layout, message, Select, Spin, Table} from 'antd';
|
||||
import {DownloadOutlined, PlayCircleOutlined, SettingOutlined, StopOutlined} from '@ant-design/icons';
|
||||
import {DownloadOutlined, PlayCircleOutlined, SettingOutlined, StopOutlined, UploadOutlined} from '@ant-design/icons';
|
||||
import '../App.css';
|
||||
import {
|
||||
ExportData,
|
||||
GetNetworkInterfaces,
|
||||
ReadRawJsonFile,
|
||||
SaveParsedDataToDatabase,
|
||||
StartCapture,
|
||||
StopAndParseCapture
|
||||
} from '../../wailsjs/go/service/App';
|
||||
@@ -170,50 +170,102 @@ function CapturePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const exportData = async () => {
|
||||
if (!capturedData.length) {
|
||||
const exportData = () => {
|
||||
// 检查是否有解析数据
|
||||
if (!parsedData || !Array.isArray(parsedData.items) || parsedData.items.length === 0) {
|
||||
showMessage('warning', '没有数据可导出');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const filename = `equipment_data_${Date.now()}.json`;
|
||||
const result = await safeApiCall(
|
||||
() => ExportData(capturedData, filename),
|
||||
'导出数据失败'
|
||||
);
|
||||
if (result === undefined) {
|
||||
showMessage('error', '导出数据失败');
|
||||
return;
|
||||
}
|
||||
// 创建要导出的数据内容
|
||||
const exportContent = JSON.stringify(parsedData, null, 2);
|
||||
|
||||
// 创建 Blob 对象
|
||||
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', '数据导出成功');
|
||||
} catch (error) {
|
||||
console.error('导出数据时发生未知错误:', error);
|
||||
console.error('导出数据时发生错误:', error);
|
||||
showMessage('error', '导出数据失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setUploadedFileName(file.name);
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
const text = e.target?.result as string;
|
||||
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 = {
|
||||
items: Array.isArray(json.items) ? json.items : [],
|
||||
heroes: Array.isArray(json.heroes) ? json.heroes : []
|
||||
items: json.items,
|
||||
heroes: json.heroes
|
||||
};
|
||||
setParsedData(safeData);
|
||||
if (safeData.items.length === 0 && safeData.heroes.length === 0) {
|
||||
showMessage('warning', '上传文件数据为空,请检查文件内容');
|
||||
} else {
|
||||
showMessage('success', `文件解析成功:${safeData.items.length}件装备,${safeData.heroes.length}个英雄`);
|
||||
}
|
||||
setUploadedFileName(''); // 清空文件名显示
|
||||
|
||||
showMessage('success', `数据导入成功:${json.items.length}件装备,${json.heroes.length}个英雄,已保存到数据库`);
|
||||
|
||||
} catch (err) {
|
||||
console.error('文件格式错误,无法解析:', err);
|
||||
showMessage('error', '文件格式错误,无法解析');
|
||||
console.error('文件处理错误:', err);
|
||||
if (err instanceof SyntaxError) {
|
||||
showMessage('error', '文件格式错误:不是有效的JSON格式');
|
||||
} else {
|
||||
showMessage('error', '数据导入失败');
|
||||
}
|
||||
setParsedData({ items: [], heroes: [] });
|
||||
}
|
||||
};
|
||||
@@ -315,19 +367,6 @@ function CapturePage() {
|
||||
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>
|
||||
@@ -379,10 +418,28 @@ function CapturePage() {
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={exportData}
|
||||
disabled={!capturedData.length}
|
||||
disabled={!parsedData || !Array.isArray(parsedData.items) || parsedData.items.length === 0}
|
||||
style={{ width: '100%', height: 32, fontSize: 14 }}>
|
||||
导出数据
|
||||
</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>
|
||||
</Card>
|
||||
|
||||
|
||||
203
frontend/src/pages/DatabasePage.tsx
Normal file
203
frontend/src/pages/DatabasePage.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Button, Card, Col, 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';
|
||||
|
||||
// 定义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 (
|
||||
<div style={{ padding: '24px' }}>
|
||||
{/* 统计卡片 */}
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatabasePage;
|
||||
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,
|
||||
};
|
||||
};
|
||||
14
frontend/wailsjs/go/service/App.d.ts
vendored
14
frontend/wailsjs/go/service/App.d.ts
vendored
@@ -2,18 +2,32 @@
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {model} from '../models';
|
||||
|
||||
export function ExportCurrentData(arg1: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 GetCapturedData():Promise<Array<string>>;
|
||||
|
||||
export function GetCurrentDataForExport():Promise<string>;
|
||||
|
||||
export function GetLatestParsedDataFromDatabase():Promise<model.ParsedResult>;
|
||||
|
||||
export function GetNetworkInterfaces():Promise<Array<model.NetworkInterface>>;
|
||||
|
||||
export function ParseData(arg1:Array<string>):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 StopAndParseCapture():Promise<model.ParsedResult>;
|
||||
|
||||
@@ -2,10 +2,22 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function ExportCurrentData(arg1) {
|
||||
return window['go']['service']['App']['ExportCurrentData'](arg1);
|
||||
}
|
||||
|
||||
export function 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() {
|
||||
return window['go']['service']['App']['GetCaptureStatus']();
|
||||
}
|
||||
@@ -14,6 +26,14 @@ export function 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() {
|
||||
return window['go']['service']['App']['GetNetworkInterfaces']();
|
||||
}
|
||||
@@ -26,6 +46,14 @@ export function 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) {
|
||||
return window['go']['service']['App']['StartCapture'](arg1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user