feat(database): add gearTxt field to parsed results and update related functions

This commit is contained in:
kever
2026-02-17 22:42:59 +08:00
parent 5dba3d9930
commit 8c4c4e77d7
13 changed files with 658 additions and 226 deletions

View File

@@ -3,6 +3,7 @@ import {Button, Card, Layout, message, Spin, Table} from 'antd';
import {DownloadOutlined, PlayCircleOutlined, StopOutlined, UploadOutlined} from '@ant-design/icons';
import '../App.css';
import {
ParseAndSaveRawJson,
ReadRawJsonFile,
SaveParsedDataToDatabase,
StartCaptureWithFilter,
@@ -239,40 +240,22 @@ function CapturePage() {
return;
}
if (!json.heroes || !Array.isArray(json.heroes)) {
showMessage('error', '数据格式错误缺少heroes数组');
return;
}
if (!json.items || !Array.isArray(json.items)) {
showMessage('error', '数据格式错误缺少items数组');
return;
}
if (json.heroes.length === 0) {
showMessage('error', '数据格式错误heroes数组不能为空');
return;
}
if (json.items.length === 0) {
showMessage('error', '数据格式错误items数组不能为空');
return;
}
const sessionName = `import_${Date.now()}`;
const equipmentJSON = JSON.stringify(json.items);
const heroesJSON = JSON.stringify(json.heroes);
await SaveParsedDataToDatabase(sessionName, equipmentJSON, heroesJSON);
const gearTxt = typeof text === 'string' ? text : JSON.stringify(json);
const parsed = await ParseAndSaveRawJson(sessionName, gearTxt);
const safeData = {
items: json.items,
heroes: json.heroes
items: parsed?.items || [],
heroes: parsed?.heroes || []
};
setParsedData(safeData);
setUploadedFileName('');
showMessage('success', `导入成功:${json.items.length}件装备,${json.heroes.length}个英雄`);
if (safeData.items.length === 0 || safeData.heroes.length === 0) {
showMessage('warning', `导入完成,但解析为空:${safeData.items.length}件装备,${safeData.heroes.length}个英雄`);
} else {
showMessage('success', `导入成功:${safeData.items.length}件装备,${safeData.heroes.length}个英雄`);
}
} catch (err) {
console.error('文件处理错误:', err);
if (err instanceof SyntaxError) {

View File

@@ -1,25 +1,24 @@
import React, {useEffect, useState} from 'react';
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 React, { useEffect, useState } from 'react';
import { Button, Card, Col, Input, Layout, Modal, Row, Select, Space, Statistic, Table } from 'antd';
import { BarChartOutlined, DatabaseOutlined, DownloadOutlined, ReloadOutlined, SettingOutlined } from '@ant-design/icons';
import * as App from '../../wailsjs/go/service/App';
import {model} from '../../wailsjs/go/models';
import {useMessage} from '../utils/useMessage';
import { model } from '../../wailsjs/go/models';
import { useMessage } from '../utils/useMessage';
const { Content } = Layout;
// 定义Equipment接口
// 定义 Equipment 接口
interface Equipment {
id: string | number;
code: string;
ct: number;
e: number;
g: number;
l: boolean;
mg: number;
op: any[];
p: number;
s: string;
sk: number;
id?: string | number;
code?: string;
set?: string;
level?: number;
enhance?: number;
gear?: string;
main?: { type?: string; value?: number };
substats?: Array<{ type?: string; value?: number }>;
ingameEquippedId?: string;
p?: string | number;
}
const DatabasePage: React.FC = () => {
@@ -29,8 +28,123 @@ const DatabasePage: React.FC = () => {
const [selectedSessionId, setSelectedSessionId] = useState<number | null>(null);
const [renameOpen, setRenameOpen] = useState(false);
const [renameValue, setRenameValue] = useState('');
const [setFilter, setSetFilter] = useState<string | null>(null);
const [gearFilter, setGearFilter] = useState<string | null>(null);
const [mainStatFilter, setMainStatFilter] = useState<string | null>(null);
const { success, error, info } = useMessage();
const SET_LABELS: Record<string, string> = {
AttackSet: '攻击套装',
DefenseSet: '防御套装',
HealthSet: '生命值套装',
SpeedSet: '速度套装',
CriticalSet: '暴击套装',
DestructionSet: '破灭套装',
HitSet: '命中套装',
ResistSet: '抵抗套装',
LifestealSet: '吸血套装',
CounterSet: '反击套装',
ImmunitySet: '免疫套装',
PenetrationSet: '穿透套装',
InjurySet: '伤口套装',
ProtectionSet: '守护套装',
TorrentSet: '激流套装',
ReversalSet: '逆袭套装',
RiposteSet: '回击套装',
WarfareSet: '开战套装',
PursuitSet: '追击套装',
RageSet: '愤怒套装',
RevengeSet: '憎恨套装',
UnitySet: '夹攻套装',
};
const GEAR_LABELS: Record<string, string> = {
Weapon: '武器',
Helmet: '头盔',
Armor: '铠甲',
Necklace: '项链',
Ring: '戒指',
Boots: '鞋子',
};
const STAT_LABELS: Record<string, string> = {
Attack: '攻击力',
Defense: '防御力',
Health: '生命值',
Speed: '速度',
AttackPercent: '攻击力%',
DefensePercent: '防御力%',
HealthPercent: '生命值%',
CriticalHitChancePercent: '暴击率',
CriticalHitDamagePercent: '暴击伤害',
EffectivenessPercent: '效果命中',
EffectResistancePercent: '效果抗性',
};
const PERCENT_STATS = new Set([
'AttackPercent',
'DefensePercent',
'HealthPercent',
'CriticalHitChancePercent',
'CriticalHitDamagePercent',
'EffectivenessPercent',
'EffectResistancePercent',
]);
const formatStat = (stat?: { type?: string; value?: number }) => {
if (!stat || !stat.type) return '-';
const rawLabel = STAT_LABELS[stat.type] || stat.type;
const value = stat.value ?? 0;
const rawValue = Number.isFinite(value) ? value : 0;
const roundedValue = Math.round(rawValue * 10) / 10;
const formattedValue = Number.isInteger(roundedValue) ? roundedValue.toString() : roundedValue.toString();
if (PERCENT_STATS.has(stat.type)) {
const label = rawLabel.endsWith('%') ? rawLabel.slice(0, -1) : rawLabel;
return `${label}${formattedValue}%`;
}
return `${rawLabel} ${formattedValue}`;
};
const formatSubstats = (subs?: Array<{ type?: string; value?: number }>) => {
if (!subs || subs.length === 0) return '-';
return subs.slice(0, 4).map(s => formatStat(s)).join(' / ');
};
const heroNameById = React.useMemo(() => {
const map = new Map<string, string>();
const heroes = latestData?.heroes || [];
heroes.forEach((h: any) => {
const id = h?.id ?? h?.ingameId ?? h?.code;
if (id !== undefined && id !== null) {
const key = String(id);
map.set(key, h?.name || h?.code || key);
}
});
return map;
}, [latestData?.heroes]);
const getEquippedHeroName = (item: Equipment) => {
const key = item.ingameEquippedId ?? item.p;
if (key === undefined || key === null || key === '') return '-';
return heroNameById.get(String(key)) || String(key);
};
const filteredItems = React.useMemo(() => {
const items = latestData?.items || [];
return items.filter((item: Equipment) => {
if (setFilter) {
if ((item.set || '') !== setFilter) return false;
}
if (gearFilter) {
if ((item.gear || '') !== gearFilter) return false;
}
if (mainStatFilter) {
if ((item.main?.type || '') !== mainStatFilter) return false;
}
return true;
});
}, [latestData?.items, setFilter, gearFilter, mainStatFilter]);
const formatSessionLabel = (session: model.ParsedSession) => {
const date = session.created_at ? new Date(session.created_at * 1000) : null;
const timeText = date ? date.toLocaleString() : '';
@@ -88,9 +202,33 @@ const DatabasePage: React.FC = () => {
loadSessions();
};
const exportData = () => {
if (!latestData?.geartxt) {
info('没有数据可导出');
return;
}
try {
const exportContent = latestData.geartxt;
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);
success('数据导出成功');
} catch (err) {
console.error('导出数据出错:', err);
error('数据导出失败');
}
};
const openRename = () => {
if (!selectedSessionId) {
info('请先选择一条解析数据');
info('请先选择一条装备记录');
return;
}
const current = sessions.find(s => s.id === selectedSessionId);
@@ -124,7 +262,7 @@ const DatabasePage: React.FC = () => {
const handleDelete = async () => {
if (!selectedSessionId) {
info('请先选择一条解析数据');
info('请先选择一条装备记录');
return;
}
Modal.confirm({
@@ -147,7 +285,7 @@ const DatabasePage: React.FC = () => {
} finally {
setLoading(false);
}
}
},
});
};
@@ -158,67 +296,46 @@ const DatabasePage: React.FC = () => {
// 装备表格列定义
const equipmentColumns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
render: (id: any) => {
const idStr = String(id || '');
return <span style={{ fontSize: '12px' }}>{idStr.length > 8 ? `${idStr.slice(0, 8)}...` : idStr}</span>;
},
},
{
title: '代码',
dataIndex: 'code',
key: 'code',
title: '套装类型',
dataIndex: 'set',
key: 'set',
width: 120,
render: (_: any, record: Equipment) => SET_LABELS[record.set || ''] || record.set || '-',
},
{
title: '等级',
dataIndex: 'g',
key: 'g',
dataIndex: 'level',
key: 'level',
width: 80,
render: (grade: number) => (
<Tag color={grade >= 5 ? 'red' : grade >= 3 ? 'orange' : 'green'}>
{grade}
</Tag>
),
render: (value: number) => (value ?? '-'),
},
{
title: '经验',
dataIndex: 'e',
key: 'e',
title: '强化等级',
dataIndex: 'enhance',
key: 'enhance',
width: 90,
render: (value: number) => (value === undefined || value === null ? '-' : `+${value}`),
},
{
title: '部位',
dataIndex: 'gear',
key: 'gear',
width: 100,
render: (exp: number) => exp.toLocaleString(),
render: (gear: string) => GEAR_LABELS[gear] || gear || '-',
},
{
title: '力量',
dataIndex: 'p',
key: 'p',
width: 80,
title: '主属性',
dataIndex: 'main',
key: 'main',
width: 180,
render: (_: any, record: Equipment) => formatStat(record.main),
},
{
title: '魔法',
dataIndex: 'mg',
key: 'mg',
width: 80,
},
{
title: '技能',
dataIndex: 'sk',
key: 'sk',
width: 80,
},
{
title: '状态',
dataIndex: 'l',
key: 'l',
width: 80,
render: (locked: boolean) => (
<Tag color={locked ? 'red' : 'green'}>
{locked ? '锁定' : '正常'}
</Tag>
),
title: '副属性',
dataIndex: 'substats',
key: 'substats',
width: 320,
render: (_: any, record: Equipment) => formatSubstats(record.substats),
},
];
@@ -268,6 +385,7 @@ const DatabasePage: React.FC = () => {
>
</Button>
<Button icon={<DownloadOutlined />} onClick={exportData} disabled={!latestData?.geartxt}></Button>
<Select
style={{ minWidth: 320 }}
placeholder="请选择解析数据"
@@ -287,11 +405,57 @@ const DatabasePage: React.FC = () => {
</Card>
{/* 装备表格 */}
<Card title="解析的装备数据">
<Card
title={
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span></span>
<Space>
<span></span>
<Select
style={{ minWidth: 180 }}
placeholder="全部"
value={setFilter ?? undefined}
allowClear
onChange={value => setSetFilter(value ?? null)}
options={Object.keys(SET_LABELS).map(key => ({
value: key,
label: SET_LABELS[key],
}))}
/>
<span></span>
<Select
style={{ minWidth: 140 }}
placeholder="全部"
value={gearFilter ?? undefined}
allowClear
onChange={value => setGearFilter(value ?? null)}
options={Object.keys(GEAR_LABELS).map(key => ({
value: key,
label: GEAR_LABELS[key],
}))}
/>
<span></span>
<Select
style={{ minWidth: 160 }}
placeholder="全部"
value={mainStatFilter ?? undefined}
allowClear
onChange={value => setMainStatFilter(value ?? null)}
options={Object.keys(STAT_LABELS)
.filter(key => !['Attack', 'Defense', 'Health'].includes(key))
.map(key => ({
value: key,
label: STAT_LABELS[key],
}))}
/>
</Space>
</div>
}
>
{latestData?.items && latestData.items.length > 0 ? (
<Table
columns={equipmentColumns}
dataSource={latestData.items}
dataSource={filteredItems}
rowKey="id"
loading={loading}
pagination={{
@@ -331,4 +495,5 @@ const DatabasePage: React.FC = () => {
);
};
export default DatabasePage;
export default DatabasePage;

View File

@@ -321,6 +321,7 @@ export namespace model {
export class ParsedResult {
items: any[];
heroes: any[];
geartxt: string;
static createFrom(source: any = {}) {
return new ParsedResult(source);
@@ -330,6 +331,7 @@ export namespace model {
if ('string' === typeof source) source = JSON.parse(source);
this.items = source["items"];
this.heroes = source["heroes"];
this.geartxt = source["geartxt"];
}
}
export class ParsedSession {

View File

@@ -30,13 +30,15 @@ export function GetParsedSessions():Promise<Array<model.ParsedSession>>;
export function OptimizeBuilds(arg1:model.OptimizeRequest):Promise<model.OptimizeResponse>;
export function ParseAndSaveRawJson(arg1:string,arg2:string):Promise<model.ParsedResult>;
export function ParseData(arg1:Array<string>):Promise<string>;
export function ReadRawJsonFile():Promise<model.ParsedResult>;
export function SaveAppSetting(arg1:string,arg2:string):Promise<void>;
export function SaveParsedDataToDatabase(arg1:string,arg2:string,arg3:string):Promise<void>;
export function SaveParsedDataToDatabase(arg1:string,arg2:string,arg3:string,arg4:string):Promise<void>;
export function StartCapture(arg1:string):Promise<void>;

View File

@@ -58,6 +58,10 @@ export function OptimizeBuilds(arg1) {
return window['go']['service']['App']['OptimizeBuilds'](arg1);
}
export function ParseAndSaveRawJson(arg1, arg2) {
return window['go']['service']['App']['ParseAndSaveRawJson'](arg1, arg2);
}
export function ParseData(arg1) {
return window['go']['service']['App']['ParseData'](arg1);
}
@@ -70,8 +74,8 @@ export function SaveAppSetting(arg1, arg2) {
return window['go']['service']['App']['SaveAppSetting'](arg1, arg2);
}
export function SaveParsedDataToDatabase(arg1, arg2, arg3) {
return window['go']['service']['App']['SaveParsedDataToDatabase'](arg1, arg2, arg3);
export function SaveParsedDataToDatabase(arg1, arg2, arg3, arg4) {
return window['go']['service']['App']['SaveParsedDataToDatabase'](arg1, arg2, arg3, arg4);
}
export function StartCapture(arg1) {