This commit is contained in:
hu xiaotong
2025-07-02 16:29:14 +08:00
parent 0246bc7060
commit 50281ce812
3 changed files with 212 additions and 165 deletions

View File

@@ -12,7 +12,8 @@
"antd": "^5.12.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^7.6.3"
"react-router-dom": "^7.6.3",
"zustand": "^5.0.6"
},
"devDependencies": {
"@types/react": "^18.2.43",
@@ -1583,14 +1584,14 @@
"version": "15.7.15",
"resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.23",
"resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.23.tgz",
"integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -4487,6 +4488,35 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zustand": {
"version": "5.0.6",
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.6.tgz",
"integrity": "sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
}
}
}

View File

@@ -14,7 +14,8 @@
"antd": "^5.12.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^7.6.3"
"react-router-dom": "^7.6.3",
"zustand": "^5.0.6"
},
"devDependencies": {
"@types/react": "^18.2.43",

View File

@@ -1,12 +1,19 @@
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 '../App.css'
import {ExportData, GetCapturedData,GetNetworkInterfaces,
ParseData,StartCapture,StopCapture, ReadRawJsonFile
} from "../../wailsjs/go/service/App";
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 '../App.css';
import {
ExportData,
GetCapturedData,
GetNetworkInterfaces,
ParseData,
ReadRawJsonFile,
StartCapture,
StopCapture
} from '../../wailsjs/go/service/App';
import {useCaptureStore} from '../store/useCaptureStore';
const {Header, Content, Sider} = Layout
const { Header, Content, Sider } = Layout;
interface NetworkInterface {
name: string
@@ -35,15 +42,25 @@ interface CaptureResult {
}
function CapturePage() {
const [interfaces, setInterfaces] = useState<NetworkInterface[]>([])
const [selectedInterface, setSelectedInterface] = useState<string>('')
const [isCapturing, setIsCapturing] = useState(false)
const [capturedData, setCapturedData] = useState<string[]>([])
const [parsedData, setParsedData] = useState<CaptureResult | null>(null)
const [loading, setLoading] = useState(false)
const [interfaceLoading, setInterfaceLoading] = useState(false)
const [uploadedFileName, setUploadedFileName] = useState<string>('')
const fileInputRef = useRef<HTMLInputElement>(null)
// 只用全局 parsedData
const parsedData = useCaptureStore(state => state.parsedData);
const setParsedData = useCaptureStore(state => state.setParsedData);
// 其余状态全部本地 useState
const [interfaces, setInterfaces] = useState<NetworkInterface[]>([]);
const [selectedInterface, setSelectedInterface] = useState('');
const [isCapturing, setIsCapturing] = useState(false);
const [capturedData, setCapturedData] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [interfaceLoading, setInterfaceLoading] = useState(false);
const [uploadedFileName, setUploadedFileName] = useState('');
const fileInputRef = useRef<HTMLInputElement>(null);
// 顶部 message 只显示一次
const showMessage = (type: 'success' | 'error' | 'warning', content: string) => {
message.destroy();
message[type](content);
};
const safeApiCall = async <T,>(
apiCall: () => Promise<T>,
@@ -51,20 +68,20 @@ function CapturePage() {
fallbackValue: T
): Promise<T> => {
try {
return await apiCall()
return await apiCall();
} catch (error) {
console.error(`${errorMessage}:`, error)
message.error(errorMessage)
return fallbackValue
console.error(`${errorMessage}:`, error);
showMessage('error', errorMessage);
return fallbackValue;
}
}
};
const fetchInterfaces = async () => {
setIsCapturing(false)
setCapturedData([])
setParsedData(null)
setLoading(false)
setInterfaceLoading(true)
setIsCapturing(false);
setCapturedData([]);
setParsedData(null);
setLoading(false);
setInterfaceLoading(true);
try {
const response = await safeApiCall(
() => GetNetworkInterfaces(),
@@ -74,85 +91,85 @@ function CapturePage() {
{ name: 'wlan0', description: 'Wireless', addresses: ['192.168.1.101'], is_loopback: false },
{ name: 'lo', description: 'Loopback', addresses: ['127.0.0.1'], is_loopback: true }
]
)
setInterfaces(response)
let defaultSelected = ''
);
setInterfaces(response);
let defaultSelected = '';
for (const iface of response) {
if (iface.addresses && iface.addresses.some(ip => ip.includes('192.168'))) {
defaultSelected = iface.name
break
defaultSelected = iface.name;
break;
}
}
if (!defaultSelected && response.length > 0) {
defaultSelected = response[0].name
defaultSelected = response[0].name;
}
setSelectedInterface(defaultSelected)
setSelectedInterface(defaultSelected);
} catch (error) {
console.error('获取网络接口时发生未知错误:', error)
console.error('获取网络接口时发生未知错误:', error);
const defaultInterfaces = [
{ 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 }
]
setInterfaces(defaultInterfaces)
setSelectedInterface(defaultInterfaces[0].name)
];
setInterfaces(defaultInterfaces);
setSelectedInterface(defaultInterfaces[0].name);
} finally {
setInterfaceLoading(false)
setInterfaceLoading(false);
}
}
};
const startCapture = async () => {
if (!selectedInterface) {
message.warning('请选择网络接口')
return
showMessage('warning', '请选择网络接口');
return;
}
setLoading(true)
setLoading(true);
try {
await safeApiCall(
() => StartCapture(selectedInterface),
'开始抓包失败,但界面将继续工作',
undefined
)
setIsCapturing(true)
message.success('开始抓包')
);
setIsCapturing(true);
showMessage('success', '开始抓包');
} catch (error) {
console.error('开始抓包时发生未知错误:', error)
setIsCapturing(true)
message.success('开始抓包(模拟模式)')
console.error('开始抓包时发生未知错误:', error);
setIsCapturing(true);
showMessage('success', '开始抓包(模拟模式)');
} finally {
setLoading(false)
setLoading(false);
}
}
};
const stopCapture = async () => {
setLoading(false)
setIsCapturing(false)
setCapturedData([])
setParsedData(null)
setLoading(false);
setIsCapturing(false);
setCapturedData([]);
setParsedData(null);
try {
setLoading(true)
setLoading(true);
await safeApiCall(
() => StopCapture(),
'停止抓包失败,但界面将继续工作',
undefined
)
setIsCapturing(false)
);
setIsCapturing(false);
const data = await safeApiCall(
() => GetCapturedData(),
'获取抓包数据失败,使用模拟数据',
['mock_data_1', 'mock_data_2', 'mock_data_3']
)
setCapturedData(data)
);
setCapturedData(data);
if (data && data.length > 0) {
data.forEach((item, idx) => {
let hexStr = ''
let hexStr = '';
if (/^[0-9a-fA-F\s]+$/.test(item)) {
hexStr = item
hexStr = item;
} else {
hexStr = Array.from(item).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join(' ')
hexStr = Array.from(item).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join(' ');
}
console.log(`抓包数据[${idx}]:`, hexStr)
})
console.log(`抓包数据[${idx}]:`, hexStr);
});
}
if (data.length > 0) {
const parsed = await safeApiCall(
@@ -166,13 +183,13 @@ function CapturePage() {
],
heroes: []
})
)
);
try {
const parsedData = JSON.parse(parsed)
setParsedData(parsedData)
message.success('数据处理完成')
const parsedData = JSON.parse(parsed);
setParsedData(parsedData);
showMessage('success', '数据处理完成');
} catch (parseError) {
console.error('解析JSON失败:', 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 },
@@ -180,110 +197,109 @@ function CapturePage() {
{ id: '3', code: 'HELMET003', ct: 60, e: 800, g: 3, l: false, mg: 8, op: [], p: 12, s: 'Common', sk: 1 }
],
heroes: []
})
message.success('数据处理完成(使用模拟数据)')
});
showMessage('success', '数据处理完成(使用模拟数据)');
}
} else {
message.warning('未捕获到数据')
showMessage('warning', '未捕获到数据');
}
} catch (error) {
console.error('停止抓包时发生未知错误:', error)
setIsCapturing(false)
setCapturedData([])
setParsedData(null)
setLoading(false)
message.error('抓包失败,已重置状态')
return
console.error('停止抓包时发生未知错误:', error);
setIsCapturing(false);
setCapturedData([]);
setParsedData(null);
setLoading(false);
showMessage('error', '抓包失败,已重置状态');
return;
} finally {
setLoading(false)
setLoading(false);
}
}
};
const exportData = async () => {
if (!capturedData.length) {
message.warning('没有数据可导出')
return
showMessage('warning', '没有数据可导出');
return;
}
try {
const filename = `equipment_data_${Date.now()}.json`
const filename = `equipment_data_${Date.now()}.json`;
await safeApiCall(
() => ExportData(capturedData, filename),
'导出数据失败',
undefined
)
message.success('数据导出成功')
);
showMessage('success', '数据导出成功');
} catch (error) {
console.error('导出数据时发生未知错误:', error)
message.success('数据导出成功(模拟模式)')
console.error('导出数据时发生未知错误:', error);
showMessage('success', '数据导出成功(模拟模式)');
}
}
};
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file) return
setUploadedFileName(file.name)
const reader = new FileReader()
const file = event.target.files?.[0];
if (!file) return;
setUploadedFileName(file.name);
const reader = new FileReader();
reader.onload = (e) => {
try {
const text = e.target?.result as string
const json = JSON.parse(text)
const text = e.target?.result as string;
const json = JSON.parse(text);
const safeData = {
items: Array.isArray(json.items) ? json.items : [],
heroes: Array.isArray(json.heroes) ? json.heroes : []
}
setParsedData(safeData)
};
setParsedData(safeData);
if (safeData.items.length === 0 && safeData.heroes.length === 0) {
message.warning('上传文件数据为空,请检查文件内容')
showMessage('warning', '上传文件数据为空,请检查文件内容');
} else {
message.success(`文件解析成功:${safeData.items.length}件装备,${safeData.heroes.length}个英雄`)
showMessage('success', `文件解析成功:${safeData.items.length}件装备,${safeData.heroes.length}个英雄`);
}
} catch (err) {
console.error('文件格式错误,无法解析:', err)
message.error('文件格式错误,无法解析')
setParsedData({ items: [], heroes: [] })
console.error('文件格式错误,无法解析:', err);
showMessage('error', '文件格式错误,无法解析');
setParsedData({ items: [], heroes: [] });
}
}
reader.readAsText(file)
}
};
reader.readAsText(file);
};
const fetchParsedDataFromBackend = async () => {
setLoading(true)
setLoading(true);
try {
const raw = await ReadRawJsonFile()
const json = JSON.parse(raw)
console.log('已加载本地解析数据:', json)
const raw = await ReadRawJsonFile();
const json = JSON.parse(raw);
console.log('已加载本地解析数据:', json);
const safeData = {
items: Array.isArray(json.items) ? json.items : [],
heroes: Array.isArray(json.heroes) ? json.heroes : []
}
setParsedData(safeData)
};
setParsedData(safeData);
if (safeData.items.length === 0 && safeData.heroes.length === 0) {
message.warning('解析数据为空,请检查数据源')
showMessage('warning', '解析数据为空,请检查数据源');
} else {
message.success(`已加载本地解析数据:${safeData.items.length}件装备,${safeData.heroes.length}个英雄`)
showMessage('success', `已加载本地解析数据:${safeData.items.length}件装备,${safeData.heroes.length}个英雄`);
}
} catch (err) {
console.error('读取本地解析数据失败:', err)
message.error('读取本地解析数据失败')
setParsedData({ items: [], heroes: [] })
console.error('读取本地解析数据失败:', err);
showMessage('error', '读取本地解析数据失败');
setParsedData({ items: [], heroes: [] });
} finally {
setLoading(false)
setLoading(false);
}
}
};
const handleRefreshParsedData = () => {
setParsedData(null)
setUploadedFileName('')
fetchParsedDataFromBackend()
}
setParsedData(null);
setUploadedFileName('');
fetchParsedDataFromBackend();
};
const handleUploadButtonClick = () => {
fileInputRef.current?.click();
}
};
useEffect(() => {
fetchInterfaces();
fetchParsedDataFromBackend();
}, []);
const equipmentColumns = [
@@ -328,107 +344,107 @@ function CapturePage() {
dataIndex: 'sk',
key: 'sk',
},
]
];
return (
<Layout style={{minHeight: '100vh'}}>
<Header style={{background: '#fff', padding: '0 20px'}}>
<div style={{display: 'flex', alignItems: 'center', justifyContent: 'space-between'}}>
<h1 style={{margin: 0}}></h1>
<div style={{display: 'flex', gap: 8, alignItems: 'center'}}>
<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/>}
icon={<SettingOutlined />}
onClick={handleRefreshParsedData}
loading={loading}
style={{flex: 1}}
style={{ flex: 1, height: 32 }}
></Button>
<Button style={{flex: 1}} onClick={handleUploadButtonClick}>
<Button style={{ flex: 1, height: 32 }} onClick={handleUploadButtonClick}>
JSON
</Button>
<input
ref={fileInputRef}
type="file"
accept="application/json"
style={{display: 'none'}}
style={{ display: 'none' }}
onChange={handleFileUpload}
/>
{uploadedFileName && (
<span style={{marginLeft: 8, color: '#888', fontSize: 12}}>{uploadedFileName}</span>
<span style={{ marginLeft: 8, color: '#888', fontSize: 12 }}>{uploadedFileName}</span>
)}
</div>
</div>
</Header>
<Layout>
<Sider width={300} style={{background: '#fff'}}>
<div style={{padding: '20px'}}>
<Sider width={220} style={{ background: '#fff' }}>
<div style={{ padding: '12px' }}>
<Card title="抓包控制" size="small">
<div style={{marginBottom: 16}}>
<div style={{ marginBottom: 12 }}>
<label></label>
<Select
style={{width: '100%', marginTop: 8}}
style={{ width: '100%', marginTop: 6 }}
value={selectedInterface}
onChange={setSelectedInterface}
placeholder="选择网络接口"
loading={interfaceLoading}
>
{interfaces.map(iface => (
{interfaces.map((iface) => (
<Select.Option key={iface.name} value={iface.name}>
{iface.addresses}
{iface.addresses.join(', ')}
</Select.Option>
))}
</Select>
</div>
<div style={{display: 'flex', gap: 8}}>
<div style={{ display: 'flex', gap: 8 }}>
<Button
type="primary"
icon={<PlayCircleOutlined/>}
icon={<PlayCircleOutlined />}
onClick={startCapture}
disabled={isCapturing || !selectedInterface}
loading={loading}
style={{flex: 1}}
style={{ flex: 1, height: 32 }}
>
</Button>
<Button
danger
icon={<StopOutlined/>}
icon={<StopOutlined />}
onClick={stopCapture}
disabled={!isCapturing}
loading={loading}
style={{flex: 1}}
style={{ flex: 1, height: 32 }}
>
</Button>
</div>
<div style={{marginTop: 16}}>
<div style={{ marginTop: 12 }}>
<Button
icon={<DownloadOutlined/>}
icon={<DownloadOutlined />}
onClick={exportData}
disabled={!capturedData.length}
style={{width: '100%'}}>
style={{ width: '100%', height: 32 }}>
</Button>
</div>
</Card>
<Card title="抓包状态" size="small" style={{marginTop: 16}}>
<Card title="抓包状态" size="small" style={{ marginTop: 12 }}>
<div>
<p>: {isCapturing ? '正在抓包...' : '准备就绪'}</p>
<p>: {capturedData.length} </p>
<p style={{ marginBottom: 4 }}>: {isCapturing ? '正在抓包...' : '准备就绪'}</p>
<p style={{ marginBottom: 4 }}>: {capturedData.length} </p>
{parsedData && (
<p>: {parsedData.items.length} </p>
<p style={{ marginBottom: 0 }}>: {parsedData.items.length} </p>
)}
</div>
</Card>
</div>
</Sider>
<Content style={{padding: '20px'}}>
<Content style={{ padding: '16px' }}>
<Spin spinning={loading}>
{parsedData && parsedData.items.length > 0 ? (
<Card title="装备数据">
@@ -436,15 +452,15 @@ function CapturePage() {
dataSource={parsedData.items}
columns={equipmentColumns}
rowKey="id"
pagination={{pageSize: 10}}
scroll={{x: true}}
pagination={{ pageSize: 10 }}
scroll={{ x: true }}
/>
</Card>
) : (
<Card title="数据预览">
<div style={{textAlign: 'center', padding: '40px'}}>
<div style={{ textAlign: 'center', padding: '40px' }}>
<p></p>
<p style={{color: '#999', fontSize: '12px'}}>
<p style={{ color: '#999', fontSize: '12px' }}>
{parsedData ? '数据为空,请检查数据源或上传文件' : '请开始抓包或上传JSON文件'}
</p>
</div>
@@ -454,7 +470,7 @@ function CapturePage() {
</Content>
</Layout>
</Layout>
)
);
}
export default CapturePage
export default CapturePage;