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

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

View File

@@ -1,6 +1,6 @@
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, UploadOutlined} from '@ant-design/icons'; import {DownloadOutlined, PlayCircleOutlined, StopOutlined, UploadOutlined} from '@ant-design/icons';
import '../App.css'; import '../App.css';
import { import {
GetNetworkInterfaces, GetNetworkInterfaces,
@@ -355,132 +355,115 @@ function CapturePage() {
]; ];
return ( return (
<Layout style={{ minHeight: '100vh' }}> <Layout>
<Header style={{ background: '#fff', padding: '0 16px', height: 48, lineHeight: '48px' }}> <Sider width={220} style={{ background: '#f5f5f5' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', height: 48 }}> <div style={{ padding: '16px 0px 12px 12px' }}>
<h1 style={{ margin: 0, fontSize: 20 }}></h1> <Card title="抓包控制" size="small" style={{ marginBottom: 12, marginTop: 0 }}>
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}> <div style={{ marginBottom: 12 }}>
<Button <label></label>
type="primary" <Select
icon={<SettingOutlined />} style={{ width: '100%', marginTop: 6 }}
onClick={handleRefreshParsedData} value={selectedInterface}
loading={loading} onChange={setSelectedInterface}
style={{ flex: 1, height: 32 }} placeholder="选择网络接口"
></Button> loading={interfaceLoading}
</div> >
</div> {interfaces.map((iface) => (
</Header> <Select.Option key={iface.name} value={iface.name}>
{iface.addresses}
</Select.Option>
))}
</Select>
</div>
<Layout> <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<Sider width={220} style={{ background: '#fff' }}> <div style={{ display: 'flex', gap: 8 }}>
<div style={{ padding: '12px' }}> <Button
<Card title="抓包控制" size="small"> type="primary"
<div style={{ marginBottom: 12 }}> icon={<PlayCircleOutlined />}
<label></label> onClick={startCapture}
<Select disabled={isCapturing || !selectedInterface}
style={{ width: '100%', marginTop: 6 }} loading={loading}
value={selectedInterface} style={{ flex: 1, height: 32, minWidth: 0, fontSize: 12 }}
onChange={setSelectedInterface}
placeholder="选择网络接口"
loading={interfaceLoading}
> >
{interfaces.map((iface) => (
<Select.Option key={iface.name} value={iface.name}>
{iface.addresses}
</Select.Option>
))}
</Select>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<div style={{ display: 'flex', gap: 8 }}>
<Button
type="primary"
icon={<PlayCircleOutlined />}
onClick={startCapture}
disabled={isCapturing || !selectedInterface}
loading={loading}
style={{ flex: 1, height: 32, minWidth: 0, fontSize: 12 }}
>
</Button>
<Button
danger
icon={<StopOutlined />}
onClick={stopCapture}
disabled={!isCapturing}
loading={loading}
style={{ flex: 1, height: 32, minWidth: 0, fontSize: 12 }}
>
</Button>
</div>
<Button
icon={<DownloadOutlined />}
onClick={exportData}
disabled={!parsedData || !Array.isArray(parsedData.items) || parsedData.items.length === 0}
style={{ width: '100%', height: 32, fontSize: 14 }}>
</Button> </Button>
<Button <Button
icon={<UploadOutlined />} danger
onClick={handleUploadButtonClick} icon={<StopOutlined />}
style={{ width: '100%', height: 32, fontSize: 14 }}> onClick={stopCapture}
disabled={!isCapturing}
loading={loading}
style={{ flex: 1, height: 32, minWidth: 0, fontSize: 12 }}
>
</Button> </Button>
<input </div>
ref={fileInputRef} <Button
type="file" icon={<DownloadOutlined />}
accept=".json,.txt" onClick={exportData}
style={{ display: 'none' }} disabled={!parsedData || !Array.isArray(parsedData.items) || parsedData.items.length === 0}
onChange={handleFileUpload} style={{ width: '100%', height: 32, fontSize: 14 }}>
/>
{uploadedFileName && ( </Button>
<span style={{ marginTop: 4, color: '#888', fontSize: 10, textAlign: 'center', display: 'block' }}> <Button
{uploadedFileName} icon={<UploadOutlined />}
</span> 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>
<Card title="抓包状态" size="small" style={{ marginTop: 12 }}>
<div>
<p style={{ marginBottom: 4 }}>: {isCapturing ? '正在抓包...' : '准备就绪'}</p>
{/*<p style={{ marginBottom: 4 }}>捕获数据: {capturedData.length} 条</p>*/}
<p style={{ marginBottom: 4 }}>: {Array.isArray(parsedData?.heroes) ? parsedData.heroes.length : 0} </p>
{parsedData && (
<p style={{ marginBottom: 0 }}>: {Array.isArray(parsedData?.items) ? parsedData.items.length : 0} </p>
)}
</div>
</Card>
</div>
</Sider>
<Content style={{ padding: '16px' }}>
<Spin spinning={loading}>
{Array.isArray(parsedData?.items) && parsedData.items.length > 0 ? (
<Card title="装备数据">
<Table
dataSource={parsedData.items}
columns={equipmentColumns}
rowKey="id"
pagination={{ pageSize: 10 }}
scroll={{ x: true }}
/>
</Card>
) : (
<Card title="数据预览">
<div style={{ textAlign: 'center', padding: '40px' }}>
<p></p>
<p style={{ color: '#999', fontSize: '12px' }}>
{parsedData ? '数据为空,请检查数据源或上传文件' : '请开始抓包或上传JSON文件'}
</p>
</div> </div>
</Card> </Card>
)}
<Card title="抓包状态" size="small" style={{ marginTop: 12 }}> </Spin>
<div> </Content>
<p style={{ marginBottom: 4 }}>: {isCapturing ? '正在抓包...' : '准备就绪'}</p>
{/*<p style={{ marginBottom: 4 }}>捕获数据: {capturedData.length} 条</p>*/}
<p style={{ marginBottom: 4 }}>: {Array.isArray(parsedData?.heroes) ? parsedData.heroes.length : 0} </p>
{parsedData && (
<p style={{ marginBottom: 0 }}>: {Array.isArray(parsedData?.items) ? parsedData.items.length : 0} </p>
)}
</div>
</Card>
</div>
</Sider>
<Content style={{ padding: '16px' }}>
<Spin spinning={loading}>
{Array.isArray(parsedData?.items) && parsedData.items.length > 0 ? (
<Card title="装备数据">
<Table
dataSource={parsedData.items}
columns={equipmentColumns}
rowKey="id"
pagination={{ pageSize: 10 }}
scroll={{ x: true }}
/>
</Card>
) : (
<Card title="数据预览">
<div style={{ textAlign: 'center', padding: '40px' }}>
<p></p>
<p style={{ color: '#999', fontSize: '12px' }}>
{parsedData ? '数据为空,请检查数据源或上传文件' : '请开始抓包或上传JSON文件'}
</p>
</div>
</Card>
)}
</Spin>
</Content>
</Layout>
</Layout> </Layout>
); );
} }

View File

@@ -1,10 +1,12 @@
import React, {useEffect, useState} from 'react'; import React, {useEffect, useState} from 'react';
import {Button, Card, Col, Row, Space, Statistic, Table, Tag,} from 'antd'; import {Button, Card, Col, Layout, Row, Space, Statistic, Table, Tag} from 'antd';
import {BarChartOutlined, DatabaseOutlined, ReloadOutlined, SettingOutlined,} from '@ant-design/icons'; import {BarChartOutlined, DatabaseOutlined, ReloadOutlined, SettingOutlined,} from '@ant-design/icons';
import * as App from '../../wailsjs/go/service/App'; import * as App from '../../wailsjs/go/service/App';
import {model} from '../../wailsjs/go/models'; import {model} from '../../wailsjs/go/models';
import {useMessage} from '../utils/useMessage'; import {useMessage} from '../utils/useMessage';
const { Content } = Layout;
// 定义Equipment接口 // 定义Equipment接口
interface Equipment { interface Equipment {
id: string | number; id: string | number;
@@ -121,82 +123,84 @@ const DatabasePage: React.FC = () => {
]; ];
return ( return (
<div style={{ padding: '24px' }}> <Layout style={{ minHeight: '100vh' }}>
{/* 统计卡片 */} <Content style={{ padding: 16 }}>
<Row gutter={16} style={{ marginBottom: '24px' }}> {/* 统计卡片 */}
<Col span={8}> <Row gutter={16} style={{ marginBottom: '24px' }}>
<Card> <Col span={8}>
<Statistic <Card>
title="数据库状态" <Statistic
value="SQLite" title="数据库状态"
prefix={<DatabaseOutlined />} value="SQLite"
suffix="已连接" prefix={<DatabaseOutlined />}
/> suffix="已连接"
</Card> />
</Col> </Card>
<Col span={8}> </Col>
<Card> <Col span={8}>
<Statistic <Card>
title="装备数量" <Statistic
value={latestData?.items?.length || 0} title="装备数量"
prefix={<BarChartOutlined />} value={latestData?.items?.length || 0}
/> prefix={<BarChartOutlined />}
</Card> />
</Col> </Card>
<Col span={8}> </Col>
<Card> <Col span={8}>
<Statistic <Card>
title="英雄数量" <Statistic
value={latestData?.heroes?.length || 0} title="英雄数量"
prefix={<SettingOutlined />} value={latestData?.heroes?.length || 0}
/> prefix={<SettingOutlined />}
</Card> />
</Col> </Card>
</Row> </Col>
</Row>
{/* 操作栏 */} {/* 操作栏 */}
<Card style={{ marginBottom: '16px' }}> <Card style={{ marginBottom: '16px' }}>
<Space> <Space>
<Button <Button
type="primary" type="primary"
icon={<ReloadOutlined />} icon={<ReloadOutlined />}
onClick={loadLatestData} onClick={loadLatestData}
loading={loading} loading={loading}
> >
</Button> </Button>
<span style={{ color: '#666' }}> <span style={{ color: '#666' }}>
</span> </span>
</Space> </Space>
</Card> </Card>
{/* 装备表格 */} {/* 装备表格 */}
<Card title="最新解析的装备数据"> <Card title="最新解析的装备数据">
{latestData?.items && latestData.items.length > 0 ? ( {latestData?.items && latestData.items.length > 0 ? (
<Table <Table
columns={equipmentColumns} columns={equipmentColumns}
dataSource={latestData.items} dataSource={latestData.items}
rowKey="id" rowKey="id"
loading={loading} loading={loading}
pagination={{ pagination={{
showSizeChanger: true, showSizeChanger: true,
showQuickJumper: true, showQuickJumper: true,
showTotal: (total, range) => showTotal: (total, range) =>
`${range[0]}-${range[1]} 条,共 ${total}`, `${range[0]}-${range[1]} 条,共 ${total}`,
}} }}
scroll={{ x: 800 }} scroll={{ x: 800 }}
/> />
) : ( ) : (
<div style={{ textAlign: 'center', padding: '40px' }}> <div style={{ textAlign: 'center', padding: '40px' }}>
<p></p> <p></p>
<p style={{ color: '#999', fontSize: '12px' }}> <p style={{ color: '#999', fontSize: '12px' }}>
</p> </p>
</div> </div>
)} )}
</Card> </Card>
</div> </Content>
</Layout>
); );
}; };

View File

@@ -1,12 +1,140 @@
import React from 'react'; import React from 'react';
import {Avatar, Button, Card, Col, Divider, Input, Layout, Pagination, Row, Select, Table, Tag} from 'antd';
import {AppstoreOutlined, FilterOutlined, UserOutlined} from '@ant-design/icons';
function OptimizerPage() { 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() {
return ( return (
<div style={{ padding: 24 }}> <Layout style={{ minHeight: '100vh' }}>
<h2></h2> <Content style={{ padding: 16 }}>
<p></p> {/* 顶部选项区 */}
</div> <Card style={{ marginBottom: 16 }}>
<Row gutter={16}>
{/* 角色头像与选择 */}
<Col span={4} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<Avatar size={64} src={hero.avatar} icon={<UserOutlined />} />
<div style={{ marginTop: 8, fontWeight: 500 }}>{hero.name}</div>
<Select style={{ width: '100%', marginTop: 8 }} defaultValue={hero.id}>
{heroList.map(h => <Select.Option key={h.id} value={h.id}>{h.name}</Select.Option>)}
</Select>
</Col>
{/* 角色属性 */}
<Col span={6}>
<div style={{ fontWeight: 500, marginBottom: 8 }}></div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{attributes.map(attr => (
<div key={attr.label} style={{ minWidth: 60 }}>{attr.label}: <b>{attr.value}</b></div>
))}
</div>
</Col>
{/* 属性过滤 */}
<Col span={7}>
<div style={{ fontWeight: 500, marginBottom: 8 }}></div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
{filterOptions.map(opt => (
<Input key={opt.value} addonBefore={opt.label} placeholder="最小值" style={{ width: 100 }} />
))}
</div>
</Col>
{/* 套装选择与操作 */}
<Col span={7}>
<div style={{ fontWeight: 500, marginBottom: 8 }}></div>
<Select 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>
<div style={{ display: 'flex', gap: 8 }}>
<Button type="primary" icon={<AppstoreOutlined />}></Button>
<Button icon={<FilterOutlined />}></Button>
</div>
</Col>
</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>2000150020000200</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>
</Content>
</Layout>
); );
} }
export default OptimizerPage;