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