DCFronted/src/views/index/WeaponMatch.vue
2025-06-19 22:10:20 +08:00

2129 lines
67 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container">
<!-- 左侧导航栏 -->
<div class="sidebar">
<h2>功能导航</h2>
<ul>
<li :class="{active: activeSection === 'match'}" @click="activeSection = 'match'">模板匹配</li>
<li :class="{active: activeSection === 'compare'}" @click="activeSection = 'compare'">武器对比</li>
<li :class="{active: activeSection === 'GOBedit'}" @click="activeSection = 'GOBedit'">物体编辑器</li><!--GOBeditgameobject--->
</ul>
</div>
<!-- 主内容区 -->
<div class="main-content">
<!-- 模板匹配部分 -->
<section v-show="activeSection === 'match'" class="match-section">
<h1><i class="icon icon-match"></i> WeaponTemplate 匹配到单位 GameObject</h1>
<div class="upload-area">
<div class="upload-card">
<h3>上传 Weapon.xml</h3>
<input type="file" @change="handleWeaponFile" class="file-upload" accept=".xml" />
</div>
<div class="upload-card">
<h3>上传单位 XML</h3>
<input type="file" @change="handleUnitFile" class="file-upload" accept=".xml" />
</div>
</div>
<div class="result-card" v-if="resultList.length">
<div class="card-header">
<h3>匹配结果</h3>
<button @click="autoSelectMatched" class="action-btn">
<i class="icon icon-auto-select"></i> 自动选择匹配项
</button>
</div>
<div class="table-responsive">
<table class="result-table">
<thead>
<tr>
<th width="50%">GameObject ID</th>
<th width="50%">匹配到的 WeaponTemplate</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in resultList" :key="index">
<td>{{ item.id }}</td>
<td>
<span v-if="item.templates && item.templates.length > 0">
{{ item.templates.join(', ') }}
</span>
<span v-else class="no-match">None</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- 武器对比部分 -->
<section v-show="activeSection === 'compare'" class="compare-section">
<h1><i class="icon icon-compare"></i> 武器对比工具</h1>
<div class="control-panel">
<div class="reload-card">
<h3>重新导入 Weapon.xml</h3>
<input type="file" @change="handleCompareWeaponFile" class="file-upload" accept=".xml" />
</div>
<div class="pagination-card">
<div class="pagination-controls">
<button @click="prevPage" :disabled="currentPage === 1" class="page-btn">
<i class="icon icon-prev"></i> 上一页
</button>
<span class="page-info">第 {{ currentPage }} 页 / 共 {{ totalPages }} 页</span>
<button @click="nextPage" :disabled="currentPage >= totalPages" class="page-btn">
下一页 <i class="icon icon-next"></i>
</button>
</div>
</div>
</div>
<div class="weapon-list-card">
<div class="card-header">
<h3>武器列表</h3>
<div>
<button @click="clearAllSelections" class="action-btn danger">
<i class="icon icon-clear"></i> 清空选择
</button>
</div>
</div>
<div class="table-responsive">
<table class="weapon-table">
<thead>
<tr>
<th width="60px">选择</th>
<th>武器 ID</th>
</tr>
</thead>
<tbody>
<tr v-for="weapon in pagedWeapons" :key="weapon.id">
<td>
<label class="checkbox-container">
<input type="checkbox" :value="weapon.id" v-model="selectedWeaponIds" />
<span class="checkmark"></span>
</label>
</td>
<td>{{ weapon.id }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="compare-tools">
<h3><i class="icon icon-attribute"></i> 属性对比</h3>
<div class="toolbar">
<button @click="toggleTableDirection" class="tool-btn" :class="{active: isReversed}">
<i class="icon" :class="isReversed ? 'icon-restore' : 'icon-reverse'"></i>
{{ isReversed ? '恢复默认视图' : '反转行列' }}
</button>
<button @click="transposeTable" class="tool-btn" :class="{active: isTransposed}">
<i class="icon" :class="isTransposed ? 'icon-restore' : 'icon-transpose'"></i>
{{ isTransposed ? '取消转置' : '转置表格' }}
</button>
<button @click="clearAllSelections" class="tool-btn danger">
<i class="icon icon-clear"></i> 清空对比武器
</button>
</div>
<div class="attribute-selector-panel">
<div class="attribute-controls">
<div class="search-box">
<i class="icon icon-search"></i>
<input
v-model="attributeSearchText"
placeholder="搜索属性..."
@input="filterAttributes"
class="search-input"
/>
<div class="button-group">
<button @click="toggleSelectAll" class="small-btn">
{{ selectAll ? '取消全选' : '全选' }}
</button>
<button @click="clearSelectedAttributes" class="small-btn danger">
清空
</button>
</div>
</div>
<div class="select-container">
<select
multiple
v-model="selectedAttributes"
class="multi-select"
size="10"
>
<option
v-for="attr in filteredAttributes"
:key="attr"
:value="attr"
class="select-option"
>
{{ attr }}
</option>
</select>
<div class="selection-info">
已选择 {{ selectedAttributes.length }} / {{ allAttributes.length }} 个属性
</div>
</div>
</div>
<div class="selected-preview">
<div class="preview-header">
<h4>已选属性预览</h4>
<span class="hint-text">点击属性可移除</span>
</div>
<div class="selected-tags">
<span
v-for="attr in selectedAttributes"
:key="attr"
class="tag"
@click="removeAttribute(attr)"
>
{{ attr }}
<i class="icon icon-close"></i>
</span>
<span v-if="selectedAttributes.length === 0" class="empty-hint">暂无选择</span>
</div>
</div>
</div>
<div class="comparison-result" v-if="selectedWeapons.length && selectedAttributes.length">
<div class="table-responsive">
<table class="comparison-table">
<thead>
<tr>
<th>{{ displayMode === 'normal' ? '属性名' : '武器 ID' }}</th>
<th v-for="weapon in selectedWeapons" :key="weapon.id">
{{ displayMode === 'normal' ? weapon.id : '属性名' }}
</th>
</tr>
</thead>
<tbody>
<template v-if="displayMode === 'normal'">
<tr v-for="attr in selectedAttributes" :key="attr">
<td class="attr-name">{{ attr }}</td>
<td v-for="weapon in selectedWeapons" :key="weapon.id"
:class="getCellClass(getWeaponAttribute(weapon, attr))">
{{ getWeaponAttribute(weapon, attr) || '-' }}
</td>
</tr>
</template>
<template v-else>
<tr v-for="weapon in selectedWeapons" :key="weapon.id">
<td class="attr-name">{{ weapon.id }}</td>
<td v-for="attr in selectedAttributes" :key="attr"
:class="getCellClass(getWeaponAttribute(weapon, attr))">
{{ getWeaponAttribute(weapon, attr) || '-' }}
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<div v-else class="empty-state">
<i class="icon icon-empty"></i>
<p>{{ !selectedWeapons.length ? '请先选择要对比的武器' : '请选择要对比的属性' }}</p>
</div>
</div>
<div class="additional-info">
<div class="info-card">
<h3><i class="icon icon-inheritance"></i> 继承关系</h3>
<div class="table-responsive" v-if="selectedWeapons.length">
<table class="info-table">
<thead>
<tr>
<th width="30%">武器 ID</th>
<th width="70%">继承链</th>
</tr>
</thead>
<tbody>
<tr v-for="weapon in selectedWeapons" :key="weapon.id">
<td>{{ weapon.id }}</td>
<td>{{ getInheritanceChain(weapon).join(' → ') }}</td>
</tr>
</tbody>
</table>
</div>
<div v-else class="empty-state small">
<i class="icon icon-info"></i>
<p>选择武器查看继承关系</p>
</div>
</div>
<div class="info-card">
<h3><i class="icon icon-ranking"></i> 继承排行榜</h3>
<div v-if="inheritanceCount.length" class="ranking-list">
<div v-for="(item, index) in inheritanceCount.slice(0, 5)" :key="index" class="ranking-item">
<span class="rank">{{ index + 1 }}</span>
<span class="weapon-name">{{ item.weapon }}</span>
<span class="count">{{ item.count }} 次</span>
</div>
</div>
<div v-else class="empty-state small">
<i class="icon icon-info"></i>
<p>暂无继承数据</p>
</div>
</div>
</div>
</section>
<!-- 物体编辑器部分 -->
<section v-show="activeSection === 'GOBedit'" class="GOBeditor-section">
<div class="file-input-container">
<input type="file" @change="handleUnitFile" accept=".xml" class="file-input" title="加载单位.xml">
<input type="file" @change="handleWeaponFile" accept=".xml" class="file-input" title="加载Weapon.xml">
<button v-if="unitDoc" @click="exportUnitXml" class="export-btn">导出XML</button>
</div>
<div v-if="unitDoc" class="unit-editor">
<div class="unit-list">
<div v-for="gameObject in unitDoc.querySelectorAll('GameObject')" :key="gameObject.getAttribute('id')"
class="unit-item" @click="selectGameObject(gameObject)"
:class="{ 'selected': selectedGameObject === gameObject }">
{{ gameObject.getAttribute('id') || '未命名单位' }}
</div>
</div>
<div v-if="selectedGameObject" class="unit-details">
<h3>单位详细信息</h3>
<!--ID面板-->
<div class="detail-row">
<span class="detail-label">ID:</span>
<input class="detail-value edit-input" v-model="editableFields.id" @change="updateGameObject">
</div>
<!--CSF面板-->
<div class="detail-group">
<div class="group-header" @click="toggleGroup('csf')">
<span>CSF相关描述</span>
<span class="toggle-icon">{{ isGroupOpen('csf') ? '' : '+' }}</span>
</div>
<div v-if="isGroupOpen('csf')" class="group-content">
<div class="detail-row">
<span class="detail-label">DisplayName:</span>
<input class="detail-value edit-input" v-model="editableFields.displayName" @change="updateDisplayName">
</div>
<div class="detail-row">
<span class="detail-label">TypeDescription:</span>
<input class="detail-value edit-input" v-model="editableFields.typeDescription" @change="updateGameObject">
</div>
<div class="detail-row">
<span class="detail-label">Description:</span>
<input class="detail-value edit-input" v-model="editableFields.description" @change="updateGameObject">
</div>
</div>
</div>
<!--建造时间面板-->
<div class="detail-row">
<span class="detail-label">建造时间:</span>
<input class="detail-value edit-input" v-model="editableFields.buildTime" @change="updateGameObject" type="number">
<span class="unit-suffix">秒</span>
</div>
<!--科技需求面板-->
<div class="detail-group">
<div class="group-header" @click="toggleGroup('tech')">
<span>科技需求</span>
<span class="toggle-icon">{{ isGroupOpen('tech') ? '' : '+' }}</span>
</div>
<div v-if="isGroupOpen('tech')" class="group-content">
<div class="detail-row">
<span class="detail-label">NeededUpgrade:</span>
<input class="detail-value edit-input" v-model="editableFields.neededUpgrade" @change="updateNeededUpgrade">
</div>
<div class="detail-row">
<span class="detail-label">建造数量上限:</span>
<input class="detail-value edit-input" v-model="editableFields.maxSimultaneous" @change="updateGameObject" type="number">
</div>
</div>
</div>
<!--造价和生命值面板-->
<div class="detail-group">
<div class="group-header" @click="toggleGroup('costHealth')">
<span>造价与生命值</span>
<span class="toggle-icon">{{ isGroupOpen('costHealth') ? '' : '+' }}</span>
</div>
<div v-if="isGroupOpen('costHealth')" class="group-content">
<div class="detail-row">
<span class="detail-label">造价:</span>
<input class="detail-value edit-input" v-model="editableFields.buildCost" @change="updateBuildCost" type="number">
</div>
<div class="detail-row">
<span class="detail-label">生命值:</span>
<input class="detail-value edit-input" v-model="editableFields.maxHealth" @change="updateMaxHealth" type="number">
</div>
</div>
</div>
<!--DPS面板-->
<!--射程面板-->
<div class="detail-group">
<div class="group-header" @click="toggleGroup('range')">
<span>武器射程</span>
<span class="toggle-icon">{{ isGroupOpen('range') ? '' : '+' }}</span>
</div>
<div v-if="isGroupOpen('range')" class="group-content">
<div v-if="weaponData.length === 0" class="detail-row">
<span class="detail-label">无武器数据</span>
</div>
<template v-else>
<div class="detail-row" v-for="(weapon, index) in weaponData" :key="index">
<span class="detail-label">武器 {{ index + 1 }} ({{ weapon.id }}):</span>
<div class="weapon-stats">
<!-- 清雾范围未添加 -->
<div>攻击范围: {{ weapon.attackRange || 0 }}</div>
<div>最小射程: {{ weapon.minAttackRange || 0 }}</div>
<div>伤害值: {{ weapon.Damage || 0 }}</div>
<div>伤害范围: {{ weapon.Radius || 0 }}</div>
</div>
</div>
</template>
</div>
</div>
<!--移速面板-->
<!--技能面板-->
<!--碾压等级面板-->
</div>
</div>
</section>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
//编辑器
const weaponXmlData = ref(null)
const weaponData = ref([])
const selectedGameObject = ref(null)
const openGroups = ref(['csf', 'tech', 'costHealth'])
const editableFields = ref({
id: '',
displayName: '',
typeDescription: '',
description: '',
buildTime: '',
neededUpgrade: '',
maxSimultaneous: '',
buildCost: '',
maxHealth: ''
})
function selectGameObject(gameObject) {
selectedGameObject.value = gameObject
updateEditableFields()
updateWeaponData()
}
function updateEditableFields() {
if (!selectedGameObject.value) return
editableFields.value = {
id: selectedGameObject.value.getAttribute('id') || '',
displayName: extractDisplayName(selectedGameObject.value) || '',
typeDescription: selectedGameObject.value.getAttribute('TypeDescription') || '',
description: selectedGameObject.value.getAttribute('Description') || '',
buildTime: selectedGameObject.value.getAttribute('BuildTime') || '',
neededUpgrade: getNeededUpgrade(selectedGameObject.value) || '',
maxSimultaneous: selectedGameObject.value.getAttribute('MaxSimultaneousOfType') || '',
buildCost: getBuildCost(selectedGameObject.value) || '',
maxHealth: getMaxHealth(selectedGameObject.value) || ''
}
}
function updateGameObject() {
if (!selectedGameObject.value) return
// 更新基本属性
selectedGameObject.value.setAttribute('id', editableFields.value.id)
selectedGameObject.value.setAttribute('TypeDescription', editableFields.value.typeDescription)
selectedGameObject.value.setAttribute('Description', editableFields.value.description)
selectedGameObject.value.setAttribute('BuildTime', editableFields.value.buildTime)
selectedGameObject.value.setAttribute('MaxSimultaneousOfType', editableFields.value.maxSimultaneous)
}
function updateDisplayName() {
if (!selectedGameObject.value) return
let displayNameNode = selectedGameObject.value.querySelector('DisplayName')
if (!displayNameNode) {
displayNameNode = unitDoc.value.createElement('DisplayName')
selectedGameObject.value.appendChild(displayNameNode)
}
displayNameNode.textContent = editableFields.value.displayName
}
function updateNeededUpgrade() {
if (!selectedGameObject.value) return
let gameDependency = selectedGameObject.value.querySelector('GameDependency')
if (!gameDependency) {
gameDependency = unitDoc.value.createElement('GameDependency')
selectedGameObject.value.appendChild(gameDependency)
}
gameDependency.setAttribute('NeededUpgrade', editableFields.value.neededUpgrade)
}
function updateBuildCost() {
if (!selectedGameObject.value) return
let resourceInfo = selectedGameObject.value.querySelector('ObjectResourceInfo')
if (!resourceInfo) {
resourceInfo = unitDoc.value.createElement('ObjectResourceInfo')
selectedGameObject.value.appendChild(resourceInfo)
}
let buildCost = resourceInfo.querySelector('BuildCost')
if (!buildCost) {
buildCost = unitDoc.value.createElement('BuildCost')
resourceInfo.appendChild(buildCost)
}
buildCost.setAttribute('Amount', editableFields.value.buildCost)
}
function updateMaxHealth() {
if (!selectedGameObject.value) return
let body = selectedGameObject.value.querySelector('Body')
if (!body) {
body = unitDoc.value.createElement('Body')
selectedGameObject.value.appendChild(body)
}
let activeBody = body.querySelector('ActiveBody')
if (!activeBody) {
activeBody = unitDoc.value.createElement('ActiveBody')
body.appendChild(activeBody)
}
activeBody.setAttribute('MaxHealth', editableFields.value.maxHealth)
}
//导出
function exportUnitXml() {
if (!unitDoc.value || !selectedGameObject.value) return;
const serializer = new XMLSerializer();
const unitId = selectedGameObject.value.getAttribute('id') || 'unit';
// 1. 导出主单位文件
const unitXmlString = serializer.serializeToString(unitDoc.value);
downloadFile(`${unitId}.xml`, unitXmlString);
// 2. 创建符合规范的config.xml
const configXml = document.implementation.createDocument(
"uri:ea.com:eala:asset",
"AssetDeclaration"
);
// 添加XML声明
const xmlDeclaration = '<?xml version="1.0" encoding="utf-8" ?>';
// 添加Tags和Includes
const tags = configXml.createElement("Tags");
configXml.documentElement.appendChild(tags);
const includes = configXml.createElement("Includes");
const include = configXml.createElement("Include");
include.setAttribute("type", "all");
include.setAttribute("source", "DATA:GlobalData/GlobalDefines.xml");
includes.appendChild(include);
configXml.documentElement.appendChild(includes);
// 添加Defines
const defines = configXml.createElement("Defines");
const define1 = configXml.createElement("Define");
define1.setAttribute("name", "FACTION_WEAPON_SECONDARY_DAMAGE_AMOUNT");
define1.setAttribute("value", "-500.0");
defines.appendChild(define1);
const define2 = configXml.createElement("Define");
define2.setAttribute("name", "EMPERORS_RESOLVE_AFFECTS");
define2.setAttribute("value", "ALLIES NEUTRALS ENEMIES");
defines.appendChild(define2);
configXml.documentElement.appendChild(defines);
// 添加匹配的WeaponTemplate
if (weaponData.value.length > 0 && weaponXmlData.value) {
weaponData.value.forEach(weapon => {
const weaponTemplate = weaponXmlData.value.querySelector(`WeaponTemplate[id="${weapon.id}"]`);
if (weaponTemplate) {
// 创建新的WeaponTemplate元素
const newWeaponTemplate = configXml.createElement("WeaponTemplate");
// 复制所有属性
Array.from(weaponTemplate.attributes).forEach(attr => {
newWeaponTemplate.setAttribute(attr.name, attr.value);
});
// 复制所有子节点
Array.from(weaponTemplate.children).forEach(child => {
const newChild = configXml.createElement(child.tagName);
// 复制子节点属性
Array.from(child.attributes).forEach(attr => {
newChild.setAttribute(attr.name, attr.value);
});
// 如果有文本内容也复制
if (child.textContent.trim()) {
newChild.textContent = child.textContent;
}
// 处理Nuggets的子节点
if (child.tagName === 'Nuggets') {
Array.from(child.children).forEach(nugget => {
const newNugget = configXml.createElement(nugget.tagName);
Array.from(nugget.attributes).forEach(attr => {
newNugget.setAttribute(attr.name, attr.value);
});
newChild.appendChild(newNugget);
});
}
newWeaponTemplate.appendChild(newChild);
});
configXml.documentElement.appendChild(newWeaponTemplate);
}
});
}
// 序列化config.xml
let configXmlString = serializer.serializeToString(configXml);
// 添加XML声明和格式化
configXmlString = xmlDeclaration + '\n' + formatXml(configXmlString);
// 下载config.xml
setTimeout(() => {
downloadFile('config.xml', configXmlString);
}, 500);
}
// XML格式化函数
function formatXml(xml) {
const PADDING = ' ';
const reg = /(>)(<)(\/*)/g;
let formatted = '';
let pad = 0;
xml = xml.replace(reg, '$1\r\n$2$3');
xml.split('\r\n').forEach(node => {
let indent = 0;
if (node.match(/.+<\/\w[^>]*>$/)) {
indent = 0;
} else if (node.match(/^<\/\w/)) {
if (pad !== 0) pad -= 1;
} else if (node.match(/^<\w[^>]*[^\/]>.*$/)) {
indent = 1;
} else {
indent = 0;
}
formatted += PADDING.repeat(pad) + node + '\r\n';
pad += indent;
});
return formatted;
}
// 文件下载函数
function downloadFile(filename, content) {
const blob = new Blob([content], { type: 'text/xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
//导出结束
function toggleGroup(groupName) {
if (openGroups.value.includes(groupName)) {
openGroups.value = openGroups.value.filter(g => g !== groupName)
} else {
openGroups.value.push(groupName)
}
}
function isGroupOpen(groupName) {
return openGroups.value.includes(groupName)
}
function extractDisplayName(gameObject) {
const displayNameNode = gameObject.querySelector('DisplayName');
return displayNameNode ? displayNameNode.textContent.trim() : null;
}
function getNeededUpgrade(gameObject) {
const gameDependency = gameObject.querySelector('GameDependency')
return gameDependency ? gameDependency.getAttribute('NeededUpgrade') : null
}
function getBuildCost(gameObject) {
const resourceInfo = gameObject.querySelector('ObjectResourceInfo');
if (!resourceInfo) return null;
const buildCost = resourceInfo.querySelector('BuildCost');
if (!buildCost) return null;
return buildCost.getAttribute('Amount');
}
function getMaxHealth(gameObject) {
const body = gameObject.querySelector('Body');
if (!body) return null;
const activeBody = body.querySelector('ActiveBody');
if (!activeBody) return null;
return activeBody.getAttribute('MaxHealth');
}
//武器比对
// 数据状态
const activeSection = ref('match')
const weaponXml = ref('')
const unitDoc = ref(null)
const resultList = ref([])
const weaponTemplateIds = ref(new Set())
const allWeapons = ref([])
const selectedWeaponIds = ref([])
const currentPage = ref(1)
const pageSize = ref(20)
const selectedAttributes = ref([])
const isReversed = ref(false)
const isTransposed = ref(false)
const attributeSearchText = ref('')
const selectAll = ref(false)
const filteredAttributes = ref([])
// 计算属性
const allAttributes = computed(() => {
const attrs = new Set();
// 属性名翻译映射表
//标了问号的就是我不确定是啥的
const attributeTranslations = {
// ================= 武器模板核心属性 =================
'id': '武器ID',
'inheritFrom': '继承自',
'AttackRange': '射程',
'MinimumAttackRange': '最小射程',
'RangeBonusMinHeight': '射程加成最小高度',// 高度相关射程加成,没出现过
'RangeBonus': '射程加成值',// 高度相关射程加成,没出现过
'RangeBonusPerFoot': '每英尺射程加成',// 高度相关射程加成,没出现过
'RequestAssistRange': '请求援助范围',// 意义不明且没出现过
'AcceptableAimDelta': '允许瞄准偏差角',
'AimDirection': '基准瞄准方向', // 意义不明
'ScatterRadius': '散射半径',
'ScatterLength': '散射长度',
'ScatterIndependently': '多抛射物独立散射', //只在V4散弹那里出现一次
'WeaponSpeed': '武器/抛射物速度',
'MinWeaponSpeed': '最小抛射物速度',
'MaxWeaponSpeed': '最大抛射物速度',
'ScaleWeaponSpeed': '缩放抛射物速度',// 意义不明且没出现过
'IgnoresContactPoints': '忽略碰撞体积(将武器瞄准目标几何中心)',
'ScaleAttackRangeByAmmoRemaining': '射程随弹药剩余量缩放',//意义姑且不明,只在世纪轰炸机那里出现一次
'CanBeDodged': '可被躲避',// 意义不明且没出现过
'IdleAfterFiringDelaySeconds': '开火后待机延迟',//似乎是让自己在攻击后会瘫痪一段时间?
'HoldAfterFiringDelaySeconds': '开火后保持延迟',// 意义不明且没出现过
'HoldDuringReload': '保持先前姿态时即可装填',
'CanFireWhileMoving': '移动射击能力',
'WeaponRecoil': '启用后坐力',
'MinTargetPitch': '最小俯仰角',
'MaxTargetPitch': '最大俯仰角',
'PreferredTargetBone': '首选目标骨骼',
'FireSound': '开火音效',
'FireSoundPerClip': '整弹匣开火音效',
'FiringLoopSound': '持续开火音效',
'FiringLoopSoundContinuesDuringReload': '装填时保持开火音效',
'FireFX': '开火特效',
'FireVeteranFX': '升级后开火特效', // 升级后的单位不一定是三星单位
'FireFlankFX': '侧翼开火特效',
'PreAttackFX': '预攻击特效',
'ClipSize': '弹匣容量',
'ContinuousFireOne': '第一阶段连续射击次数',
'ContinuousFireTwo': '第二阶段连续射击次数',
'ContinuousFireCoastSeconds': '连续射击间隔', // 意义不明且没出现过
'AutoReloadWhenIdleSeconds': '空闲时自动装弹时间',// 意义不明且没出现过
'ShotsPerBarrel': '每炮管射弹数',
'DamageDealtAtSelfPosition': '伤害作用于自身位置', //在鬼王和大和的撞击武器那里出现,似乎是用来让武器在路径全程都造成效果的?
'RequiredFiringObjectStatus': '开火所需状态标志',
'ForbiddenFiringObjectStatus': '禁止开火的状态标志',
'CheckStatusFlagsInRangeChecks': '射程检查时包含状态标志',
'ProjectileSelf': '发射自身作为抛射物', // 用于跳跃类单位和神风特攻队
'MeleeWeapon': '近战武器',
'ChaseWeapon': '追击武器', // 意义不明
'LeechRangeWeapon': '吸血射程武器',
'HitStoredTarget': '会攻击预设目标',
'CapableOfFollowingWaypoints': '能否跟随路径点',
'ShowsAmmoPips': '是否显示弹药指示器',
'AllowAttackGarrisonedBldgs': '能否攻击驻军建筑',
'PlayFXWhenStealthed': '隐身时是否播放特效',// 没出现过
'ContinueAttackRange': '持续攻击范围', //意义姑且不明,只在世纪轰炸机那里出现一次
'SuspendFXDelaySeconds': '特效暂停延迟',// 意义不明且没出现过
'IsAimingWeapon': '是否为瞄准武器',
'NoVictimNeeded': '是否需要攻击目标',
'HitPercentage': '命中率', //用处可以很大,但原作中只是用来控制跳跃类单位让它不会把自己一头创死的
'HitPassengerPercentage': '乘客命中概率',
'PassengerProportionalAttack': '是否按乘客比例攻击',
'HealthProportionalResolution': '生命值比例分辨率', // 在鬼王和大和的撞击武器那里出现,似乎是用来让武器在路径全程都造成效果的?
'MaxAttackPassengers': '最大攻击乘客数',
'FinishAttackOnceStarted': '强制完成已开始攻击',// 大部分近战武器都启用了这个,所以会出现二☆人☆幸☆终的情况
'RestrictedHeightRange': '高度限制范围',
'CannotTargetCastleVictims': '能否攻击城堡保护目标',
'RequireFollowThru': '攻击动作必须完整执行', // 攻击动作必须完整执行
'ShareTimers': '共享计时器',// 意义不明且没出现过
'ShouldPlayUnderAttackEvaEvent': '是否播放受袭语音',
'InstantLoadClipOnActivate': '激活武器时才开始装填(无法立刻发射)',
'Flags': '行为控制标志集',
'LockWhenUsing': '使用时锁定',// 意义不明且没出现过
'BombardType': '轰炸类型武器',// 意义不明且没出现过
'UseInnateAttributes': '使用先天属性',// 意义不明且没出现过
'PreAttackType': '攻击前准备类型',
'ReAcquireDetailType': '目标重锁定模式',
'AutoReloadsClip': '自动装填机制 (AUTO-自动再装填/NONE-无法再装填/RETURN_TO_BASE-只能在基地再装填)',
'SingleAmmoReloadedNotFullSound': '单发装填音效', // 在注释里出现过CC3遗留代码
'ClipReloadedSound': '弹匣重装音效', // 在注释里出现过CC3遗留代码
'RadiusDamageAffects': '范围伤害影响对象',
'FXTrigger': '特效触发类型',
'ProjectileCollidesWith': '抛射物碰撞对象类型',
'RequiredAntiMask': '可攻击的目标类型',
'ForbiddenAntiMask': '禁止攻击的目标类型',
'StopFiringOnCanBeInvisible': '隐身时停止开火',// 没出现过
'ProjectileStreamName': '投射物流名称',
'ContactWeapon': '接触式武器',
'UseCenterForRangeCheck': '使用几何中心点计算射程',
'VirtualDamage': '自动分弹模式/虚拟伤害类型(NONE-不进行自动分弹/SOLO-自动分弹时只计算自己/SHARED-自动分弹时计算所有单位的总火力)',
'PreAttackWeapon': '预攻击武器',
'RevealShroudOnFire': '开火时揭露战争迷雾',
'ShouldPlayTargetDeadEvaEvent': '目标死亡时播放语音',
// ================= 时间控制元素属性 =================
'MinSeconds': '最小持续时间(秒)',
'MaxSeconds': '最大持续时间(秒)',
// ================= AI智能提示属性 =================
'IsAntiGarrisonWeapon': '反驻军武器',
'MaxSpeedOfTarget': '可命中的目标最大速度',
'UseLongLockOnTimeCode': '使用长锁定时间逻辑', // 如标枪兵锁定模式
'UseAsWarheadForDamageCalculations': 'AI伤害计算弹头',
// ================= 武器效果组件通用属性 =================
'Radius': '作用半径',
'PartitionFilterTestType': '属性过滤器作用区域形状',
'ForbiddenTargetObjectStatus': '瘫痪/冲击波失效的目标对象状态',
'ForbiddenTargetModelCondition': '瘫痪/冲击波失效的目标模型状态',
'RequiredUpgrade': '所需升级',
'ForbiddenUpgrade': '禁止升级',
// ================= 伤害弹头属性 =================
'Damage': '基础伤害值',
'DamageTaperOff': '伤害衰减系数', // 随距离伤害递减
'MinRadius': '最小作用半径', // 伤害生效最小距离,没出现过
'DamageArc': '扇形伤害范围角度', // 扇形伤害区域
'DamageArcInverted': '反转伤害扇形', // 反转扇形区域,没出现过
'DamageMaxHeight': '伤害最大高度', // 不确定仅电船的F使用这个参数
'DamageMaxHeightAboveTerrain': '伤害最大离地高度',// 不确定,没出现过
'FlankingBonus': '侧翼攻击加成', // 从侧面攻击伤害加成,没出现过
'FlankedScalar': '被侧翼攻击倍率', // 被侧击时的伤害倍率,没出现过
'DelayTimeSeconds': '伤害延迟时间(秒)', // 伤害生效延迟
'DamageType': '伤害类型', // 穿甲/高爆等
'DeathType': '死亡效果类型', // 爆炸/融化等
'DamageFXType': '伤害特效类型',
'DamageSubType': '伤害子类型', // 进一步细分伤害类型,没出现过
'OnlyKillOwnerWhenTriggered': '仅在被触发时杀死拥有者',
'DrainLifeMultiplier': '生命吸取倍率', // 吸血比例没出现过在RAAA中只出现了Leech的吸血弹头
'DrainLife': '生命吸取', // 吸血效果开关没出现过在RAAA中只出现了Leech的吸血弹头
'DamageSpeed': '伤害传播速度', // 只在将军刽子手的F弹头中出现过可能是伤害蔓延速度
'UnderAttackOverrideEvaEvent': '覆盖受袭语音事件',
'VictimShroudRevealer': '目标战争迷雾揭示者', // 意义不明且没出现过
'NotifyOwnerOnVictimDeath': '目标死亡通知拥有者', //Leech类属性——吸到武器了。投稿者变态BUG锤
'NotifyObserversOnPreDamageEffectPosition': '伤害位置预报', // 超武特有属性
'ForceFXPositionToVictim': '特效绑定目标位置', // 意义不明,只在真空内爆弹和城管的弹头里出现过
'RadiusAffectsBridges': '影响桥梁',
'InvalidTargetStatus': '无效目标状态标志',
'DamageScalarDetails': '伤害比例详情',
'Scalar': '伤害比例',
'Filter': '对象过滤器',
// ================= 持续伤害弹头属性 =================
'DamageInterval': '伤害间隔(秒)', // 仅空投有毒罐头使用
'DamageDuration': '伤害总时长(秒)', // 仅空投有毒罐头使用
'RemoveIfHealed': '治疗时移除效果', // 仅空投有毒罐头使用
// ================= 生命吸取弹头属性 =================
'PercentDamageToHeal': '伤害转化为治疗百分比',// 没出现过
'PercentMaxHealthToTake': '最大生命值吸取比例', // 在苏联碾压回血技能处出现,按目标最大生命比例吸血
// ================= 投射物弹头属性 =================
'ProjectileTemplate': '抛射物模板',
'WarheadTemplate': '弹头模板',
'WeaponLaunchBoneSlotOverride': '武器发射骨骼槽', // 指定发射骨骼,没出现过
'AttackOffset': '攻击位置偏移',
'VeterancyLevel': '老兵等级',
'SpecificBarrelOverride': '特定炮管覆盖',
'x': 'X轴偏移',
'y': 'Y轴偏移',
'z': 'Z轴偏移',
// ================= 压制弹头属性 =================
'Suppression': '压制强度', // 士气打击值
'DurationSeconds': '效果持续时间(秒)',
'SuppressionTaperOff': '压制衰减系数', // 随距离压制效果递减
'SuppressionArc': '压制扇形角度', // 扇形压制区域,没出现过
'SuppressionArcInverted': '反转压制扇形', // 反转扇形区域,没出现过
// ================= 激光弹头属性 =================
'Lifetime': '激光持续时间',
'LaserId': '激光ID', //谁能告诉我那一大串数字是啥?
'HitGroundFX': '命中地面特效',
'OverShootDistance': '激光延长线距离',
// ================= 对象创建弹头属性 =================
'WeaponOCL': '武器对象创建列表', // 武器会从OCL列表创建对象
'TargetAsPrimaryObject': '设目标为主要对象', //意义姑且不明,只在天皇之怒和点防御无人机上出现
// ================= 麻痹弹头属性 =================
'EffectArc': '麻痹效果扇形角度',// 扇形麻痹区域,没出现过
'DurationSeconds': '麻痹持续时间(秒)',
'ParalyzeType': '麻痹类型', // EMP/瘫痪等
'RemoveParalyzeType': '解除麻痹类型', // 狗叫和镰刀武器使用
'ParalyzeFX': '麻痹特效',
// ================= 信息战弹头属性 =================
'InfoWarType': '信息战类型', // 没出现过
'RadarJamRadius': '雷达干扰半径',// 没出现过
'RadarJamDuration': '雷达干扰持续时间',// 没出现过
// ================= 泰矿消耗弹头属性 =================
'AmountToSpend': '消耗泰矿数量',
// ================= 冲击波弹头属性 =================
'ShockWaveAmount': '冲击波强度',
'ShockWaveRadius': '冲击波半径',
'ShockWaveArc': '扇形冲击波范围角度',
'ShockWaveTaperOff': '冲击波衰减',
'ShockWaveSpeed': '冲击波速度',// 没出现过
'ShockWaveZMult': '垂直方向冲击波系数', // 垂直方向效果强度
'CyclonicFactor': '气旋因子', // 意义不明且没出现过
'ShockwaveDelaySeconds': '冲击波延迟时间(秒)',
'InvertShockWave': '反转冲击波方向', //不确定,在且只在电磁奇点上出现
'FlipDirection': '翻转方向',
'OnlyWhenJustDied': '仅在目标刚死亡时触发',
'ShockWaveClearRadius': '冲击波清除半径', //只在百合子那里出现一次,而且和默认值是一样的
'ShockWaveClearWaveMult': '清除冲击波倍数',
'ShockWaveClearFlingHeight': '清除抛射高度',
'KillObjectFilter': '击杀对象过滤器',
// ================= 特殊能力弹头属性 =================
'SpecialPowerTemplate': '特殊能力模板',
// ================= 属性修改弹头属性 =================
'AttributeModifierName': '属性修改器名称',
'AttributeModifierOwnerName': '属性修改器所有者', //不确定
'DamageFXType': '伤害特效类型',
'DamageArc': '伤害作用弧度',
'AntiCategories': '反制类别', // 意义不明且没出现过
'AntiFX': '反制特效', // 意义不明且没出现过
// ================= 线性伤害弹头属性 =================
//线性伤害指的是莎莎和波能坦克那种伤害类型
'OffsetAngle': '线性伤害偏移角度', // 不确定且没出现过
'LineWidth': '线性伤害宽度', // 波能坦克等
'LineLengthLeadIn': '起始线长',// 没出现过
'LineLengthLeadOut': '结束线长',// 没出现过
'UseDynamicLineLength': '线性伤害使用动态线长',
'OverShootDistance': '线性伤害延长距离',
// ================= 着色弹头属性 =================
//Tint类属性会给整个建筑/单位上色
'PreColorTime': '着色淡入时间',
'SustainedColorTime': '持续着色时间',
'PostColorTime': '着色淡出时间',
'Frequency': '脉冲频率', // 意义不明
'Amplitude': '脉冲幅度', // 意义不明
'Color': '着色颜色',
'r': '红色分量',
'g': '绿色分量',
'b': '蓝色分量',
// ================= 地形弹坑弹头属性 =================
'Depth': '弹坑深度',// 没出现过
'Lift': '地形隆起比例', // 弹坑边缘隆起程度,没出现过
// ================= 泰伯利亚矿弹头属性 =================
'FieldAmount': '矿场数量',// 没出现过
'SpawnedInFieldBonus': '矿场内生成加成',// 没出现过
// ================= 散射弹头属性 =================
'ScatterMin': '最小散射角度',// 没出现过
'ScatterMax': '最大散射角度',// 没出现过
// ================= 附加弹头属性 =================
'AttachModuleId': 'ATTR模块ID',
// ================= 生命值剥离弹头属性 =================
'AmountToStrip': '生命值剥离百分比', //只在百合子那里出现一次,因为她是按生命值上限百分比扣血的
// ================= 对象攻击弹头属性 =================
'Weapon': '使用的武器',
'FireOnVictimObject': '对目标使用武器', // 天皇之怒和点防御无人机使用
'VictimMustBeAlive': '目标必须存活',// 没出现过
'Filter': '对象过滤器',
// ================= 通知目标弹头属性 =================
'MinTimeToImpactFudgeFactor': '最小命中时间容差', //不确定,在且只在海翼和盟军的小火箭上出现
'MaxTimeToImpactFudgeFactor': '最大命中时间容差', //不确定,在且只在海翼和盟军的小火箭上出现
// ================= 规则与过滤属性 =================
'Rule': '生效规则(ALL/ANY/NONE)',
'Include': '包含对象类型',
'Exclude': '排除对象类型',
'Relationship': '阵营关系(敌/友/中等)',
'StatusBitFlags': '目标需要的状态标志', // 意义不明
'StatusBitFlagsExclude': '排除目标的状态标志',
'RequiredModelConditions': '所需模型状态',
// ================= 武器模板子元素属性 =================
'SpecialObjectFilter': '特殊对象过滤器',
'OverrideVoiceAttackSound': '覆盖攻击语音',// 意义不明且没出现过
'OverrideVoiceEnterStateAttackSound': '覆盖状态切换攻击语音',// 意义不明且没出现过
'SurpriseAttackObjectFilter': '突袭攻击对象过滤器',
'CombinedAttackObjectFilter': '联合攻击对象过滤器',
'HitStoredObjectFilter': '命中存储对象过滤器',
'ScatterRadiusVsType': '散射类型设置',
'IncompatibleAttributeModifier': '不兼容属性修改器',
// ================= 其他弹头属性 =================
'SpawnTemplate': '生成模板',
'SpawnProbability': '生成概率',
'SpawnedModelConditionFlags': '生成模型状态标志',
'Amount': '伤害量',
'PercentDamageToContained': '对包含单位的伤害比例',
'DamageObjectFilter': '伤害对象过滤器',
'MaxUnitsToDamage': '最大伤害单位数',
'WindowBlastFX': '窗口爆炸特效',
'EventName': '事件名称',
'SendToEnemies': '发送给敌人',
'SendToAllies': '发送给盟友',
'SendToNeutral': '发送给中立单位',
// ================= 散射类型子属性 =================
'ScatterRadiusVsType.Radius': '散射半径',
'ScatterRadiusVsType.RequiredModelConditions': '散射所需模型状态'
// 可以根据需要添加更多翻译
};
// 递归提取节点及其子节点的所有属性
const extractAttributes = (node, parentName = '') => {
// 添加当前节点的属性
Array.from(node.attributes || []).forEach(attr => {
const translation = attributeTranslations[attr.name];
// 格式化为"原属性名(翻译)"的形式,如果没有翻译则只显示原属性名
const displayName = translation ? `${attr.name}(${translation})` : attr.name;
// 如果有父节点名称,则添加前缀
const fullName = parentName ? `${parentName}下的${displayName}` : displayName;
if (!parentName) {
// 顶层属性加粗
attrs.add(`**${fullName}**`);
} else {
attrs.add(fullName);
}
});
// 递归处理子节点
Array.from(node.children || []).forEach(child => {
// 使用当前节点的名称作为子节点的父节点名称
const nodeName = node.attributes?.name?.value || node.nodeName;
extractAttributes(child, nodeName);
});
};
// 遍历所有武器,提取属性和子节点属性
allWeapons.value.forEach(w => {
extractAttributes(w.node);
});
return Array.from(attrs);
});
const pagedWeapons = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
return allWeapons.value.slice(start, start + pageSize.value)
})
const totalPages = computed(() => {
return Math.ceil(allWeapons.value.length / pageSize.value)
})
const selectedWeapons = computed(() => {
return allWeapons.value.filter(w => selectedWeaponIds.value.includes(w.id))
})
const inheritanceCount = computed(() => {
const inheritanceMap = new Map()
allWeapons.value.forEach(weapon => {
const parent = weapon.node.getAttribute('inheritFrom')
if (parent) {
inheritanceMap.set(parent, (inheritanceMap.get(parent) || 0) + 1)
}
})
return Array.from(inheritanceMap.entries())
.map(([weapon, count]) => ({ weapon, count }))
.sort((a, b) => b.count - a.count)
})
const displayMode = computed(() => {
if (isReversed.value && isTransposed.value) return 'normal'
if (isReversed.value) return 'reversed'
if (isTransposed.value) return 'transposed'
return 'normal'
})
// 方法
function getWeaponAttribute(weapon, attr, searchChildren = true) {
// 处理所有可能的属性显示格式:
// 1. 加粗格式:**WeaponSpeed**
// 2. 带父节点前缀Weapon下的Speed
// 3. 带翻译格式Speed(速度)
// 4. 组合格式Weapon下的Speed(速度)
let rawAttr = attr;
// 1. 去除Markdown加粗标记
rawAttr = rawAttr.replace(/\*\*/g, '');
// 2. 去除"下的"前缀部分
if (rawAttr.includes('下的')) {
rawAttr = rawAttr.split('下的')[1];
}
// 3. 去除括号翻译部分
if (rawAttr.includes('(')) {
rawAttr = rawAttr.split('(')[0];
}
// 4. 去除可能存在的首尾空格
rawAttr = rawAttr.trim();
// 5. 优先检查当前节点
const value = weapon.node.getAttribute(rawAttr);
if (value !== null) return value;
// 6. 若未找到且允许查找子节点,则递归搜索
if (searchChildren && weapon.node.children) {
for (const child of weapon.node.children) {
const childValue = getWeaponAttribute({ node: child }, rawAttr, true);
if (childValue !== null) return childValue;
}
}
// 7. 最终未找到返回 null
return null;
}
function filterAttributes() {
filteredAttributes.value = allAttributes.value.filter(attr =>
attr.toLowerCase().includes(attributeSearchText.value.toLowerCase())
)
selectAll.value = false
}
function toggleSelectAll() {
selectAll.value = !selectAll.value
if (selectAll.value) {
selectedAttributes.value = [...filteredAttributes.value]
} else {
selectedAttributes.value = []
}
}
function clearSelectedAttributes() {
selectedAttributes.value = []
}
function removeAttribute(attr) {
selectedAttributes.value = selectedAttributes.value.filter(a => a !== attr)
}
function prevPage() {
if (currentPage.value > 1) currentPage.value--
}
function nextPage() {
if (currentPage.value < totalPages.value) currentPage.value++
}
function toggleTableDirection() {
isReversed.value = !isReversed.value
}
function transposeTable() {
isTransposed.value = !isTransposed.value
}
function getCellClass(value) {
return value ? 'has-value' : 'no-value'
}
function clearAllSelections() {
selectedWeaponIds.value = []
}
function autoSelectMatched() {
const allMatchedTemplates = new Set()
resultList.value.forEach(item => {
if (item.templates && item.templates.length > 0) {
item.templates.forEach(t => allMatchedTemplates.add(t))
}
})
selectedWeaponIds.value = Array.from(allMatchedTemplates)
activeSection.value = 'compare'
}
function handleWeaponFile(e) {
const file = e.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = () => {
const parser = new DOMParser()
const newContent = reader.result
weaponXmlData.value = parser.parseFromString(reader.result, 'text/xml')
weaponXml.value = newContent
localStorage.setItem('weaponXml', newContent)
parseWeaponFile(newContent)
updateWeaponData()
}
reader.readAsText(file)
}
///武器数据更新xml
function updateWeaponData() {
if (!selectedGameObject.value || !weaponXmlData.value) {
weaponData.value = []
return
}
const weapons = []
// 查找单位的所有武器模板
for (let slotName of ['WeaponSlotHardpoint', 'WeaponSlotTurret', 'WeaponSlotHierarchicalTurret', 'WeaponSlotContained']) {
const weaponNodes = selectedGameObject.value.querySelectorAll(`Behaviors > WeaponSetUpdate > ${slotName} > Weapon`)
weaponNodes.forEach(weaponNode => {
const templateId = weaponNode.getAttribute('Template')
if (templateId) {
const weaponTemplate = weaponXmlData.value.querySelector(`WeaponTemplate[id="${templateId}"]`)
if (weaponTemplate) {
const attackRange = weaponTemplate.getAttribute('AttackRange')
const minAttackRange = weaponTemplate.getAttribute('MinimumAttackRange')
// 查找DamageNugget
let Radius = 0
let Damage = 0
const damageNugget = weaponTemplate.querySelector('Nuggets > DamageNugget')
if (damageNugget) {
Radius = damageNugget.getAttribute('Radius') || 0
Damage = damageNugget.getAttribute('Damage') || 0
}
weapons.push({
id: templateId,
attackRange,
minAttackRange,
Damage,
Radius
})
}
}
})
}
weaponData.value = weapons
}
////////
function handleCompareWeaponFile(e) {
handleWeaponFile(e)
}
function handleUnitFile(e) {
const file = e.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = () => {
const parser = new DOMParser()
unitDoc.value = parser.parseFromString(reader.result, 'text/xml')
tryMatchAndDisplay()
}
reader.readAsText(file)
}
function parseWeaponFile(fileContent) {
const parser = new DOMParser()
const xml = parser.parseFromString(fileContent, 'text/xml')
const templates = xml.querySelectorAll('WeaponTemplate')
weaponTemplateIds.value = new Set(
Array.from(templates).map(t => t.getAttribute('id')).filter(Boolean)
)
allWeapons.value = Array.from(templates).map(node => ({
id: node.getAttribute('id'),
node
}))
tryMatchAndDisplay()
}
function tryMatchAndDisplay() {
if (!unitDoc.value || weaponTemplateIds.value.size === 0) return
resultList.value = []
const gameObjects = unitDoc.value.querySelectorAll('GameObject')
gameObjects.forEach(go => {
const goId = go.getAttribute('id') || '(无 ID)'
const matchedTemplates = new Set()
for (let slotName of ['WeaponSlotHardpoint', 'WeaponSlotTurret', 'WeaponSlotHierarchicalTurret', 'WeaponSlotContained']) {
const weapons = go.querySelectorAll(`Behaviors > WeaponSetUpdate > ${slotName} > Weapon`)
weapons.forEach(w => {
const tmpl = w.getAttribute('Template')
if (tmpl && weaponTemplateIds.value.has(tmpl)) {
matchedTemplates.add(tmpl)
}
})
if (matchedTemplates.size) break
}
resultList.value.push({
id: goId,
templates: Array.from(matchedTemplates)
})
})
}
function getInheritanceChain(weapon) {
const chain = [weapon.id]
let current = weapon.node.getAttribute('inheritFrom')
while (current) {
chain.push(current)
const parent = allWeapons.value.find(w => w.id === current)
if (!parent) break
current = parent.node.getAttribute('inheritFrom')
}
return chain
}
// 初始化
onMounted(() => {
const storedWeaponXml = localStorage.getItem('weaponXml')
if (storedWeaponXml) {
weaponXml.value = storedWeaponXml
parseWeaponFile(storedWeaponXml)
}
filterAttributes()
})
</script>
<style scoped>
/* 基础样式 */
:root {
--primary-color: #409eff;
--success-color: #67c23a;
--warning-color: #e6a23c;
--danger-color: #f56c6c;
--info-color: #909399;
--text-primary: #303133;
--text-regular: #606266;
--text-secondary: #909399;
--border-color: #dcdfe6;
--bg-color: #f5f7fa;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', Arial, sans-serif;
color: var(--text-primary);
line-height: 1.5;
background-color: #f0f2f5;
}
/* 布局样式 */
.app-container {
display: flex;
min-height: 100vh;
}
.sidebar {
width: 220px;
background-color: #001529;
color: white;
padding: 20px 0;
}
.sidebar h2 {
padding: 0 20px;
margin-bottom: 20px;
font-size: 18px;
font-weight: 600;
}
.sidebar ul {
list-style: none;
}
.sidebar li {
padding: 12px 20px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
}
.sidebar li:hover {
background-color: #1890ff;
}
.sidebar li.active {
background-color: #1890ff;
}
.sidebar li i {
margin-right: 8px;
}
.main-content {
flex: 1;
padding: 20px;
background-color: #f0f2f5;
overflow-y: auto;
}
/* 卡片样式 */
.upload-card, .result-card, .weapon-list-card,
.compare-tools, .info-card, .reload-card,
.pagination-card {
background-color: white;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
padding: 20px;
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.card-header h3 {
margin: 0;
font-size: 16px;
color: var(--text-primary);
}
/* 上传区域 */
.upload-area {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.upload-card {
flex: 1;
text-align: center;
}
.upload-card h3 {
margin-bottom: 15px;
color: var(--text-primary);
}
.file-upload {
/* display: none; */
}
.file-upload + label {
display: inline-block;
padding: 10px 20px;
background-color: var(--primary-color);
color: white;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.file-upload + label:hover {
background-color: #66b1ff;
}
/* 表格样式 */
.table-responsive {
overflow-x: auto;
}
.result-table, .weapon-table, .comparison-table, .info-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.result-table th, .result-table td,
.weapon-table th, .weapon-table td,
.comparison-table th, .comparison-table td,
.info-table th, .info-table td {
padding: 12px 15px;
border: 1px solid var(--border-color);
text-align: left;
}
.result-table th, .weapon-table th,
.comparison-table th, .info-table th {
background-color: #fafafa;
font-weight: 600;
color: var(--text-primary);
}
.result-table tr:nth-child(even),
.weapon-table tr:nth-child(even),
.info-table tr:nth-child(even) {
background-color: #f9f9f9;
}
.result-table tr:hover,
.weapon-table tr:hover,
.info-table tr:hover {
background-color: #f5f5f5;
}
.no-match {
color: var(--text-secondary);
font-style: italic;
}
/* 分页样式 */
.pagination-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
}
.page-btn {
padding: 8px 16px;
background-color: white;
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
transition: all 0.3s;
}
.page-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.page-btn:hover:not(:disabled) {
color: var(--primary-color);
border-color: var(--primary-color);
}
.page-info {
font-size: 14px;
color: var(--text-regular);
}
/* 按钮样式 */
.action-btn {
padding: 8px 16px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
transition: background-color 0.3s;
}
.action-btn:hover {
background-color: #66b1ff;
}
.action-btn.danger {
background-color: var(--danger-color);
}
.action-btn.danger:hover {
background-color: #f78989;
}
.tool-btn {
padding: 8px 16px;
background-color: rgb(149, 149, 149);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
transition: all 0.3s;
}
.tool-btn:hover {
color: var(--primary-color);
border-color: var(--primary-color);
}
.tool-btn.active {
background-color: rgb(206, 206, 206);
color: rgb(0, 0, 0);
border-color: var(--primary-color);
}
.small-btn {
padding: 6px 12px;
font-size: 12px;
border-radius: 3px;
background-color: white;
border: 1px solid var(--border-color);
cursor: pointer;
transition: all 0.3s;
}
.small-btn:hover {
color: var(--primary-color);
border-color: var(--primary-color);
}
.small-btn.danger {
color: var(--danger-color);
border-color: var(--danger-color);
}
.small-btn.danger:hover {
background-color: #fef0f0;
}
/* 属性选择器 */
.attribute-selector-panel {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.attribute-controls {
flex: 1;
max-width: 350px;
}
.search-box {
position: relative;
margin-bottom: 10px;
display: flex;
gap: 10px;
}
.search-box .icon-search {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
color: var(--text-secondary);
}
.search-input {
flex: 1;
padding: 8px 12px 8px 32px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s;
}
.search-input:focus {
border-color: var(--primary-color);
outline: none;
}
.button-group {
display: flex;
gap: 8px;
}
.select-container {
position: relative;
}
.multi-select {
width: 100%;
padding: 8px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: white;
font-size: 14px;
min-height: 200px;
transition: border-color 0.3s;
}
.multi-select:focus {
border-color: var(--primary-color);
outline: none;
}
.select-option {
padding: 8px 12px;
cursor: pointer;
}
.select-option:hover {
background-color: #f5f7fa;
}
.selection-info {
margin-top: 8px;
font-size: 12px;
color: var(--text-secondary);
text-align: right;
}
/* 已选属性预览 */
.selected-preview {
flex: 1;
min-width: 0;
background: white;
border-radius: 6px;
border: 1px solid var(--border-color);
}
.preview-header {
padding: 12px 15px;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.preview-header h4 {
margin: 0;
font-size: 14px;
color: var(--text-primary);
}
.hint-text {
font-size: 12px;
color: var(--text-secondary);
}
.selected-tags {
padding: 12px;
display: flex;
flex-wrap: wrap;
gap: 8px;
max-height: 200px;
overflow-y: auto;
}
.tag {
display: inline-flex;
align-items: center;
background: #f0f7ff;
padding: 4px 10px;
border-radius: 4px;
font-size: 13px;
color: var(--primary-color);
cursor: pointer;
transition: all 0.3s;
}
.tag:hover {
background: #d9ecff;
}
.tag .icon-close {
margin-left: 6px;
font-size: 12px;
opacity: 0.7;
}
.tag:hover .icon-close {
opacity: 1;
}
.empty-hint {
color: var(--text-secondary);
font-size: 13px;
font-style: italic;
}
/* 对比结果表格 */
.comparison-table {
width: 100%;
border-collapse: collapse;
}
.comparison-table th, .comparison-table td {
padding: 12px 15px;
border: 1px solid var(--border-color);
}
.comparison-table th {
background-color: #fafafa;
font-weight: 600;
}
.attr-name {
font-weight: 600;
background-color: #fafafa !important;
}
.has-value {
background-color: #f6ffed;
}
.no-value {
background-color: #fff2f0;
color: var(--text-secondary);
}
/* 空状态 */
.empty-state {
padding: 40px 0;
text-align: center;
color: var(--text-secondary);
}
.empty-state.small {
padding: 20px 0;
}
.empty-state .icon {
font-size: 48px;
margin-bottom: 15px;
opacity: 0.5;
}
.empty-state.small .icon {
font-size: 24px;
}
.empty-state p {
margin-top: 10px;
}
/* 继承排行榜 */
.ranking-list {
padding: 10px 0;
}
.ranking-item {
display: flex;
align-items: center;
padding: 10px 15px;
border-bottom: 1px dashed var(--border-color);
}
.ranking-item:last-child {
border-bottom: none;
}
.rank {
width: 24px;
height: 24px;
background-color: #f5f5f5;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
margin-right: 12px;
}
.ranking-item:nth-child(1) .rank {
background-color: #fff7e6;
color: #faad14;
}
.ranking-item:nth-child(2) .rank {
background-color: #fff2f0;
color: #ff4d4f;
}
.ranking-item:nth-child(3) .rank {
background-color: #f6ffed;
color: #52c41a;
}
.weapon-name {
flex: 1;
font-size: 14px;
}
.count {
font-size: 13px;
color: var(--text-secondary);
}
/* 复选框样式 */
.checkbox-container {
display: block;
position: relative;
padding-left: 25px;
cursor: pointer;
user-select: none;
}
.checkbox-container input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.checkmark {
position: absolute;
top: 0;
left: 0;
height: 18px;
width: 18px;
background-color: white;
border: 1px solid var(--border-color);
border-radius: 3px;
}
.checkbox-container:hover input ~ .checkmark {
border-color: var(--primary-color);
}
.checkbox-container input:checked ~ .checkmark {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.checkmark:after {
content: "";
position: absolute;
display: none;
}
.checkbox-container input:checked ~ .checkmark:after {
display: block;
}
.checkbox-container .checkmark:after {
left: 6px;
top: 2px;
width: 5px;
height: 10px;
border: solid rgb(19, 38, 255);
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
/* 图标样式 */
.icon {
display: inline-block;
width: 1em;
height: 1em;
vertical-align: middle;
fill: currentColor;
}
/* 响应式设计 */
@media (max-width: 992px) {
.app-container {
flex-direction: column;
}
.sidebar {
width: 100%;
padding: 10px;
}
.sidebar ul {
display: flex;
}
.sidebar li {
padding: 10px 15px;
}
.upload-area {
flex-direction: column;
}
.attribute-selector-panel {
flex-direction: column;
}
.attribute-controls {
max-width: 100%;
}
}
@media (max-width: 768px) {
.pagination-controls {
flex-direction: column;
gap: 10px;
}
.card-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.button-group {
flex-wrap: wrap;
}
}
.GOBeditor-section {
padding: 20px;
font-family: Arial, sans-serif;
}
.file-input-container {
margin-bottom: 20px;
}
.unit-editor {
display: flex;
gap: 20px;
}
.unit-list {
width: 250px;
border-right: 1px solid #ddd;
padding-right: 10px;
max-height: 80vh;
overflow-y: auto;
}
.unit-item {
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
margin-bottom: 5px;
background-color: #f5f5f5;
}
.unit-item:hover {
background-color: #e0e0e0;
}
.unit-details {
flex: 1;
padding: 10px;
background-color: #f9f9f9;
border-radius: 5px;
}
.detail-row {
display: flex;
margin-bottom: 10px;
}
.detail-label {
font-weight: bold;
min-width: 150px;
color: #333;
}
.detail-value {
color: #666;
}
.detail-group {
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 5px;
overflow: hidden;
}
.group-header {
padding: 8px 12px;
background-color: #e9e9e9;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.group-header:hover {
background-color: #e0e0e0;
}
.group-content {
padding: 10px 15px;
background-color: white;
}
.toggle-icon {
font-weight: bold;
}
/* 新增样式 */
.weapon-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
margin-left: 10px;
padding: 8px;
background-color: #f0f0f0;
border-radius: 4px;
}
.weapon-stats div {
white-space: nowrap;
}
/* 调整文件输入容器样式 */
.file-input-container {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 20px;
}
.file-input {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: white;
}
</style>