需求改了好多,不需要自己输用户名和qq号了
This commit is contained in:
parent
66efa6c310
commit
93f42d3d9b
500
src/assets/styles/weapon.css
Normal file
500
src/assets/styles/weapon.css
Normal file
@ -0,0 +1,500 @@
|
||||
/* 武器工具集样式 */
|
||||
.weapon-match-container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: white;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.tab-navigation {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 12px 24px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 25px;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-color: rgba(255, 255, 255, 0.6);
|
||||
box-shadow: 0 4px 15px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* 通用内容卡片样式 */
|
||||
.content-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-header i {
|
||||
margin-right: 12px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.table-container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.result-table,
|
||||
.weapon-table,
|
||||
.comparison-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 {
|
||||
padding: 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.result-table th,
|
||||
.weapon-table th,
|
||||
.comparison-table th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.result-table tr:hover,
|
||||
.weapon-table tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.action-btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.danger:hover {
|
||||
background: #c82333;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
padding: 10px 20px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.tool-btn:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.tool-btn.active {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.small-btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.small-btn:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.small-btn.danger {
|
||||
color: #dc3545;
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.small-btn.danger:hover {
|
||||
background: #fff5f5;
|
||||
}
|
||||
|
||||
/* 输入框样式 */
|
||||
.file-input,
|
||||
.detail-input,
|
||||
.search-input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.file-input:focus,
|
||||
.detail-input:focus,
|
||||
.search-input:focus {
|
||||
border-color: #667eea;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
/* 复选框样式 */
|
||||
.checkbox-wrapper {
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-wrapper input {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: white;
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.checkbox-wrapper input:checked ~ .checkmark {
|
||||
background: #667eea;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.checkmark:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: none;
|
||||
left: 6px;
|
||||
top: 2px;
|
||||
width: 6px;
|
||||
height: 10px;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.checkbox-wrapper input:checked ~ .checkmark:after {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 标签样式 */
|
||||
.template-tag,
|
||||
.attribute-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: #e7f3ff;
|
||||
color: #1c7ed6;
|
||||
padding: 4px 12px;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.template-tag:hover,
|
||||
.attribute-tag:hover {
|
||||
background: #d0ebff;
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.empty-state {
|
||||
padding: 60px 30px;
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.empty-state.small {
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.empty-state.small i {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.empty-state.small p {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 对比结果样式 */
|
||||
.attr-name {
|
||||
font-weight: 600;
|
||||
background: #f8f9fa !important;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.has-value {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.no-value {
|
||||
background: #fff2f0;
|
||||
color: #cf1322;
|
||||
}
|
||||
|
||||
/* 分页样式 */
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
padding: 8px 16px;
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.page-btn:hover:not(:disabled) {
|
||||
border-color: #667eea;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.page-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.page-info {
|
||||
font-size: 14px;
|
||||
color: #6c757d;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.weapon-match-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.tab-navigation {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
width: 200px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.action-btn,
|
||||
.tool-btn {
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.result-table th,
|
||||
.result-table td,
|
||||
.weapon-table th,
|
||||
.weapon-table td,
|
||||
.comparison-table th,
|
||||
.comparison-table td {
|
||||
padding: 12px 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.weapon-match-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
width: 100%;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.action-btn,
|
||||
.tool-btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.result-table th,
|
||||
.result-table td,
|
||||
.weapon-table th,
|
||||
.weapon-table td,
|
||||
.comparison-table th,
|
||||
.comparison-table td {
|
||||
padding: 8px 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.template-tag,
|
||||
.attribute-tag {
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 30px 15px;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 36px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
925
src/components/weapon/ObjectEditor.vue
Normal file
925
src/components/weapon/ObjectEditor.vue
Normal file
@ -0,0 +1,925 @@
|
||||
<template>
|
||||
<div class="object-editor">
|
||||
<div class="table-container">
|
||||
<div class="section-header">
|
||||
<h2>物体编辑器</h2>
|
||||
</div>
|
||||
|
||||
<!-- 文件控制区 -->
|
||||
<div class="file-controls">
|
||||
<div class="file-input-group">
|
||||
<div class="file-input-item">
|
||||
<label class="file-label">
|
||||
<i class="icon-upload"></i>
|
||||
加载单位.xml
|
||||
<input type="file" @change="handleUnitFile" accept=".xml" class="file-input">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="file-input-item">
|
||||
<label class="file-label">
|
||||
<i class="icon-upload"></i>
|
||||
加载Weapon.xml
|
||||
<input type="file" @change="handleWeaponFile" accept=".xml" class="file-input">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="unitDoc"
|
||||
@click="exportUnitXml"
|
||||
class="export-btn"
|
||||
>
|
||||
<i class="icon-export"></i>
|
||||
导出XML
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑器主体 -->
|
||||
<div v-if="unitDoc" class="editor-main">
|
||||
<!-- 单位列表 -->
|
||||
<div class="unit-list">
|
||||
<div class="list-header">
|
||||
<h3>GameObject 列表</h3>
|
||||
</div>
|
||||
<div class="list-content">
|
||||
<div
|
||||
v-for="gameObject in unitDoc.querySelectorAll('GameObject')"
|
||||
:key="gameObject.getAttribute('id')"
|
||||
class="unit-item"
|
||||
:class="{ selected: selectedGameObject === gameObject }"
|
||||
@click="selectGameObject(gameObject)"
|
||||
>
|
||||
<i class="icon-unit"></i>
|
||||
{{ gameObject.getAttribute('id') || '未命名单位' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详情面板 -->
|
||||
<div v-if="selectedGameObject" class="details-panel">
|
||||
<div class="panel-header">
|
||||
<h3>单位详细信息</h3>
|
||||
</div>
|
||||
|
||||
<div class="panel-content">
|
||||
<!-- ID 面板 -->
|
||||
<div class="detail-row">
|
||||
<label class="detail-label">ID:</label>
|
||||
<input
|
||||
class="detail-input"
|
||||
v-model="editableFields.id"
|
||||
@change="updateGameObject"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- CSF 面板 -->
|
||||
<div class="detail-group">
|
||||
<div class="group-header" @click="toggleGroup('csf')">
|
||||
<span>CSF相关描述</span>
|
||||
<i :class="['toggle-icon', isGroupOpen('csf') ? 'icon-minus' : 'icon-plus']"></i>
|
||||
</div>
|
||||
<div v-if="isGroupOpen('csf')" class="group-content">
|
||||
<div class="detail-row">
|
||||
<label class="detail-label">DisplayName:</label>
|
||||
<input
|
||||
class="detail-input"
|
||||
v-model="editableFields.displayName"
|
||||
@change="updateDisplayName"
|
||||
>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<label class="detail-label">TypeDescription:</label>
|
||||
<input
|
||||
class="detail-input"
|
||||
v-model="editableFields.typeDescription"
|
||||
@change="updateGameObject"
|
||||
>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<label class="detail-label">Description:</label>
|
||||
<input
|
||||
class="detail-input"
|
||||
v-model="editableFields.description"
|
||||
@change="updateGameObject"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 建造时间面板 -->
|
||||
<div class="detail-row">
|
||||
<label class="detail-label">建造时间:</label>
|
||||
<div class="input-with-suffix">
|
||||
<input
|
||||
class="detail-input"
|
||||
v-model="editableFields.buildTime"
|
||||
@change="updateGameObject"
|
||||
type="number"
|
||||
>
|
||||
<span class="suffix">秒</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 科技需求面板 -->
|
||||
<div class="detail-group">
|
||||
<div class="group-header" @click="toggleGroup('tech')">
|
||||
<span>科技需求</span>
|
||||
<i :class="['toggle-icon', isGroupOpen('tech') ? 'icon-minus' : 'icon-plus']"></i>
|
||||
</div>
|
||||
<div v-if="isGroupOpen('tech')" class="group-content">
|
||||
<div class="detail-row">
|
||||
<label class="detail-label">NeededUpgrade:</label>
|
||||
<input
|
||||
class="detail-input"
|
||||
v-model="editableFields.neededUpgrade"
|
||||
@change="updateNeededUpgrade"
|
||||
>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<label class="detail-label">建造数量上限:</label>
|
||||
<input
|
||||
class="detail-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>
|
||||
<i :class="['toggle-icon', isGroupOpen('costHealth') ? 'icon-minus' : 'icon-plus']"></i>
|
||||
</div>
|
||||
<div v-if="isGroupOpen('costHealth')" class="group-content">
|
||||
<div class="detail-row">
|
||||
<label class="detail-label">造价:</label>
|
||||
<input
|
||||
class="detail-input"
|
||||
v-model="editableFields.buildCost"
|
||||
@change="updateBuildCost"
|
||||
type="number"
|
||||
>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<label class="detail-label">生命值:</label>
|
||||
<input
|
||||
class="detail-input"
|
||||
v-model="editableFields.maxHealth"
|
||||
@change="updateMaxHealth"
|
||||
type="number"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 武器射程面板 -->
|
||||
<div class="detail-group">
|
||||
<div class="group-header" @click="toggleGroup('range')">
|
||||
<span>武器射程</span>
|
||||
<i :class="['toggle-icon', isGroupOpen('range') ? 'icon-minus' : 'icon-plus']"></i>
|
||||
</div>
|
||||
<div v-if="isGroupOpen('range')" class="group-content">
|
||||
<div v-if="weaponData.length === 0" class="empty-weapons">
|
||||
<i class="icon-empty"></i>
|
||||
<span>无武器数据</span>
|
||||
</div>
|
||||
<div v-else class="weapon-list">
|
||||
<div
|
||||
v-for="(weapon, index) in weaponData"
|
||||
:key="index"
|
||||
class="weapon-item"
|
||||
>
|
||||
<div class="weapon-header">
|
||||
<strong>武器 {{ index + 1 }} ({{ weapon.id }})</strong>
|
||||
</div>
|
||||
<div class="weapon-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">攻击范围:</span>
|
||||
<span class="stat-value">{{ weapon.attackRange || 0 }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">最小射程:</span>
|
||||
<span class="stat-value">{{ weapon.minAttackRange || 0 }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">伤害值:</span>
|
||||
<span class="stat-value">{{ weapon.Damage || 0 }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">伤害范围:</span>
|
||||
<span class="stat-value">{{ weapon.Radius || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="empty-state">
|
||||
<i class="icon-empty"></i>
|
||||
<p>请加载单位 XML 文件开始编辑</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const weaponXmlData = ref(null)
|
||||
const weaponData = ref([])
|
||||
const selectedGameObject = ref(null)
|
||||
const unitDoc = ref(null)
|
||||
const openGroups = ref(['csf', 'tech', 'costHealth'])
|
||||
const editableFields = ref({
|
||||
id: '',
|
||||
displayName: '',
|
||||
typeDescription: '',
|
||||
description: '',
|
||||
buildTime: '',
|
||||
neededUpgrade: '',
|
||||
maxSimultaneous: '',
|
||||
buildCost: '',
|
||||
maxHealth: ''
|
||||
})
|
||||
|
||||
const selectGameObject = (gameObject) => {
|
||||
selectedGameObject.value = gameObject
|
||||
updateEditableFields()
|
||||
updateWeaponData()
|
||||
}
|
||||
|
||||
const 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) || ''
|
||||
}
|
||||
}
|
||||
|
||||
const 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)
|
||||
}
|
||||
|
||||
const 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
|
||||
}
|
||||
|
||||
const 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)
|
||||
}
|
||||
|
||||
const 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)
|
||||
}
|
||||
|
||||
const 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)
|
||||
}
|
||||
|
||||
const 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")
|
||||
const xmlDeclaration = '<?xml version="1.0" encoding="utf-8" ?>'
|
||||
|
||||
// 添加基本结构
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
// 添加武器模板
|
||||
if (weaponData.value.length > 0 && weaponXmlData.value) {
|
||||
weaponData.value.forEach(weapon => {
|
||||
const weaponTemplate = weaponXmlData.value.querySelector(`WeaponTemplate[id="${weapon.id}"]`)
|
||||
if (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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let configXmlString = serializer.serializeToString(configXml)
|
||||
configXmlString = xmlDeclaration + '\n' + formatXml(configXmlString)
|
||||
|
||||
setTimeout(() => {
|
||||
downloadFile('config.xml', configXmlString)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const 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
|
||||
}
|
||||
|
||||
const 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)
|
||||
}
|
||||
|
||||
const toggleGroup = (groupName) => {
|
||||
if (openGroups.value.includes(groupName)) {
|
||||
openGroups.value = openGroups.value.filter(g => g !== groupName)
|
||||
} else {
|
||||
openGroups.value.push(groupName)
|
||||
}
|
||||
}
|
||||
|
||||
const isGroupOpen = (groupName) => {
|
||||
return openGroups.value.includes(groupName)
|
||||
}
|
||||
|
||||
const extractDisplayName = (gameObject) => {
|
||||
const displayNameNode = gameObject.querySelector('DisplayName')
|
||||
return displayNameNode ? displayNameNode.textContent.trim() : null
|
||||
}
|
||||
|
||||
const getNeededUpgrade = (gameObject) => {
|
||||
const gameDependency = gameObject.querySelector('GameDependency')
|
||||
return gameDependency ? gameDependency.getAttribute('NeededUpgrade') : null
|
||||
}
|
||||
|
||||
const getBuildCost = (gameObject) => {
|
||||
const resourceInfo = gameObject.querySelector('ObjectResourceInfo')
|
||||
if (!resourceInfo) return null
|
||||
|
||||
const buildCost = resourceInfo.querySelector('BuildCost')
|
||||
if (!buildCost) return null
|
||||
|
||||
return buildCost.getAttribute('Amount')
|
||||
}
|
||||
|
||||
const 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 updateWeaponData = () => {
|
||||
if (!selectedGameObject.value || !weaponXmlData.value) {
|
||||
weaponData.value = []
|
||||
return
|
||||
}
|
||||
|
||||
const weapons = []
|
||||
const slotNames = ['WeaponSlotHardpoint', 'WeaponSlotTurret', 'WeaponSlotHierarchicalTurret', 'WeaponSlotContained']
|
||||
|
||||
slotNames.forEach(slotName => {
|
||||
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')
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const handleWeaponFile = (e) => {
|
||||
const file = e.target.files[0]
|
||||
if (!file) return
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const parser = new DOMParser()
|
||||
weaponXmlData.value = parser.parseFromString(reader.result, 'text/xml')
|
||||
updateWeaponData()
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
const 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')
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.object-editor {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1a237e;
|
||||
}
|
||||
|
||||
.file-controls {
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.file-input-group {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-input-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-label {
|
||||
position: relative;
|
||||
padding: 10px 20px;
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.file-label:hover {
|
||||
border-color: #1a237e;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.export-btn {
|
||||
padding: 10px 20px;
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.export-btn:hover {
|
||||
background: #218838;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.editor-main {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.unit-list {
|
||||
border-right: 1px solid #f0f0f0;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.list-header h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.list-content {
|
||||
padding: 8px 0;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.unit-item {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-left: 3px solid transparent;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.unit-item:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.unit-item.selected {
|
||||
background: #e3f2fd;
|
||||
border-left-color: #1a237e;
|
||||
color: #1a237e;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.details-panel {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 16px 20px;
|
||||
background: white;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
padding: 20px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
min-width: 120px;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.detail-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.detail-input:focus {
|
||||
border-color: #1a237e;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(26, 35, 126, 0.1);
|
||||
}
|
||||
|
||||
.input-with-suffix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.suffix {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.detail-group {
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
padding: 12px 16px;
|
||||
background: #f8f9fa;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: background-color 0.2s ease;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.group-header:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.group-content {
|
||||
padding: 16px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.empty-weapons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.weapon-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.weapon-item {
|
||||
padding: 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.weapon-header {
|
||||
margin-bottom: 12px;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.weapon-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 60px 40px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.editor-main {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.unit-list {
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.list-content {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.file-input-group {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
min-width: auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.weapon-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
396
src/components/weapon/TemplateMatch.vue
Normal file
396
src/components/weapon/TemplateMatch.vue
Normal file
@ -0,0 +1,396 @@
|
||||
<template>
|
||||
<div class="template-match">
|
||||
<div class="table-container">
|
||||
<div class="section-header">
|
||||
<h2>WeaponTemplate 匹配到单位 GameObject</h2>
|
||||
</div>
|
||||
|
||||
<!-- 文件上传区 -->
|
||||
<div class="upload-section">
|
||||
<div class="upload-card">
|
||||
<div class="upload-header">
|
||||
<i class="icon-upload"></i>
|
||||
<h3>上传 Weapon.xml</h3>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
@change="handleWeaponFile"
|
||||
accept=".xml"
|
||||
class="file-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="upload-card">
|
||||
<div class="upload-header">
|
||||
<i class="icon-upload"></i>
|
||||
<h3>上传单位 XML</h3>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
@change="handleUnitFile"
|
||||
accept=".xml"
|
||||
class="file-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 匹配结果 -->
|
||||
<div v-if="resultList.length" class="result-section">
|
||||
<div class="result-header">
|
||||
<h3>匹配结果</h3>
|
||||
<button
|
||||
@click="autoSelectMatched"
|
||||
class="action-btn primary"
|
||||
>
|
||||
<i class="icon-auto-select"></i>
|
||||
自动选择匹配项
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="result-table-container">
|
||||
<table class="result-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>GameObject ID</th>
|
||||
<th>匹配到的 WeaponTemplate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, index) in resultList" :key="index">
|
||||
<td class="object-id">{{ item.id }}</td>
|
||||
<td class="templates">
|
||||
<span v-if="item.templates && item.templates.length > 0" class="template-list">
|
||||
<span
|
||||
v-for="template in item.templates"
|
||||
:key="template"
|
||||
class="template-tag"
|
||||
>
|
||||
{{ template }}
|
||||
</span>
|
||||
</span>
|
||||
<span v-else class="no-match">None</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="empty-state">
|
||||
<i class="icon-empty"></i>
|
||||
<p>请上传 Weapon.xml 和单位 XML 文件以开始匹配</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const emit = defineEmits(['switch-tab'])
|
||||
|
||||
const resultList = ref([])
|
||||
const weaponTemplateIds = ref(new Set())
|
||||
const unitDoc = ref(null)
|
||||
|
||||
const handleWeaponFile = (e) => {
|
||||
const file = e.target.files[0]
|
||||
if (!file) return
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
parseWeaponFile(reader.result)
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
const 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)
|
||||
}
|
||||
|
||||
const 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)
|
||||
)
|
||||
|
||||
// 存储到 localStorage
|
||||
localStorage.setItem('weaponXml', fileContent)
|
||||
|
||||
tryMatchAndDisplay()
|
||||
}
|
||||
|
||||
const 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()
|
||||
|
||||
const slotNames = ['WeaponSlotHardpoint', 'WeaponSlotTurret', 'WeaponSlotHierarchicalTurret', 'WeaponSlotContained']
|
||||
|
||||
for (const slotName of slotNames) {
|
||||
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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const autoSelectMatched = () => {
|
||||
const allMatchedTemplates = new Set()
|
||||
resultList.value.forEach(item => {
|
||||
if (item.templates && item.templates.length > 0) {
|
||||
item.templates.forEach(t => allMatchedTemplates.add(t))
|
||||
}
|
||||
})
|
||||
|
||||
// 将匹配的模板存储起来,供对比页面使用
|
||||
localStorage.setItem('selectedWeaponIds', JSON.stringify(Array.from(allMatchedTemplates)))
|
||||
|
||||
// 切换到对比页面
|
||||
emit('switch-tab', 'compare')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.template-match {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1a237e;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.upload-card {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.upload-card:hover {
|
||||
border-color: #1a237e;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.upload-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.upload-header i {
|
||||
font-size: 24px;
|
||||
color: #1a237e;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.upload-header h3 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.file-input:hover {
|
||||
border-color: #1a237e;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
padding: 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.result-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: #1a237e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.primary:hover {
|
||||
background: #0d47a1;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.result-table-container {
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.result-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.result-table th,
|
||||
.result-table td {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.result-table th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
color: #1a237e;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.result-table tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.result-table td {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.object-id {
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.template-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.template-tag {
|
||||
background: #e3f2fd;
|
||||
color: #1a237e;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.no-match {
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.upload-section {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.template-list {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
1128
src/components/weapon/WeaponCompare.vue
Normal file
1128
src/components/weapon/WeaponCompare.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -47,7 +47,7 @@ const routes = [
|
||||
{
|
||||
path: 'weapon-match',
|
||||
name: 'WeaponMatch',
|
||||
component: () => import('@/views/index/WeaponMatch.vue'),
|
||||
component: () => import('@/views/weapon/WeaponMatch.vue'),
|
||||
meta: { requiresAuth: true, requiredPrivilege: ['lv-admin','lv-mod'] }
|
||||
},
|
||||
{
|
||||
|
147
src/views/weapon/WeaponMatch.vue
Normal file
147
src/views/weapon/WeaponMatch.vue
Normal file
@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div class="weapon-match">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1>武器工具集</h1>
|
||||
<div class="header-subtitle">
|
||||
<span>武器模板匹配、对比分析与单位编辑</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 功能导航 -->
|
||||
<div class="action-bar">
|
||||
<div class="nav-tabs">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:class="['tab-btn', { active: activeTab === tab.key }]"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
<i :class="tab.icon"></i>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 功能区域 -->
|
||||
<div class="content-wrapper">
|
||||
<!-- 模板匹配 -->
|
||||
<TemplateMatch
|
||||
v-if="activeTab === 'match'"
|
||||
@switch-tab="switchTab"
|
||||
/>
|
||||
|
||||
<!-- 武器对比 -->
|
||||
<WeaponCompare
|
||||
v-if="activeTab === 'compare'"
|
||||
/>
|
||||
|
||||
<!-- 物体编辑器 -->
|
||||
<ObjectEditor
|
||||
v-if="activeTab === 'editor'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import TemplateMatch from '@/components/weapon/TemplateMatch.vue'
|
||||
import WeaponCompare from '@/components/weapon/WeaponCompare.vue'
|
||||
import ObjectEditor from '@/components/weapon/ObjectEditor.vue'
|
||||
|
||||
const activeTab = ref('match')
|
||||
|
||||
const tabs = [
|
||||
{ key: 'match', label: '模板匹配', icon: 'icon-match' },
|
||||
{ key: 'compare', label: '武器对比', icon: 'icon-compare' },
|
||||
{ key: 'editor', label: '物体编辑器', icon: 'icon-edit' }
|
||||
]
|
||||
|
||||
const switchTab = (tabKey) => {
|
||||
activeTab.value = tabKey
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.weapon-match {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1a237e;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
color: #1a237e;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
background: #f5f7fa;
|
||||
border-color: #1a237e;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: #1a237e;
|
||||
color: white;
|
||||
border-color: #1a237e;
|
||||
}
|
||||
|
||||
.tab-btn i {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.weapon-match {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
Loading…
x
Reference in New Issue
Block a user