feat(editor): 完成英雄配置编辑器全功能开发
本提交新增完整的英雄配置可视化编辑器: 1. 初始化前后端项目结构,配置monorepo工作区 2. 实现后端API服务,支持读取游戏配置、导出hexport.ts文件 3. 开发前端Vue应用,包含完整的英雄增删改、属性配置、技能编辑功能 4. 实现多类型编辑器组件:基础属性、普攻/大招编辑、触发技能、光环、复活、属性加成等 5. 清理冗余的配置元数据文件
This commit is contained in:
14
put/web/src/components/BonusEditor.vue
Normal file
14
put/web/src/components/BonusEditor.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="(b, idx) in model" :key="idx" style="display: flex; gap: 8px; margin-bottom: 6px">
|
||||
<el-input-number v-model="b.lv" :min="1" :max="20" :controls="false" style="width: 100px" placeholder="lv" />
|
||||
<el-input-number v-model="b.value" :controls="false" style="width: 140px" placeholder="value" />
|
||||
<el-button type="danger" size="small" text @click="model.splice(idx, 1)">删除</el-button>
|
||||
</div>
|
||||
<el-button size="small" @click="model.push({ lv: 12, value: 0 })">+ 添加</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({ model: { type: Array, required: true } })
|
||||
</script>
|
||||
43
put/web/src/components/FieldEditor.vue
Normal file
43
put/web/src/components/FieldEditor.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="(f, idx) in model" :key="idx" class="skill-block">
|
||||
<div class="head">
|
||||
<span>光环档位 lv {{ f.lv }}</span>
|
||||
<el-button type="danger" size="small" text @click="model.splice(idx, 1)">删除档位</el-button>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="等级 lv">
|
||||
<el-input-number v-model="f.lv" :min="1" :max="20" :controls="false" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" class="grid-span">
|
||||
<el-input v-model="f.desc" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="光环词条">
|
||||
<div style="width: 100%">
|
||||
<div v-for="(u, i) in f.uuids" :key="i" style="display: flex; gap: 8px; margin-bottom: 6px">
|
||||
<el-select v-model="u.uuid" placeholder="光环" filterable style="width: 220px">
|
||||
<el-option v-for="fs in fieldOptions" :key="fs.uuid" :label="`${fs.uuid} ${fs.name}`" :value="fs.uuid" />
|
||||
</el-select>
|
||||
<el-input-number v-model="u.value" :step="0.01" :controls="false" style="width: 120px" placeholder="value" />
|
||||
<el-input v-model="u.name" placeholder="英雄语境名(可选)" style="width: 200px" />
|
||||
<el-button type="danger" size="small" text @click="f.uuids.splice(i, 1)">删除</el-button>
|
||||
</div>
|
||||
<el-button size="small" @click="f.uuids.push({ uuid: null, value: 0, name: '' })">+ 添加词条</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-button size="small" type="primary" @click="model.push({ lv: 6, uuids: [], desc: '' })">+ 添加光环档位</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
model: { type: Array, required: true },
|
||||
fieldOptions: { type: Array, default: () => [] },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.grid-span { grid-column: 1 / -1; }
|
||||
</style>
|
||||
56
put/web/src/components/OverrideEditor.vue
Normal file
56
put/web/src/components/OverrideEditor.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="override-editor">
|
||||
<div v-for="(item, idx) in rows" :key="idx" class="override-row">
|
||||
<el-select v-model="item.key" placeholder="字段" size="small" style="width: 200px" filterable>
|
||||
<el-option v-for="k in availableKeys(idx)" :key="k.key" :label="k.label" :value="k.key" />
|
||||
</el-select>
|
||||
<el-input-number v-model="item.value" size="small" :controls="false" style="width: 120px" />
|
||||
<el-button type="danger" size="small" text @click="remove(idx)">删除</el-button>
|
||||
</div>
|
||||
<el-button size="small" @click="add">+ 添加覆盖</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { OVERRIDE_KEYS } from '../constants'
|
||||
|
||||
const props = defineProps({ modelValue: { type: Object, default: () => ({}) } })
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
// 本地行数组,允许 key 为空
|
||||
const rows = ref(Object.entries(props.modelValue || {}).map(([key, value]) => ({ key, value })))
|
||||
|
||||
// 外部 modelValue 变化时同步(如切换英雄)
|
||||
watch(() => props.modelValue, (v) => {
|
||||
const next = Object.entries(v || {}).map(([key, value]) => ({ key, value }))
|
||||
// 简单比较,避免输入时循环
|
||||
if (JSON.stringify(next) !== JSON.stringify(rows.value.filter(r => r.key))) {
|
||||
rows.value = next
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// 每次行变化都尝试同步(过滤空 key)
|
||||
watch(rows, (arr) => {
|
||||
const obj = {}
|
||||
for (const it of arr) if (it.key) obj[it.key] = it.value
|
||||
emit('update:modelValue', obj)
|
||||
}, { deep: true })
|
||||
|
||||
// 已选中的 key 不再出现在其他行的下拉中
|
||||
function availableKeys(idx) {
|
||||
const used = new Set(rows.value.map((r, i) => (i === idx ? null : r.key)).filter(Boolean))
|
||||
return OVERRIDE_KEYS.filter((k) => !used.has(k.key))
|
||||
}
|
||||
|
||||
function add() {
|
||||
rows.value.push({ key: '', value: 0 })
|
||||
}
|
||||
function remove(idx) {
|
||||
rows.value.splice(idx, 1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.override-row { display: flex; gap: 8px; margin-bottom: 6px; align-items: center; }
|
||||
</style>
|
||||
47
put/web/src/components/ReviveEditor.vue
Normal file
47
put/web/src/components/ReviveEditor.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-alert type="warning" :closable="false" show-icon style="margin-bottom: 10px">
|
||||
revive 与 skills[1](大招)互斥;配置 revive 时按 lv1/lv9/lv16 三档成长。
|
||||
</el-alert>
|
||||
<div v-for="(r, idx) in model" :key="idx" class="skill-block">
|
||||
<div class="head">
|
||||
<span>复活档位 lv {{ r.lv }}</span>
|
||||
<el-button type="danger" size="small" text @click="model.splice(idx, 1)">删除档位</el-button>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="等级 lv">
|
||||
<el-input-number v-model="r.lv" :min="1" :max="20" :controls="false" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="复活技能 s_uuid">
|
||||
<el-select v-model="r.s_uuid" filterable>
|
||||
<el-option v-for="s in reviveOptions" :key="s.uuid" :label="`${s.uuid} ${s.name}`" :value="s.uuid" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="r_num 次数">
|
||||
<el-input-number v-model="r.r_num" :min="1" :controls="false" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="upr 复活血量百分比">
|
||||
<el-input-number v-model="r.upr" :min="0" :max="100" :controls="false" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="名称(仅第1档)" v-if="idx === 0">
|
||||
<el-input v-model="r.name" placeholder="默认:凤凰涅槃" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" class="grid-span">
|
||||
<el-input v-model="r.desc" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<el-button size="small" type="primary" @click="model.push({ lv: 1, s_uuid: 6501, r_num: 1, upr: 30, name: '', desc: '' })">+ 添加复活档位</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
model: { type: Array, required: true },
|
||||
reviveOptions: { type: Array, default: () => [] },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.grid-span { grid-column: 1 / -1; }
|
||||
</style>
|
||||
107
put/web/src/components/SkillEditor.vue
Normal file
107
put/web/src/components/SkillEditor.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="技能底座">
|
||||
<el-select v-model="skill.uuid" placeholder="选择技能" filterable @change="onUuidChange">
|
||||
<el-option v-for="s in options" :key="s.uuid" :label="`${s.uuid} ${s.name}`" :value="s.uuid">
|
||||
<span>{{ s.uuid }} {{ s.name }}</span>
|
||||
<span class="muted" style="float: right">{{ s.info }}</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="显示名">
|
||||
<el-input v-model="skill.name" placeholder="对外展示名(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="攻速档位">
|
||||
<el-select v-model="cdSpeedKey" @change="onSpeedChange">
|
||||
<el-option v-for="s in ATK_SPEEDS" :key="s.key" :label="s.label" :value="s.key" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="CD(秒)">
|
||||
<el-input-number v-model="skill.cd" :step="0.05" :min="0" :controls="false" style="width: 100%" @change="onCdManual" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述模板" class="grid-span">
|
||||
<el-input v-model="skill.desc" type="textarea" :rows="2" placeholder="支持 {target}/{ap}/{para} 等占位符" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="overrides">
|
||||
<OverrideEditor v-model="skill.overrides" />
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="isUlt">
|
||||
<el-divider content-position="left">大招 lv_entries 分档</el-divider>
|
||||
<div v-for="(e, idx) in skill.lv_entries" :key="idx" class="skill-block">
|
||||
<div class="head">
|
||||
<span>档位 lv {{ e.lv }} (SkillLv.Lv{{ e.s_lv }})</span>
|
||||
<el-button type="danger" size="small" text @click="skill.lv_entries.splice(idx, 1)">移除</el-button>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="等级 lv">
|
||||
<el-input-number v-model="e.lv" :min="1" :max="20" :controls="false" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="SkillLv">
|
||||
<el-select v-model="e.s_lv">
|
||||
<el-option v-for="n in 5" :key="n" :label="`Lv${n}`" :value="n" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="overrides">
|
||||
<OverrideEditor v-model="e.overrides" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="e.desc" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-button size="small" @click="addEntry">+ 添加档位(lv9/lv16)</el-button>
|
||||
<span class="muted" style="margin-left: 8px">建议按 lv9 / lv16 两档</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { ATK_SPEEDS } from '../constants'
|
||||
import OverrideEditor from './OverrideEditor.vue'
|
||||
|
||||
const props = defineProps({
|
||||
skill: { type: Object, required: true },
|
||||
options: { type: Array, default: () => [] },
|
||||
isUlt: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
const cdSpeedKey = refFromCd(props.skill)
|
||||
|
||||
function refFromCd(skill) {
|
||||
// 简单映射回攻速档
|
||||
return computed({
|
||||
get() {
|
||||
const found = ATK_SPEEDS.find((s) => Math.abs(cdOf(s.key) - skill.cd) < 0.001)
|
||||
return found?.key || 'Normal'
|
||||
},
|
||||
set() {},
|
||||
})
|
||||
}
|
||||
|
||||
function cdOf(key) {
|
||||
const map = { VeryFast: 0.75, Fast: 0.9, Normal: 1.05, Slow: 1.2, VerySlow: 1.5 }
|
||||
return map[key] ?? 1.05
|
||||
}
|
||||
|
||||
function onSpeedChange(key) {
|
||||
props.skill.cd = cdOf(key)
|
||||
props.skill.cdExpr = `AtkSpeedSet[AtkSpeedLv.${key}].cd`
|
||||
}
|
||||
function onCdManual() {
|
||||
props.skill.cdExpr = null
|
||||
}
|
||||
function onUuidChange() {
|
||||
// 选择底座时清空名字(回退默认)
|
||||
}
|
||||
function addEntry() {
|
||||
props.skill.lv_entries.push({ lv: 9, s_lv: 2, overrides: {}, desc: '' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.grid-span { grid-column: 1 / -1; }
|
||||
</style>
|
||||
72
put/web/src/components/TriggerGroupEditor.vue
Normal file
72
put/web/src/components/TriggerGroupEditor.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="(group, sid) in model" :key="sid" class="skill-block">
|
||||
<div class="head">
|
||||
<span>技能 {{ sid }} — {{ skillName(sid) }}</span>
|
||||
<el-button type="danger" size="small" text @click="removeGroup(sid)">删除组</el-button>
|
||||
</div>
|
||||
<div v-for="(entry, idx) in group" :key="idx" style="border-top: 1px dashed #eee; padding-top: 8px; margin-top: 8px">
|
||||
<div class="form-grid">
|
||||
<el-form-item label="等级 lv">
|
||||
<el-input-number v-model="entry.lv" :min="1" :max="20" :controls="false" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="SkillLv">
|
||||
<el-select v-model="entry.s_lv">
|
||||
<el-option v-for="n in 5" :key="n" :label="`Lv${n}`" :value="n" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="t_num (1=每次,3≈33%,4≈25%)">
|
||||
<el-input-number v-model="entry.t_num" :min="1" :controls="false" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="组名(仅第1档)" v-if="idx === 0">
|
||||
<el-input v-model="entry.name" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="overrides">
|
||||
<OverrideEditor v-model="entry.overrides" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="entry.desc" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
<el-button type="danger" size="small" text @click="group.splice(idx, 1)">删除档位</el-button>
|
||||
</div>
|
||||
<el-button size="small" @click="addEntry(group)" style="margin-top: 6px">+ 添加档位</el-button>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; align-items: center">
|
||||
<el-select v-model="newSid" placeholder="选择触发技能" filterable style="width: 240px">
|
||||
<el-option v-for="s in skillOptions" :key="s.uuid" :label="`${s.uuid} ${s.name}`" :value="s.uuid" />
|
||||
</el-select>
|
||||
<el-button size="small" type="primary" @click="addGroup">+ 新增触发组</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import OverrideEditor from './OverrideEditor.vue'
|
||||
import { defaultTriggerEntry } from '../constants'
|
||||
|
||||
const props = defineProps({
|
||||
model: { type: Object, required: true }, // TriggerGrouped
|
||||
skillOptions: { type: Array, default: () => [] },
|
||||
skillMap: { type: Object, default: () => ({}) },
|
||||
})
|
||||
const newSid = ref(null)
|
||||
|
||||
function skillName(sid) {
|
||||
return props.skillMap[sid]?.name || ''
|
||||
}
|
||||
function addGroup() {
|
||||
if (!newSid.value) return
|
||||
if (!props.model[newSid.value]) props.model[newSid.value] = []
|
||||
props.model[newSid.value].push(defaultTriggerEntry(6))
|
||||
newSid.value = null
|
||||
}
|
||||
function removeGroup(sid) {
|
||||
delete props.model[sid]
|
||||
}
|
||||
function addEntry(group) {
|
||||
const lastLv = group.length ? group[group.length - 1].lv : 6
|
||||
group.push({ lv: lastLv + 3, s_lv: 2, name: '', t_num: 3, overrides: {}, desc: '' })
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user