diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5d9d9b2..8ada1be 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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 + } + } } } } diff --git a/frontend/package.json b/frontend/package.json index 0d5f838..3ee64fe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/pages/CapturePage.tsx b/frontend/src/pages/CapturePage.tsx index 8c994a4..784cbde 100644 --- a/frontend/src/pages/CapturePage.tsx +++ b/frontend/src/pages/CapturePage.tsx @@ -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([]) - const [selectedInterface, setSelectedInterface] = useState('') - const [isCapturing, setIsCapturing] = useState(false) - const [capturedData, setCapturedData] = useState([]) - const [parsedData, setParsedData] = useState(null) - const [loading, setLoading] = useState(false) - const [interfaceLoading, setInterfaceLoading] = useState(false) - const [uploadedFileName, setUploadedFileName] = useState('') - const fileInputRef = useRef(null) + // 只用全局 parsedData + const parsedData = useCaptureStore(state => state.parsedData); + const setParsedData = useCaptureStore(state => state.setParsedData); + + // 其余状态全部本地 useState + const [interfaces, setInterfaces] = useState([]); + const [selectedInterface, setSelectedInterface] = useState(''); + const [isCapturing, setIsCapturing] = useState(false); + const [capturedData, setCapturedData] = useState([]); + const [loading, setLoading] = useState(false); + const [interfaceLoading, setInterfaceLoading] = useState(false); + const [uploadedFileName, setUploadedFileName] = useState(''); + const fileInputRef = useRef(null); + + // 顶部 message 只显示一次 + const showMessage = (type: 'success' | 'error' | 'warning', content: string) => { + message.destroy(); + message[type](content); + }; const safeApiCall = async ( apiCall: () => Promise, @@ -51,20 +68,20 @@ function CapturePage() { fallbackValue: T ): Promise => { 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) => { - 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 ( - -
-
-

装备数据导出工具

-
+ +
+
+

装备数据导出工具

+
- {uploadedFileName && ( - {uploadedFileName} + {uploadedFileName} )}
- -
+ +
-
+
-
+
-
+
- +
-

状态: {isCapturing ? '正在抓包...' : '准备就绪'}

-

捕获数据: {capturedData.length} 条

+

状态: {isCapturing ? '正在抓包...' : '准备就绪'}

+

捕获数据: {capturedData.length} 条

{parsedData && ( -

解析装备: {parsedData.items.length} 件

+

解析装备: {parsedData.items.length} 件

)}
- + {parsedData && parsedData.items.length > 0 ? ( @@ -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 }} /> ) : ( -
+

暂无数据

-

+

{parsedData ? '数据为空,请检查数据源或上传文件' : '请开始抓包或上传JSON文件'}

@@ -454,7 +470,7 @@ function CapturePage() { - ) + ); } -export default CapturePage \ No newline at end of file +export default CapturePage; \ No newline at end of file