需求改了好多,不需要自己输用户名和qq号了

This commit is contained in:
Kunagisa 2025-07-10 18:21:05 +08:00
parent 66efa6c310
commit 93f42d3d9b
6 changed files with 3097 additions and 1 deletions

View 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;
}
}

View 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>

View 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>

File diff suppressed because it is too large Load Diff

View File

@ -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'] }
},
{

View 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>