2129 lines
67 KiB
Vue
2129 lines
67 KiB
Vue
<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><!--GOBedit:gameobject--->
|
||
</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> |