feat(database): add CRUD operations for parsed sessions and update session name functionality

This commit is contained in:
kever
2026-02-16 00:39:24 +08:00
parent 41814a2bc8
commit f0a26e31f9
18 changed files with 1119 additions and 1573 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
98e3c997ed559906a235af48ee6be17d
340ef73da49fc2e1f0f32752fcad55cd

View File

@@ -1,24 +1,19 @@
import React, {useEffect, useRef, useState} from 'react';
import {Button, Card, Layout, message, Select, Spin, Table} from 'antd';
import {Button, Card, Layout, message, Spin, Table} from 'antd';
import {DownloadOutlined, PlayCircleOutlined, StopOutlined, UploadOutlined} from '@ant-design/icons';
import '../App.css';
import {
GetNetworkInterfaces,
ReadRawJsonFile,
SaveParsedDataToDatabase,
StartCapture,
StartCaptureWithFilter,
StopCapture,
StopAndParseCapture
} from '../../wailsjs/go/service/App';
import { GetCaptureStatus } from '../../wailsjs/go/service/App';
import { EventsOn } from '../../wailsjs/runtime';
import {useCaptureStore} from '../store/useCaptureStore';
const { Header, Content, Sider } = Layout;
interface NetworkInterface {
name: string
description: string
addresses: string[]
is_loopback: boolean
}
const { Content, Sider } = Layout;
interface Equipment {
id: string
@@ -40,21 +35,15 @@ interface CaptureResult {
}
function CapturePage() {
// 只用全局 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 [statusText, setStatusText] = useState('准备就绪');
const fileInputRef = useRef<HTMLInputElement>(null);
// 顶部 message 只显示一次
const showMessage = (type: 'success' | 'error' | 'warning', content: string) => {
message.destroy();
message[type](content);
@@ -73,65 +62,91 @@ function CapturePage() {
}
};
const fetchInterfaces = async () => {
const resetUiState = () => {
setIsCapturing(false);
setCapturedData([]);
setLoading(false);
setInterfaceLoading(true);
try {
const response = await safeApiCall(
() => GetNetworkInterfaces(),
'获取网络接口失败'
);
if (!response || response.length === 0) {
showMessage('error', '未获取到任何网络接口');
setInterfaces([]);
setSelectedInterface('');
return;
}
setInterfaces(response);
let defaultSelected = '';
console.log("获取的网卡:"+JSON.stringify(response))
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);
showMessage('error', '获取网络接口时发生未知错误');
setInterfaces([]);
setSelectedInterface('');
} finally {
setInterfaceLoading(false);
}
setUploadedFileName('');
setParsedData({ items: [], heroes: [] } as CaptureResult);
setStatusText('准备就绪');
};
useEffect(() => {
let cancelled = false;
const syncCaptureStatus = async () => {
resetUiState();
try {
const status = await GetCaptureStatus();
if (!cancelled && status?.status === 'starting') {
setIsCapturing(true);
setStatusText('启动中...');
} else if (!cancelled && status?.is_capturing) {
setIsCapturing(true);
setStatusText('抓包中');
}
} catch (err) {
// ignore status sync errors
}
};
syncCaptureStatus();
return () => {
// Leave capture page: ensure backend is not left running
StopCapture().catch(() => {});
cancelled = true;
};
}, []);
useEffect(() => {
const off = EventsOn('capture:ready_to_parse', () => {
setStatusText('抓包成功,待解析数据');
});
const offStarted = EventsOn('capture:started', () => {
setIsCapturing(true);
setStatusText('抓包中');
});
const offStartFailed = EventsOn('capture:start_failed', () => {
setIsCapturing(false);
setStatusText('准备就绪');
showMessage('error', '开始抓包失败');
});
return () => {
off();
offStarted();
offStartFailed();
};
}, []);
const startCapture = async () => {
if (!selectedInterface) {
showMessage('warning', '请选择网络接口');
return;
}
setLoading(true);
try {
const result = await safeApiCall(
() => StartCapture(selectedInterface),
'开始抓包失败'
);
if (result === undefined) {
// Best-effort reset to avoid "capture already running"
await StopCapture().catch(() => {});
setParsedData({ items: [], heroes: [] } as CaptureResult);
setIsCapturing(true);
setStatusText('启动中...');
try {
console.log('StartCaptureWithFilter calling', new Date().toISOString());
await StartCaptureWithFilter('', '');
console.log('StartCaptureWithFilter returned', new Date().toISOString());
showMessage('success', '开始抓包');
} catch (err) {
const msg = String(err);
if (msg.includes('capture already running')) {
// 后端仍在抓包,允许前端停止
setIsCapturing(true);
setStatusText('抓包中');
showMessage('warning', '抓包已在进行');
return;
}
setIsCapturing(false);
setStatusText('准备就绪');
showMessage('error', '开始抓包失败');
return;
}
setIsCapturing(true);
showMessage('success', '开始抓包');
} catch (error) {
console.error('开始抓包时发生未知错误:', error);
console.error('开始抓包出错:', error);
setIsCapturing(false);
setStatusText('准备就绪');
showMessage('error', '开始抓包失败');
} finally {
setLoading(false);
@@ -139,31 +154,45 @@ function CapturePage() {
};
const stopCapture = async () => {
console.log('stopCapture clicked', new Date().toISOString());
setLoading(false);
setIsCapturing(false);
setCapturedData([]);
setStatusText('抓取中...');
try {
setLoading(true);
// 新接口:直接停止抓包并解析
const parsedData = await safeApiCall(
() => StopAndParseCapture(),
'停止抓包并解析数据失败'
);
console.log("解析数据:"+JSON.stringify(parsedData))
console.log('StopAndParseCapture calling', new Date().toISOString());
const parsedData = await StopAndParseCapture();
console.log('StopAndParseCapture returned', new Date().toISOString());
if (!parsedData || !Array.isArray((parsedData as CaptureResult).items)) {
setParsedData({ items: [], heroes: [] } as CaptureResult);
showMessage('error', '解析数据失败');
setIsCapturing(false);
setStatusText('解析失败');
showMessage('error', '解析失败');
return;
}
setParsedData(parsedData as CaptureResult);
setIsCapturing(false);
setStatusText('导入成功');
showMessage('success', '数据处理完成');
} catch (error) {
console.error('停止抓包时发生未知错误:', error);
setIsCapturing(false);
setCapturedData([]);
const errMsg = String(error);
console.error('停止抓包出错:', error);
setParsedData({ items: [], heroes: [] } as CaptureResult);
setLoading(false);
showMessage('error', '抓包失败,已重置状态');
if (errMsg.includes('capture starting')) {
setIsCapturing(true);
setStatusText('启动中,停止已请求');
showMessage('warning', '启动中,停止已请求');
return;
}
if (errMsg.includes('no captured data') || errMsg.includes('没有数据')) {
setIsCapturing(false);
setStatusText('未获取到任何数据');
showMessage('error', '未获取到任何数据');
return;
}
setIsCapturing(false);
setStatusText('抓包失败');
showMessage('error', '抓包失败');
return;
} finally {
setLoading(false);
@@ -171,37 +200,26 @@ function CapturePage() {
};
const exportData = () => {
// 检查是否有解析数据
if (!parsedData || !Array.isArray(parsedData.items) || parsedData.items.length === 0) {
showMessage('warning', '没有数据可导出');
return;
}
try {
// 创建要导出的数据内容
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);
showMessage('error', '导出数据失败');
console.error('导出数据出错:', error);
showMessage('error', '数据导出失败');
}
};
@@ -216,7 +234,6 @@ function CapturePage() {
const text = e.target?.result as string;
const json = JSON.parse(text);
// 校验数据格式
if (!json || typeof json !== 'object') {
showMessage('error', '文件格式错误不是有效的JSON对象');
return;
@@ -242,23 +259,20 @@ function CapturePage() {
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: json.items,
heroes: json.heroes
};
setParsedData(safeData);
setUploadedFileName(''); // 清空文件名显示
showMessage('success', `数据导入成功:${json.items.length}件装备,${json.heroes.length}个英雄,已保存到数据库`);
setUploadedFileName('');
showMessage('success', `导入成功:${json.items.length}件装备,${json.heroes.length}个英雄`);
} catch (err) {
console.error('文件处理错误:', err);
if (err instanceof SyntaxError) {
@@ -276,7 +290,6 @@ function CapturePage() {
setLoading(true);
try {
const json = await ReadRawJsonFile();
console.log('已加载本地解析数据:', json);
const safeData = {
items: Array.isArray(json.items) ? json.items : [],
heroes: Array.isArray(json.heroes) ? json.heroes : []
@@ -306,52 +319,15 @@ function CapturePage() {
fileInputRef.current?.click();
};
useEffect(() => {
fetchInterfaces();
}, []);
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',
},
{ 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 (
@@ -359,30 +335,13 @@ function CapturePage() {
<Sider width={220} style={{ background: '#f5f5f5' }}>
<div style={{ padding: '16px 0px 12px 12px' }}>
<Card title="抓包控制" size="small" style={{ marginBottom: 12, marginTop: 0 }}>
<div style={{ marginBottom: 12 }}>
<label></label>
<Select
style={{ width: '100%', marginTop: 6 }}
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', flexDirection: 'column', gap: 8 }}>
<div style={{ display: 'flex', gap: 8 }}>
<Button
type="primary"
icon={<PlayCircleOutlined />}
onClick={startCapture}
disabled={isCapturing || !selectedInterface}
disabled={isCapturing}
loading={loading}
style={{ flex: 1, height: 32, minWidth: 0, fontSize: 12 }}
>
@@ -393,7 +352,6 @@ function CapturePage() {
icon={<StopOutlined />}
onClick={stopCapture}
disabled={!isCapturing}
loading={loading}
style={{ flex: 1, height: 32, minWidth: 0, fontSize: 12 }}
>
@@ -429,8 +387,19 @@ function CapturePage() {
<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 }}>
: <span style={{
color: (
statusText === '未获取到任何数据' ||
statusText === '解析失败' ||
statusText === '抓包失败'
)
? '#d32029'
: (statusText === '抓包成功,待解析数据' || statusText === '导入成功')
? '#389e0d'
: undefined
}}>{statusText}</span>
</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>
@@ -457,7 +426,7 @@ function CapturePage() {
<div style={{ textAlign: 'center', padding: '40px' }}>
<p></p>
<p style={{ color: '#999', fontSize: '12px' }}>
{parsedData ? '数据为空,请检查数据源或上传文件' : '请开始抓包或上传JSON文件'}
{parsedData ? '数据为空,请检查数据源或导入文件' : '请开始抓包或导入JSON文件'}
</p>
</div>
</Card>
@@ -468,4 +437,4 @@ function CapturePage() {
);
}
export default CapturePage;
export default CapturePage;

View File

@@ -1,5 +1,5 @@
import React, {useEffect, useState} from 'react';
import {Button, Card, Col, Layout, Row, Space, Statistic, Table, Tag} from 'antd';
import {Button, Card, Col, Input, Layout, Modal, Row, Select, 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';
@@ -25,16 +25,24 @@ interface Equipment {
const DatabasePage: React.FC = () => {
const [loading, setLoading] = useState(false);
const [latestData, setLatestData] = useState<model.ParsedResult | null>(null);
const [sessions, setSessions] = useState<model.ParsedSession[]>([]);
const [selectedSessionId, setSelectedSessionId] = useState<number | null>(null);
const [renameOpen, setRenameOpen] = useState(false);
const [renameValue, setRenameValue] = useState('');
const { success, error, info } = useMessage();
// 加载最新解析数据
const loadLatestData = async () => {
// 防止重复调用
if (loading) return;
setLoading(true);
const formatSessionLabel = (session: model.ParsedSession) => {
const date = session.created_at ? new Date(session.created_at * 1000) : null;
const timeText = date ? date.toLocaleString() : '';
return timeText ? `${session.session_name}${timeText}` : session.session_name;
};
const loadParsedDataById = async (id: number, skipLoading?: boolean) => {
if (!skipLoading) {
setLoading(true);
}
try {
const parsedResult = await App.GetLatestParsedDataFromDatabase();
const parsedResult = await App.GetParsedDataByID(id);
if (parsedResult && (parsedResult.items?.length > 0 || parsedResult.heroes?.length > 0)) {
setLatestData(parsedResult);
success('数据加载成功');
@@ -46,13 +54,101 @@ const DatabasePage: React.FC = () => {
error('加载数据失败');
console.error('Load data error:', err);
setLatestData(null);
} finally {
if (!skipLoading) {
setLoading(false);
}
}
};
const loadSessions = async (preferLatest?: boolean) => {
if (loading) return;
setLoading(true);
try {
const list = await App.GetParsedSessions();
setSessions(list || []);
if (list && list.length > 0) {
const nextId = preferLatest ? list[0].id : (selectedSessionId ?? list[0].id);
setSelectedSessionId(nextId);
await loadParsedDataById(nextId, true);
} else {
setLatestData(null);
info('暂无解析数据');
}
} catch (err) {
error('加载数据失败');
console.error('Load data error:', err);
setLatestData(null);
} finally {
setLoading(false);
}
};
const openRename = () => {
if (!selectedSessionId) {
info('请先选择一条解析数据');
return;
}
const current = sessions.find(s => s.id === selectedSessionId);
setRenameValue(current?.session_name || '');
setRenameOpen(true);
};
const handleRenameOk = async () => {
if (!selectedSessionId) {
setRenameOpen(false);
return;
}
const nextName = renameValue.trim();
if (!nextName) {
info('名称不能为空');
return;
}
try {
setLoading(true);
await App.UpdateParsedSessionName(selectedSessionId, nextName);
success('名称已更新');
setRenameOpen(false);
await loadSessions();
} catch (err) {
error('更新名称失败');
console.error('Rename error:', err);
} finally {
setLoading(false);
}
};
const handleDelete = async () => {
if (!selectedSessionId) {
info('请先选择一条解析数据');
return;
}
Modal.confirm({
title: '确认删除该解析数据?',
content: '删除后无法恢复',
okText: '删除',
cancelText: '取消',
okButtonProps: { danger: true },
onOk: async () => {
try {
setLoading(true);
await App.DeleteParsedSession(selectedSessionId);
success('已删除');
setRenameOpen(false);
setSelectedSessionId(null);
await loadSessions(true);
} catch (err) {
error('删除失败');
console.error('Delete error:', err);
} finally {
setLoading(false);
}
}
});
};
useEffect(() => {
loadLatestData();
loadSessions();
}, []);
// 装备表格列定义
@@ -163,19 +259,31 @@ const DatabasePage: React.FC = () => {
<Button
type="primary"
icon={<ReloadOutlined />}
onClick={loadLatestData}
onClick={loadSessions}
loading={loading}
>
</Button>
<span style={{ color: '#666' }}>
</span>
<Select
style={{ minWidth: 320 }}
placeholder="请选择解析数据"
value={selectedSessionId ?? undefined}
onChange={(value: number) => {
setSelectedSessionId(value);
loadParsedDataById(value);
}}
options={sessions.map(s => ({
value: s.id,
label: formatSessionLabel(s),
}))}
/>
<Button onClick={openRename}></Button>
<Button danger onClick={handleDelete}></Button>
</Space>
</Card>
{/* 装备表格 */}
<Card title="最新解析的装备数据">
<Card title="解析的装备数据">
{latestData?.items && latestData.items.length > 0 ? (
<Table
columns={equipmentColumns}
@@ -199,9 +307,24 @@ const DatabasePage: React.FC = () => {
</div>
)}
</Card>
<Modal
title="重命名解析数据"
open={renameOpen}
onOk={handleRenameOk}
onCancel={() => setRenameOpen(false)}
okText="保存"
cancelText="取消"
>
<Input
value={renameValue}
onChange={e => setRenameValue(e.target.value)}
placeholder="请输入名称"
maxLength={50}
/>
</Modal>
</Content>
</Layout>
);
};
export default DatabasePage;
export default DatabasePage;

View File

@@ -48,6 +48,22 @@ export namespace model {
this.heroes = source["heroes"];
}
}
export class ParsedSession {
id: number;
session_name: string;
created_at: number;
static createFrom(source: any = {}) {
return new ParsedSession(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
this.session_name = source["session_name"];
this.created_at = source["created_at"];
}
}
}

View File

@@ -2,6 +2,8 @@
// This file is automatically generated. DO NOT EDIT
import {model} from '../models';
export function DeleteParsedSession(arg1:number):Promise<void>;
export function ExportCurrentData(arg1:string):Promise<void>;
export function ExportData(arg1:Array<string>,arg2:string):Promise<void>;
@@ -20,6 +22,10 @@ export function GetLatestParsedDataFromDatabase():Promise<model.ParsedResult>;
export function GetNetworkInterfaces():Promise<Array<model.NetworkInterface>>;
export function GetParsedDataByID(arg1:number):Promise<model.ParsedResult>;
export function GetParsedSessions():Promise<Array<model.ParsedSession>>;
export function ParseData(arg1:Array<string>):Promise<string>;
export function ReadRawJsonFile():Promise<model.ParsedResult>;
@@ -30,6 +36,10 @@ export function SaveParsedDataToDatabase(arg1:string,arg2:string,arg3:string):Pr
export function StartCapture(arg1:string):Promise<void>;
export function StartCaptureWithFilter(arg1:string,arg2:string):Promise<void>;
export function StopAndParseCapture():Promise<model.ParsedResult>;
export function StopCapture():Promise<void>;
export function UpdateParsedSessionName(arg1:number,arg2:string):Promise<void>;

View File

@@ -2,6 +2,10 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function DeleteParsedSession(arg1) {
return window['go']['service']['App']['DeleteParsedSession'](arg1);
}
export function ExportCurrentData(arg1) {
return window['go']['service']['App']['ExportCurrentData'](arg1);
}
@@ -38,6 +42,14 @@ export function GetNetworkInterfaces() {
return window['go']['service']['App']['GetNetworkInterfaces']();
}
export function GetParsedDataByID(arg1) {
return window['go']['service']['App']['GetParsedDataByID'](arg1);
}
export function GetParsedSessions() {
return window['go']['service']['App']['GetParsedSessions']();
}
export function ParseData(arg1) {
return window['go']['service']['App']['ParseData'](arg1);
}
@@ -58,6 +70,10 @@ export function StartCapture(arg1) {
return window['go']['service']['App']['StartCapture'](arg1);
}
export function StartCaptureWithFilter(arg1, arg2) {
return window['go']['service']['App']['StartCaptureWithFilter'](arg1, arg2);
}
export function StopAndParseCapture() {
return window['go']['service']['App']['StopAndParseCapture']();
}
@@ -65,3 +81,7 @@ export function StopAndParseCapture() {
export function StopCapture() {
return window['go']['service']['App']['StopCapture']();
}
export function UpdateParsedSessionName(arg1, arg2) {
return window['go']['service']['App']['UpdateParsedSessionName'](arg1, arg2);
}

View File

@@ -48,6 +48,10 @@ export function EventsOff(eventName, ...additionalEventNames) {
return window.runtime.EventsOff(eventName, ...additionalEventNames);
}
export function EventsOffAll() {
return window.runtime.EventsOffAll();
}
export function EventsOnce(eventName, callback) {
return EventsOnMultiple(eventName, callback, 1);
}