feat(editor): 完成英雄配置编辑器全功能开发
本提交新增完整的英雄配置可视化编辑器: 1. 初始化前后端项目结构,配置monorepo工作区 2. 实现后端API服务,支持读取游戏配置、导出hexport.ts文件 3. 开发前端Vue应用,包含完整的英雄增删改、属性配置、技能编辑功能 4. 实现多类型编辑器组件:基础属性、普攻/大招编辑、触发技能、光环、复活、属性加成等 5. 清理冗余的配置元数据文件
This commit is contained in:
271
put/web/src/App.vue
Normal file
271
put/web/src/App.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<div class="hero-app">
|
||||
<header class="hero-header">
|
||||
<h1>英雄配置编辑器</h1>
|
||||
<div class="actions">
|
||||
<el-button size="small" @click="refreshMeta" :loading="loadingMeta">刷新配置数据</el-button>
|
||||
<el-button size="small" type="warning" @click="doExport" :disabled="!heroes.length">导出 hexport.ts</el-button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="hero-main">
|
||||
<aside class="hero-sidebar">
|
||||
<div class="side-head">
|
||||
<span>英雄列表 ({{ heroes.length }})</span>
|
||||
<el-button size="small" type="primary" text @click="addHero">+ 新增</el-button>
|
||||
</div>
|
||||
<div class="hero-list">
|
||||
<div
|
||||
v-for="(h, i) in heroes"
|
||||
:key="i"
|
||||
class="hero-item"
|
||||
:class="{ active: i === activeIdx }"
|
||||
@click="activeIdx = i"
|
||||
>
|
||||
<div>
|
||||
<div>{{ h.uuid }} {{ h.name || '(未命名)' }}</div>
|
||||
<div class="meta">{{ roleLabel(h.roleKey) }} · {{ typeLabel(h.typeKey) }} · ★{{ h.rarity }}</div>
|
||||
</div>
|
||||
<el-button type="danger" size="small" text @click.stop="removeHero(i)">删</el-button>
|
||||
</div>
|
||||
<div v-if="!heroes.length" class="empty">点击右上角"新增"创建英雄</div>
|
||||
</div>
|
||||
</aside>
|
||||
<section class="hero-editor" v-if="current">
|
||||
<!-- 基础属性 -->
|
||||
<div class="panel">
|
||||
<h3>基础属性</h3>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="uuid">
|
||||
<el-input-number v-model="current.uuid" :min="5001" :controls="false" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="current.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="资源路径 path">
|
||||
<el-input v-model="current.path" placeholder="hm1 / hk2 ..." />
|
||||
</el-form-item>
|
||||
<el-form-item label="品质 rarity">
|
||||
<el-select v-model="current.rarity">
|
||||
<el-option v-for="n in 5" :key="n" :label="`${n} 星`" :value="n" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="职业 role">
|
||||
<el-select v-model="current.roleKey">
|
||||
<el-option v-for="r in HERO_ROLES" :key="r.key" :label="r.label" :value="r.key" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="成长 grow_type">
|
||||
<el-select v-model="current.growKey">
|
||||
<el-option v-for="g in GROW_TYPES" :key="g.key" :label="g.label" :value="g.key" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="形态 type">
|
||||
<el-select v-model="current.typeKey">
|
||||
<el-option v-for="t in H_TYPES" :key="t.key" :label="t.label" :value="t.key" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="攻击距离 dis(可选)">
|
||||
<el-input-number v-model="current.dis" :min="0" :controls="false" style="width: 100%" placeholder="默认按形态" />
|
||||
</el-form-item>
|
||||
<el-form-item label="hp 偏移">
|
||||
<el-input-number v-model="current.statBase.hp" :controls="false" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="ap 偏移">
|
||||
<el-input-number v-model="current.statBase.ap" :controls="false" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="def 偏移">
|
||||
<el-input-number v-model="current.statBase.def" :controls="false" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="注释">
|
||||
<el-input v-model="current.comment" placeholder="导出文件中显示在条目上方" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="muted">基础值 = HERO_ROLE_BASE[{{ current.roleKey }}] → hp {{ roleBase?.hp }} / ap {{ roleBase?.ap }} / def {{ roleBase?.def }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 普攻 -->
|
||||
<div class="panel">
|
||||
<h3>普攻 skills[0]</h3>
|
||||
<SkillEditor v-if="basicSkill" :skill="basicSkill" :options="basicSkillOptions" />
|
||||
<el-button v-else size="small" type="primary" @click="addBasicSkill">+ 添加普攻</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 大招 -->
|
||||
<div class="panel">
|
||||
<h3>大招 skills[1](与 revive 互斥)</h3>
|
||||
<template v-if="current.revive.length === 0">
|
||||
<SkillEditor v-if="ultSkill" :skill="ultSkill" :options="ultSkillOptions" :is-ult="true" />
|
||||
<el-button v-else size="small" type="primary" @click="addUltSkill">+ 添加大招</el-button>
|
||||
</template>
|
||||
<el-alert v-else type="info" :closable="false">已配置 revive,大招不可用</el-alert>
|
||||
</div>
|
||||
|
||||
<!-- 触发技能 -->
|
||||
<div class="panel">
|
||||
<h3>触发技能(lv6 激活)</h3>
|
||||
<el-tabs>
|
||||
<el-tab-pane label="atking 攻击后"><TriggerGroupEditor :model="current.atking" :skill-options="allSkills" :skill-map="skillMap" /></el-tab-pane>
|
||||
<el-tab-pane label="atked 受击后"><TriggerGroupEditor :model="current.atked" :skill-options="allSkills" :skill-map="skillMap" /></el-tab-pane>
|
||||
<el-tab-pane label="fstart 战斗开始"><TriggerGroupEditor :model="current.fstart" :skill-options="allSkills" :skill-map="skillMap" /></el-tab-pane>
|
||||
<el-tab-pane label="dead 死亡后"><TriggerGroupEditor :model="current.dead" :skill-options="allSkills" :skill-map="skillMap" /></el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 光环 -->
|
||||
<div class="panel">
|
||||
<h3>驻场光环 field</h3>
|
||||
<FieldEditor :model="current.field" :field-options="fieldOptions" />
|
||||
</div>
|
||||
|
||||
<!-- 复活 -->
|
||||
<div class="panel">
|
||||
<h3>复活 revive(与大招互斥)</h3>
|
||||
<ReviveEditor :model="current.revive" :revive-options="reviveOptions" />
|
||||
</div>
|
||||
|
||||
<!-- 额外加成 -->
|
||||
<div class="panel">
|
||||
<h3>额外属性加成</h3>
|
||||
<el-tabs>
|
||||
<el-tab-pane label="ap_bonus"><BonusEditor :model="current.ap_bonus" /></el-tab-pane>
|
||||
<el-tab-pane label="hp_bonus"><BonusEditor :model="current.hp_bonus" /></el-tab-pane>
|
||||
<el-tab-pane label="def_bonus"><BonusEditor :model="current.def_bonus" /></el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 描述 -->
|
||||
<div class="panel">
|
||||
<h3>英雄描述 info</h3>
|
||||
<el-input v-model="current.info" type="textarea" :rows="3" />
|
||||
</div>
|
||||
</section>
|
||||
<section v-else class="hero-editor"><div class="empty">请先选择或新增英雄</div></section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, h } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { api } from './api'
|
||||
import {
|
||||
HERO_ROLES, GROW_TYPES, H_TYPES,
|
||||
ROLE_ATK_SKILL, ROLE_ULT_SKILL, SUPPORT_ULT,
|
||||
defaultHero, defaultSkill,
|
||||
} from './constants'
|
||||
import SkillEditor from './components/SkillEditor.vue'
|
||||
import TriggerGroupEditor from './components/TriggerGroupEditor.vue'
|
||||
import FieldEditor from './components/FieldEditor.vue'
|
||||
import ReviveEditor from './components/ReviveEditor.vue'
|
||||
import BonusEditor from './components/BonusEditor.vue'
|
||||
|
||||
const heroes = ref([])
|
||||
const activeIdx = ref(-1)
|
||||
const meta = ref(null)
|
||||
const loadingMeta = ref(false)
|
||||
|
||||
const current = computed(() => heroes.value[activeIdx.value])
|
||||
|
||||
const skillMap = computed(() => meta.value?.SkillSet || {})
|
||||
const allSkills = computed(() => Object.values(skillMap.value).sort((a, b) => a.uuid - b.uuid))
|
||||
const fieldOptions = computed(() => Object.values(meta.value?.FieldSkillSet || {}).sort((a, b) => a.uuid - b.uuid))
|
||||
const reviveOptions = computed(() => allSkills.value.filter((s) => s.uuid >= 6500 && s.uuid < 6600))
|
||||
|
||||
const roleBase = computed(() => {
|
||||
if (!current.value || !meta.value?.HERO_ROLE_BASE) return null
|
||||
const roleEnum = HERO_ROLES.find((r) => r.key === current.value.roleKey)?.value
|
||||
return meta.value.HERO_ROLE_BASE[roleEnum]
|
||||
})
|
||||
|
||||
const basicSkill = computed(() => current.value?.skills?.[0])
|
||||
const ultSkill = computed(() => current.value?.skills?.[1])
|
||||
|
||||
const basicSkillOptions = computed(() => {
|
||||
if (!current.value) return []
|
||||
const allowed = ROLE_ATK_SKILL[current.value.roleKey] || []
|
||||
return allSkills.value.filter((s) => allowed.includes(s.uuid))
|
||||
})
|
||||
|
||||
const ultSkillOptions = computed(() => {
|
||||
if (!current.value) return []
|
||||
const allowed = [...(ROLE_ULT_SKILL[current.value.roleKey] || []), ...SUPPORT_ULT]
|
||||
return allSkills.value.filter((s) => allowed.includes(s.uuid))
|
||||
})
|
||||
|
||||
function roleLabel(key) { return HERO_ROLES.find((r) => r.key === key)?.label || key }
|
||||
function typeLabel(key) { return H_TYPES.find((t) => t.key === key)?.label || key }
|
||||
|
||||
async function loadMeta() {
|
||||
loadingMeta.value = true
|
||||
try {
|
||||
meta.value = await api.meta()
|
||||
} catch (e) {
|
||||
ElMessage.error('加载配置数据失败: ' + (e?.response?.data?.error || e.message))
|
||||
} finally {
|
||||
loadingMeta.value = false
|
||||
}
|
||||
}
|
||||
async function refreshMeta() {
|
||||
try {
|
||||
await api.refresh()
|
||||
await loadMeta()
|
||||
ElMessage.success('已刷新')
|
||||
} catch (e) {
|
||||
ElMessage.error('刷新失败')
|
||||
}
|
||||
}
|
||||
|
||||
function addHero() {
|
||||
const nh = defaultHero()
|
||||
const used = new Set(heroes.value.map((h) => h.uuid))
|
||||
let uuid = 5001
|
||||
while (used.has(uuid)) uuid++
|
||||
nh.uuid = uuid
|
||||
heroes.value.push(nh)
|
||||
activeIdx.value = heroes.value.length - 1
|
||||
}
|
||||
function removeHero(i) {
|
||||
heroes.value.splice(i, 1)
|
||||
if (activeIdx.value >= heroes.value.length) activeIdx.value = heroes.value.length - 1
|
||||
}
|
||||
|
||||
function addBasicSkill() {
|
||||
if (!current.value) return
|
||||
const first = basicSkillOptions.value[0]
|
||||
if (!first) return ElMessage.warning('当前职业无可用普攻底座')
|
||||
current.value.skills[0] = defaultSkill(first.uuid)
|
||||
}
|
||||
function addUltSkill() {
|
||||
if (!current.value) return
|
||||
const first = ultSkillOptions.value[0]
|
||||
if (!first) return ElMessage.warning('当前职业无可用大招底座')
|
||||
const sk = defaultSkill(first.uuid)
|
||||
// 大招默认 summon_count=1,并提供 lv9/lv16 档位
|
||||
sk.cd = 8
|
||||
sk.cdExpr = null
|
||||
sk.overrides = { summon_count: 1 }
|
||||
sk.lv_entries = [
|
||||
{ lv: 9, s_lv: 2, overrides: { summon_count: 1 }, desc: '' },
|
||||
{ lv: 16, s_lv: 3, overrides: { summon_count: 2 }, desc: '' },
|
||||
]
|
||||
current.value.skills[1] = sk
|
||||
}
|
||||
|
||||
async function doExport() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`将写入 ${heroes.value.length} 个英雄到 assets/script/game/common/config/hexport.ts`,
|
||||
'导出确认',
|
||||
{ confirmButtonText: '写入', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
} catch { return }
|
||||
try {
|
||||
const r = await api.exportHeroes(heroes.value)
|
||||
ElMessage.success(`已写入 ${r.bytes} 字节 → ${r.path}`)
|
||||
} catch (e) {
|
||||
ElMessage.error('导出失败: ' + (e?.response?.data?.error || e.message))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadMeta)
|
||||
</script>
|
||||
Reference in New Issue
Block a user