1. 调整proc_skill触发时机为施法出手时判定,一次施法仅判定一次,与命中/弹道数无关 2. 移除HeroAtkSystem中旧的proc_skill触发逻辑,新增SCastSystem的tryProcSkill方法 3. 更新技能配置注释、编辑器文案与文档说明,同步调整服务端技能描述显示 4. 全局设置Element Plus组件为small尺寸,优化编辑器UI布局 5. 清理冗余的proc_skill快照传递逻辑,移除unused的reviveOptions计算属性
497 lines
20 KiB
Vue
497 lines
20 KiB
Vue
<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="success" @click="doSave" :disabled="!heroes.length">保存到服务器</el-button>
|
||
<el-button size="small" type="primary" @click="doApply" :disabled="!heroes.length">写入 heroSet.ts</el-button>
|
||
<el-button size="small" type="danger" @click="doExportCsv" :disabled="!heroes.length">导出英雄设计表.csv</el-button>
|
||
</div>
|
||
</header>
|
||
<div class="hero-main">
|
||
<!-- 首页:英雄卡片列表 -->
|
||
<div class="home-toolbar">
|
||
<span class="muted">共 {{ heroes.length }} 个英雄</span>
|
||
<div class="toolbar-sort">
|
||
<el-radio-group v-model="sortKey" size="small">
|
||
<el-radio-button value="none">默认</el-radio-button>
|
||
<el-radio-button value="role">按职业</el-radio-button>
|
||
<el-radio-button value="rarity">按星级</el-radio-button>
|
||
</el-radio-group>
|
||
<el-radio-group v-if="sortKey !== 'none'" v-model="sortOrder" size="small">
|
||
<el-radio-button value="asc">升序</el-radio-button>
|
||
<el-radio-button value="desc">降序</el-radio-button>
|
||
</el-radio-group>
|
||
</div>
|
||
<el-button size="small" type="primary" @click="addHero">+ 新建英雄</el-button>
|
||
</div>
|
||
<section class="hero-rows">
|
||
<div v-if="!heroes.length" class="empty">暂无英雄,点击右上角「新建英雄」开始配置</div>
|
||
<div v-for="{ h, i } in sortedHeroes" :key="h.uuid" class="hero-row" @click="openEdit(i)">
|
||
<div class="row-main">
|
||
<span class="row-uuid">{{ h.uuid }}</span>
|
||
<span class="row-name">{{ h.name || '(未命名)' }}</span>
|
||
<div class="row-sub">
|
||
<span class="row-role" :class="`role-${h.roleKey?.toLowerCase()}`">{{ roleLabel(h.roleKey) }}</span>
|
||
<span class="card-rarity">{{ '★'.repeat(h.rarity) }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="row-meta">
|
||
<template v-if="heroStats(h)">
|
||
<span class="stat">HP {{ heroStats(h).hp }}</span>
|
||
<span class="stat">AP {{ heroStats(h).ap }}</span>
|
||
<span class="stat">DEF {{ heroStats(h).def }}</span>
|
||
</template>
|
||
</div>
|
||
<div class="row-skills">
|
||
<div class="row-skill">
|
||
<span class="tag">普攻</span>{{ skillLabel(h.skills?.[0]) }}
|
||
<span class="skill-detail">{{ skillDetailFull(h.skills?.[0]) }}</span>
|
||
</div>
|
||
<div class="row-skill">
|
||
<span class="tag">大招</span>{{ skillLabel(h.skills?.[1]) }}
|
||
<span class="skill-detail">{{ skillDetailFull(h.skills?.[1]) }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="row-triggers" v-if="hasTriggers(h) || fieldSummary(h).length">
|
||
<span v-if="fieldSummary(h).length" class="trigger-chip aura">光环: {{ fieldSummary(h).join(';') }}</span>
|
||
<span v-for="line in triggerSummary(h)" :key="line" class="trigger-chip">{{ line }}</span>
|
||
<span v-if="bonusCount(h)" class="trigger-chip">加成: ×{{ bonusCount(h) }}</span>
|
||
</div>
|
||
<!-- 英雄简介:整行展示,超长省略,悬停查看全文 -->
|
||
<div class="row-info" v-if="h.info" :title="h.info">{{ h.info }}</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 编辑弹窗 -->
|
||
<el-dialog
|
||
v-model="editVisible"
|
||
:title="`编辑英雄 ${current?.uuid || ''} ${current?.name || ''}`"
|
||
width="94%"
|
||
top="3vh"
|
||
class="editor-dialog"
|
||
:close-on-click-modal="false"
|
||
>
|
||
<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="闪避 dodge(可选)">
|
||
<el-input-number v-model="current.dodge" :min="0" :max="100" :controls="false" style="width: 100%" placeholder="默认 15" />
|
||
</el-form-item>
|
||
<el-form-item label="命中 accuracy(可选)">
|
||
<el-input-number v-model="current.accuracy" :min="0" :max="100" :controls="false" style="width: 100%" placeholder="默认 10" />
|
||
</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" :skill-options="allSkills" :buff-options="buffOptions" :basic-lv2="true" />
|
||
<el-button v-else size="small" type="primary" @click="addBasicSkill">+ 添加普攻</el-button>
|
||
</div>
|
||
|
||
<!-- 大招 -->
|
||
<div class="panel">
|
||
<h3>大招 skills[1]</h3>
|
||
<SkillEditor v-if="ultSkill" :skill="ultSkill" :options="ultSkillOptions" :skill-options="allSkills" :buff-options="buffOptions" :is-ult="true" />
|
||
<el-button v-else size="small" type="primary" @click="addUltSkill">+ 添加大招</el-button>
|
||
</div>
|
||
|
||
<!-- 触发技能 -->
|
||
<div class="panel">
|
||
<h3>触发技能(lv4 激活)</h3>
|
||
<el-tabs>
|
||
<el-tab-pane label="atking 攻击后"><TriggerGroupEditor :model="current.atking" :skill-options="allSkills" :skill-map="skillMap" :buff-options="buffOptions" /></el-tab-pane>
|
||
<el-tab-pane label="atked 受击后"><TriggerGroupEditor :model="current.atked" :skill-options="allSkills" :skill-map="skillMap" :buff-options="buffOptions" /></el-tab-pane>
|
||
<el-tab-pane label="fstart 战斗开始"><TriggerGroupEditor :model="current.fstart" :skill-options="allSkills" :skill-map="skillMap" :buff-options="buffOptions" /></el-tab-pane>
|
||
<el-tab-pane label="dead 死亡后"><TriggerGroupEditor :model="current.dead" :skill-options="allSkills" :skill-map="skillMap" :buff-options="buffOptions" /></el-tab-pane>
|
||
</el-tabs>
|
||
</div>
|
||
|
||
<!-- 光环 -->
|
||
<div class="panel">
|
||
<h3>驻场光环 field</h3>
|
||
<FieldEditor :model="current.field" :field-options="fieldOptions" />
|
||
</div>
|
||
|
||
<!-- 复活已纳入 dead 触发组,通过 dead 编辑器的 overrides 配置 revive_hp / revive_count -->
|
||
<div class="panel">
|
||
<h3>复活配置(dead 触发组内)</h3>
|
||
<el-alert type="info" :closable="false" show-icon style="margin-bottom: 10px">
|
||
复活已纳入 dead 触发组:在 dead 中添加技能(如 6501),并在 overrides 中配置 revive_hp(回血比例%)和 revive_count(次数上限)。
|
||
</el-alert>
|
||
</div>
|
||
|
||
<!-- 额外加成 -->
|
||
<div class="panel">
|
||
<h3>额外属性加成(hp/ap/def 百分比,crt/frz/stun/para 百分点)</h3>
|
||
<BonusEditor :model="current.bonus" />
|
||
</div>
|
||
|
||
<!-- 描述 -->
|
||
<div class="panel">
|
||
<h3>英雄描述 info</h3>
|
||
<el-input v-model="current.info" type="textarea" :rows="3" />
|
||
</div>
|
||
</section>
|
||
<template #footer>
|
||
<el-button size="small" type="danger" plain :disabled="activeIdx < 0" @click="removeHero(activeIdx)">删除当前英雄</el-button>
|
||
<el-button size="small" @click="editVisible = false">关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, watch, onMounted } 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 BonusEditor from './components/BonusEditor.vue'
|
||
|
||
const heroes = ref([])
|
||
const activeIdx = ref(-1)
|
||
const meta = ref(null)
|
||
const loadingMeta = ref(false)
|
||
// 编辑弹窗可见性:首页卡片点「修改/新建」打开,关闭仅收起不丢数据
|
||
const editVisible = ref(false)
|
||
|
||
const current = computed(() => heroes.value[activeIdx.value])
|
||
|
||
// 首页列表排序:none 原始顺序 / role 按职业枚举序(坦克→辅助)/ rarity 按星级
|
||
const sortKey = ref('none')
|
||
const sortOrder = ref('desc')
|
||
// 切换排序维度时重置方向:职业升序直观(坦克在前),星级降序直观(高星在前)
|
||
watch(sortKey, (k) => { sortOrder.value = k === 'role' ? 'asc' : 'desc' })
|
||
|
||
// 排序视图:携带原始下标,保证点击卡片仍能定位到 heroes 中的编辑对象,且不改写保存顺序
|
||
const sortedHeroes = computed(() => {
|
||
const list = heroes.value.map((h, i) => ({ h, i }))
|
||
if (sortKey.value === 'none') return list
|
||
const dir = sortOrder.value === 'asc' ? 1 : -1
|
||
const roleIdx = (key) => HERO_ROLES.findIndex((r) => r.key === key)
|
||
list.sort((a, b) => {
|
||
const d = sortKey.value === 'role'
|
||
? roleIdx(a.h.roleKey) - roleIdx(b.h.roleKey)
|
||
: (a.h.rarity || 0) - (b.h.rarity || 0)
|
||
// 同维度内按 uuid 升序兜底,保证排序稳定
|
||
return d !== 0 ? d * dir : a.h.uuid - b.h.uuid
|
||
})
|
||
return list
|
||
})
|
||
|
||
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))
|
||
// buff 候选:timed_buff_id 覆写下拉用(BuffList 按 id 升序)
|
||
const buffOptions = computed(() => Object.values(meta.value?.BuffList || {}).sort((a, b) => a.id - b.id))
|
||
|
||
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 }
|
||
|
||
// 卡片技能摘要:优先引用处命名,其次回退 SkillSet 底座名
|
||
function skillLabel(sk) {
|
||
if (!sk) return '-'
|
||
return `${sk.uuid} ${sk.name || skillMap.value[sk.uuid]?.name || ''}`.trim()
|
||
}
|
||
// 覆写键 → 短标签映射,未列出的键直接用原始键名兜底,保证任何参数都能显示
|
||
const OV_LABELS = {
|
||
hit_count: 'hit',
|
||
timed_buff_id: 'buff',
|
||
buff_value: 'val',
|
||
buff_chance: 'chance',
|
||
timed_debuff_id: 'debuff',
|
||
debuff_value: 'dval',
|
||
debuff_chance: 'dchance',
|
||
summon_count: '数',
|
||
thorns_dmg: 'thorns',
|
||
thorns_rate: 'thorns',
|
||
}
|
||
|
||
// 覆写对象转简写(如 ap100、crt10、proc:6001×100%、bck_chance20)
|
||
function overrideParts(ov) {
|
||
if (!ov) return []
|
||
const parts = []
|
||
for (const [k, v] of Object.entries(ov)) {
|
||
// 跳过空值与编辑器内部标记键
|
||
if (v == null || k === 'TGroup' || k === 'desc') continue
|
||
if (k === 'proc_skill') {
|
||
const sub = overrideParts(v.overrides).join(',')
|
||
parts.push(`proc:${v.s_uuid}×${v.chance}%${sub ? `(${sub})` : ''}`)
|
||
continue
|
||
}
|
||
if (typeof v === 'object') {
|
||
parts.push(`${k}${JSON.stringify(v)}`)
|
||
continue
|
||
}
|
||
const label = OV_LABELS[k] ?? k
|
||
parts.push(k === 'thorns_rate' ? `${label}${v}%` : `${label}${v}`)
|
||
}
|
||
return parts
|
||
}
|
||
|
||
// 技能完整明细:lv1 + 各 lv_entry 档位参数
|
||
function skillDetailFull(sk) {
|
||
if (!sk) return ''
|
||
const parts = []
|
||
const lv1 = overrideParts(sk.overrides).join(' ')
|
||
if (lv1) parts.push(lv1)
|
||
for (const e of sk.lv_entries || []) {
|
||
const ep = overrideParts(e.overrides).join(' ')
|
||
parts.push(`lv${e.lv}:${ep || '-'}`)
|
||
}
|
||
return parts.join(' | ')
|
||
}
|
||
// lv1 基础三维:HERO_ROLE_BASE + 编辑器偏移(meta 未加载时返回 null 不展示)
|
||
function heroStats(h) {
|
||
const roleEnum = HERO_ROLES.find((r) => r.key === h.roleKey)?.value
|
||
const base = meta.value?.HERO_ROLE_BASE?.[roleEnum]
|
||
if (!base) return null
|
||
return {
|
||
hp: base.hp + (h.statBase?.hp || 0),
|
||
ap: base.ap + (h.statBase?.ap || 0),
|
||
def: base.def + (h.statBase?.def || 0),
|
||
}
|
||
}
|
||
// 触发明细行:atking: 6004 奋力一击 lv4 ap300 格式,仅输出已启用的组
|
||
function triggerSummary(h) {
|
||
const lines = []
|
||
for (const tk of ['atking', 'atked', 'fstart', 'dead']) {
|
||
const grp = h[tk]
|
||
if (!grp) continue
|
||
for (const [sid, entries] of Object.entries(grp)) {
|
||
const minLv = Math.min(...entries.map((e) => e.lv))
|
||
const name = entries[0]?.name || skillMap.value[sid]?.name || ''
|
||
const e = entries[0]
|
||
const ov = e?.overrides || {}
|
||
const parts = [`t_num${e?.t_num}`, ...overrideParts(ov)]
|
||
lines.push(`${tk}: ${sid} ${name} lv${minLv} ${parts.join(' ')}`)
|
||
}
|
||
}
|
||
return lines
|
||
}
|
||
function hasTriggers(h) {
|
||
return ['atking', 'atked', 'fstart', 'dead'].some((k) => h[k] && Object.keys(h[k]).length)
|
||
}
|
||
// 光环明细行:lv4 · 大圣神威 0.25
|
||
function fieldSummary(h) {
|
||
if (!h.field?.length) return []
|
||
const fieldMap = meta.value?.FieldSkillSet || {}
|
||
return h.field.map((f) => {
|
||
const auras = f.uuids.map((u) => `${u.name || fieldMap[u.uuid]?.name || u.uuid} ${u.value}`).join('、')
|
||
return `lv${f.lv} · ${auras}`
|
||
})
|
||
}
|
||
// 额外加成档位总数(bonus 数组长度)
|
||
function bonusCount(h) {
|
||
return h.bonus?.length || 0
|
||
}
|
||
|
||
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 openEdit(i) {
|
||
activeIdx.value = i
|
||
editVisible.value = true
|
||
}
|
||
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)
|
||
openEdit(heroes.value.length - 1)
|
||
}
|
||
function removeHero(i) {
|
||
heroes.value.splice(i, 1)
|
||
if (activeIdx.value >= heroes.value.length) activeIdx.value = heroes.value.length - 1
|
||
// 删除后关闭弹窗回到首页
|
||
editVisible.value = false
|
||
}
|
||
|
||
function addBasicSkill() {
|
||
if (!current.value) return
|
||
const first = basicSkillOptions.value[0]
|
||
if (!first) return ElMessage.warning('当前职业无可用普攻底座')
|
||
const sk = defaultSkill(first.uuid)
|
||
// 普攻默认提供 lv2 强化档位
|
||
sk.lv_entries = [
|
||
{ lv: 2, s_lv: 2, overrides: { ap: 120 }, desc: '' },
|
||
]
|
||
current.value.skills[0] = sk
|
||
}
|
||
function addUltSkill() {
|
||
if (!current.value) return
|
||
const first = ultSkillOptions.value[0]
|
||
if (!first) return ElMessage.warning('当前职业无可用大招底座')
|
||
const sk = defaultSkill(first.uuid)
|
||
// 大招提供 lv8(SkillLv.Lv2)/lv16(SkillLv.Lv3) 档位,不默认注入 summon_count
|
||
sk.cd = 8
|
||
sk.cdExpr = null
|
||
sk.overrides = {}
|
||
sk.lv_entries = [
|
||
{ lv: 8, s_lv: 2, overrides: {}, desc: '' },
|
||
{ lv: 16, s_lv: 3, overrides: {}, desc: '' },
|
||
]
|
||
current.value.skills[1] = sk
|
||
}
|
||
|
||
async function loadHeroes() {
|
||
try {
|
||
const r = await api.fetchHeroes()
|
||
if (Array.isArray(r.heroes) && r.heroes.length) {
|
||
// 兼容旧数据:ap_bonus/hp_bonus/def_bonus 已废弃,统一迁移为空 bonus
|
||
for (const h of r.heroes) {
|
||
if (!Array.isArray(h.bonus)) h.bonus = []
|
||
delete h.ap_bonus; delete h.hp_bonus; delete h.def_bonus
|
||
}
|
||
heroes.value = r.heroes
|
||
}
|
||
} catch (e) {
|
||
ElMessage.error('加载英雄数据失败: ' + (e?.response?.data?.error || e.message))
|
||
}
|
||
}
|
||
|
||
async function doSave() {
|
||
try {
|
||
const r = await api.saveHeroes(heroes.value)
|
||
ElMessage.success(`已保存 ${r.count} 个英雄 → ${r.path}`)
|
||
} catch (e) {
|
||
ElMessage.error('保存失败: ' + (e?.response?.data?.error || e.message))
|
||
}
|
||
}
|
||
|
||
async function doApply() {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`将用 ${heroes.value.length} 个英雄直接覆盖 heroSet.ts 的英雄配置区域(@hero-setup:heroes-begin/end 标记之间),怪物配置不受影响。`,
|
||
'写入确认',
|
||
{ confirmButtonText: '覆盖写入', cancelButtonText: '取消', type: 'warning' }
|
||
)
|
||
} catch { return }
|
||
try {
|
||
const r = await api.applyHeroes(heroes.value)
|
||
ElMessage.success(`已写入 ${r.count} 个英雄 → ${r.path}`)
|
||
} catch (e) {
|
||
ElMessage.error('写入失败: ' + (e?.response?.data?.error || e.message))
|
||
}
|
||
}
|
||
|
||
async function doExportCsv() {
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`将用 ${heroes.value.length} 个英雄覆盖写入 assets/script/game/common/config/英雄设计表.csv`,
|
||
'导出确认',
|
||
{ confirmButtonText: '覆盖写入', cancelButtonText: '取消', type: 'warning' }
|
||
)
|
||
} catch { return }
|
||
try {
|
||
const r = await api.exportCsv(heroes.value)
|
||
ElMessage.success(`已写入 ${r.count} 个英雄 → ${r.path}`)
|
||
} catch (e) {
|
||
ElMessage.error('导出失败: ' + (e?.response?.data?.error || e.message))
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadMeta()
|
||
loadHeroes()
|
||
})
|
||
</script>
|