feat(database): 实现数据库功能并优化数据导出

- 新增数据库相关 API 和服务
- 实现数据导出功能,支持导出到 JSON 文件
- 优化数据导入流程,增加数据校验
- 新增数据库页面,展示解析数据和统计信息
- 更新捕获页面,支持导入数据到数据库
This commit is contained in:
hu xiaotong
2025-07-04 12:48:40 +08:00
parent 910e2d4c4d
commit 1b90af57ba
20 changed files with 973 additions and 356312 deletions

View File

@@ -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>

View File

@@ -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>

View 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;

View 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,
};
};

View File

@@ -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>;

View File

@@ -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);
}