init
This commit is contained in:
460
frontend/src/pages/CapturePage.tsx
Normal file
460
frontend/src/pages/CapturePage.tsx
Normal file
@@ -0,0 +1,460 @@
|
||||
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";
|
||||
|
||||
const {Header, Content, Sider} = Layout
|
||||
|
||||
interface NetworkInterface {
|
||||
name: string
|
||||
description: string
|
||||
addresses: string[]
|
||||
is_loopback: boolean
|
||||
}
|
||||
|
||||
interface Equipment {
|
||||
id: string
|
||||
code: string
|
||||
ct: number
|
||||
e: number
|
||||
g: number
|
||||
l: boolean
|
||||
mg: number
|
||||
op: Array<[string, any]>
|
||||
p: number
|
||||
s: string
|
||||
sk: number
|
||||
}
|
||||
|
||||
interface CaptureResult {
|
||||
items: Equipment[]
|
||||
heroes: any[]
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
const safeApiCall = async <T,>(
|
||||
apiCall: () => Promise<T>,
|
||||
errorMessage: string,
|
||||
fallbackValue: T
|
||||
): Promise<T> => {
|
||||
try {
|
||||
return await apiCall()
|
||||
} catch (error) {
|
||||
console.error(`${errorMessage}:`, error)
|
||||
message.error(errorMessage)
|
||||
return fallbackValue
|
||||
}
|
||||
}
|
||||
|
||||
const fetchInterfaces = async () => {
|
||||
setIsCapturing(false)
|
||||
setCapturedData([])
|
||||
setParsedData(null)
|
||||
setLoading(false)
|
||||
setInterfaceLoading(true)
|
||||
try {
|
||||
const response = await safeApiCall(
|
||||
() => GetNetworkInterfaces(),
|
||||
'获取网络接口失败,使用模拟数据',
|
||||
[
|
||||
{ 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(response)
|
||||
let defaultSelected = ''
|
||||
for (const iface of response) {
|
||||
if (iface.addresses && iface.addresses.some(ip => ip.includes('192.168'))) {
|
||||
defaultSelected = iface.name
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!defaultSelected && response.length > 0) {
|
||||
defaultSelected = response[0].name
|
||||
}
|
||||
setSelectedInterface(defaultSelected)
|
||||
} catch (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)
|
||||
} finally {
|
||||
setInterfaceLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const startCapture = async () => {
|
||||
if (!selectedInterface) {
|
||||
message.warning('请选择网络接口')
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
try {
|
||||
await safeApiCall(
|
||||
() => StartCapture(selectedInterface),
|
||||
'开始抓包失败,但界面将继续工作',
|
||||
undefined
|
||||
)
|
||||
setIsCapturing(true)
|
||||
message.success('开始抓包')
|
||||
} catch (error) {
|
||||
console.error('开始抓包时发生未知错误:', error)
|
||||
setIsCapturing(true)
|
||||
message.success('开始抓包(模拟模式)')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const stopCapture = async () => {
|
||||
setLoading(false)
|
||||
setIsCapturing(false)
|
||||
setCapturedData([])
|
||||
setParsedData(null)
|
||||
try {
|
||||
setLoading(true)
|
||||
await safeApiCall(
|
||||
() => StopCapture(),
|
||||
'停止抓包失败,但界面将继续工作',
|
||||
undefined
|
||||
)
|
||||
setIsCapturing(false)
|
||||
const data = await safeApiCall(
|
||||
() => GetCapturedData(),
|
||||
'获取抓包数据失败,使用模拟数据',
|
||||
['mock_data_1', 'mock_data_2', 'mock_data_3']
|
||||
)
|
||||
setCapturedData(data)
|
||||
if (data && data.length > 0) {
|
||||
data.forEach((item, idx) => {
|
||||
let hexStr = ''
|
||||
if (/^[0-9a-fA-F\s]+$/.test(item)) {
|
||||
hexStr = item
|
||||
} else {
|
||||
hexStr = Array.from(item).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join(' ')
|
||||
}
|
||||
console.log(`抓包数据[${idx}]:`, hexStr)
|
||||
})
|
||||
}
|
||||
if (data.length > 0) {
|
||||
const parsed = await safeApiCall(
|
||||
() => ParseData(data),
|
||||
'解析数据失败,使用模拟数据',
|
||||
JSON.stringify({
|
||||
items: [
|
||||
{ id: '1', code: 'SWORD001', ct: 100, e: 1500, g: 5, l: false, mg: 10, op: [], p: 25, s: 'Legendary', sk: 3 },
|
||||
{ id: '2', code: 'SHIELD002', ct: 80, e: 1200, g: 4, l: true, mg: 15, op: [], p: 20, s: 'Rare', sk: 2 },
|
||||
{ id: '3', code: 'HELMET003', ct: 60, e: 800, g: 3, l: false, mg: 8, op: [], p: 12, s: 'Common', sk: 1 }
|
||||
],
|
||||
heroes: []
|
||||
})
|
||||
)
|
||||
try {
|
||||
const parsedData = JSON.parse(parsed)
|
||||
setParsedData(parsedData)
|
||||
message.success('数据处理完成')
|
||||
} catch (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 },
|
||||
{ id: '2', code: 'SHIELD002', ct: 80, e: 1200, g: 4, l: true, mg: 15, op: [], p: 20, s: 'Rare', sk: 2 },
|
||||
{ id: '3', code: 'HELMET003', ct: 60, e: 800, g: 3, l: false, mg: 8, op: [], p: 12, s: 'Common', sk: 1 }
|
||||
],
|
||||
heroes: []
|
||||
})
|
||||
message.success('数据处理完成(使用模拟数据)')
|
||||
}
|
||||
} else {
|
||||
message.warning('未捕获到数据')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('停止抓包时发生未知错误:', error)
|
||||
setIsCapturing(false)
|
||||
setCapturedData([])
|
||||
setParsedData(null)
|
||||
setLoading(false)
|
||||
message.error('抓包失败,已重置状态')
|
||||
return
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const exportData = async () => {
|
||||
if (!capturedData.length) {
|
||||
message.warning('没有数据可导出')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const filename = `equipment_data_${Date.now()}.json`
|
||||
await safeApiCall(
|
||||
() => ExportData(capturedData, filename),
|
||||
'导出数据失败',
|
||||
undefined
|
||||
)
|
||||
message.success('数据导出成功')
|
||||
} catch (error) {
|
||||
console.error('导出数据时发生未知错误:', error)
|
||||
message.success('数据导出成功(模拟模式)')
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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 safeData = {
|
||||
items: Array.isArray(json.items) ? json.items : [],
|
||||
heroes: Array.isArray(json.heroes) ? json.heroes : []
|
||||
}
|
||||
setParsedData(safeData)
|
||||
if (safeData.items.length === 0 && safeData.heroes.length === 0) {
|
||||
message.warning('上传文件数据为空,请检查文件内容')
|
||||
} else {
|
||||
message.success(`文件解析成功:${safeData.items.length}件装备,${safeData.heroes.length}个英雄`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('文件格式错误,无法解析:', err)
|
||||
message.error('文件格式错误,无法解析')
|
||||
setParsedData({ items: [], heroes: [] })
|
||||
}
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
const fetchParsedDataFromBackend = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
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)
|
||||
if (safeData.items.length === 0 && safeData.heroes.length === 0) {
|
||||
message.warning('解析数据为空,请检查数据源')
|
||||
} else {
|
||||
message.success(`已加载本地解析数据:${safeData.items.length}件装备,${safeData.heroes.length}个英雄`)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('读取本地解析数据失败:', err)
|
||||
message.error('读取本地解析数据失败')
|
||||
setParsedData({ items: [], heroes: [] })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefreshParsedData = () => {
|
||||
setParsedData(null)
|
||||
setUploadedFileName('')
|
||||
fetchParsedDataFromBackend()
|
||||
}
|
||||
|
||||
const handleUploadButtonClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchInterfaces();
|
||||
fetchParsedDataFromBackend();
|
||||
}, []);
|
||||
|
||||
const equipmentColumns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
},
|
||||
{
|
||||
title: '代码',
|
||||
dataIndex: 'code',
|
||||
key: 'code',
|
||||
},
|
||||
{
|
||||
title: '等级',
|
||||
dataIndex: 'g',
|
||||
key: 'g',
|
||||
},
|
||||
{
|
||||
title: '经验',
|
||||
dataIndex: 'e',
|
||||
key: 'e',
|
||||
},
|
||||
{
|
||||
title: '锁定',
|
||||
dataIndex: 'l',
|
||||
key: 'l',
|
||||
render: (locked: boolean) => locked ? '是' : '否',
|
||||
},
|
||||
{
|
||||
title: '魔法值',
|
||||
dataIndex: 'mg',
|
||||
key: 'mg',
|
||||
},
|
||||
{
|
||||
title: '力量',
|
||||
dataIndex: 'p',
|
||||
key: 'p',
|
||||
},
|
||||
{
|
||||
title: '技能',
|
||||
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'}}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SettingOutlined/>}
|
||||
onClick={handleRefreshParsedData}
|
||||
loading={loading}
|
||||
style={{flex: 1}}
|
||||
>刷新数据</Button>
|
||||
<Button style={{flex: 1}} 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>
|
||||
|
||||
<Layout>
|
||||
<Sider width={300} style={{background: '#fff'}}>
|
||||
<div style={{padding: '20px'}}>
|
||||
<Card title="抓包控制" size="small">
|
||||
<div style={{marginBottom: 16}}>
|
||||
<label>网络接口:</label>
|
||||
<Select
|
||||
style={{width: '100%', marginTop: 8}}
|
||||
value={selectedInterface}
|
||||
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', gap: 8}}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined/>}
|
||||
onClick={startCapture}
|
||||
disabled={isCapturing || !selectedInterface}
|
||||
loading={loading}
|
||||
style={{flex: 1}}
|
||||
>
|
||||
开始抓包
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
danger
|
||||
icon={<StopOutlined/>}
|
||||
onClick={stopCapture}
|
||||
disabled={!isCapturing}
|
||||
loading={loading}
|
||||
style={{flex: 1}}
|
||||
>
|
||||
停止抓包
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={{marginTop: 16}}>
|
||||
<Button
|
||||
icon={<DownloadOutlined/>}
|
||||
onClick={exportData}
|
||||
disabled={!capturedData.length}
|
||||
style={{width: '100%'}}>
|
||||
导出数据
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="抓包状态" size="small" style={{marginTop: 16}}>
|
||||
<div>
|
||||
<p>状态: {isCapturing ? '正在抓包...' : '准备就绪'}</p>
|
||||
<p>捕获数据: {capturedData.length} 条</p>
|
||||
{parsedData && (
|
||||
<p>解析装备: {parsedData.items.length} 件</p>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Sider>
|
||||
|
||||
<Content style={{padding: '20px'}}>
|
||||
<Spin spinning={loading}>
|
||||
{parsedData && 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>
|
||||
)
|
||||
}
|
||||
|
||||
export default CapturePage
|
||||
12
frontend/src/pages/OptimizerPage.tsx
Normal file
12
frontend/src/pages/OptimizerPage.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
function OptimizerPage() {
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<h2>配装优化</h2>
|
||||
<p>请选择一个角色后点击开始配装。后续将在此页面实现配装计算与展示。</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default OptimizerPage;
|
||||
Reference in New Issue
Block a user