需求改了好多,不需要自己输用户名和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',
|
path: 'weapon-match',
|
||||||
name: 'WeaponMatch',
|
name: 'WeaponMatch',
|
||||||
component: () => import('@/views/index/WeaponMatch.vue'),
|
component: () => import('@/views/weapon/WeaponMatch.vue'),
|
||||||
meta: { requiresAuth: true, requiredPrivilege: ['lv-admin','lv-mod'] }
|
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