1. 新增触发组条目字段t_chance(触发概率)和pity_add(保底叠加),仅攻击/受击组生效 2. 为TriggerGroupEditor传入分组标识,动态显示概率配置表单项 3. 新增条目创建时继承同组末档概率参数的逻辑 4. 更新常量定义与文档说明,补充概率参数使用规则 5. 在前端展示和服务端导出中添加概率相关信息展示
422 lines
16 KiB
Vue
422 lines
16 KiB
Vue
<template>
|
||
<div class="tpl-manager">
|
||
<div class="tpl-toolbar">
|
||
<div class="tpl-toolbar-left">
|
||
<span class="tpl-title">预设库</span>
|
||
<span class="muted">触发/光环预设是跨职业复用的资产,新英雄向导中勾选即可套用</span>
|
||
</div>
|
||
<div class="tpl-toolbar-actions">
|
||
<el-button size="small" type="success" @click="doSave">保存预设</el-button>
|
||
<el-button size="small" type="primary" @click="addPreset">+ 新建{{ activeTab === 'trigger' ? '触发' : '光环' }}预设</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-tabs v-model="activeTab" class="tpl-tabs">
|
||
<!-- 触发预设 -->
|
||
<el-tab-pane label="触发预设" name="trigger">
|
||
<div class="preset-sub-tabs">
|
||
<el-radio-group v-model="triggerGroup" size="small">
|
||
<el-radio-button v-for="g in TRIGGER_GROUPS" :key="g.key" :value="g.key">{{ g.label }}</el-radio-button>
|
||
</el-radio-group>
|
||
<span class="muted">共 {{ filteredTriggerPresets.length }} 个</span>
|
||
</div>
|
||
<div class="tpl-scroll">
|
||
<div class="tpl-cards">
|
||
<div v-for="p in filteredTriggerPresets" :key="p.id" class="tpl-card" @click="openEditTrigger(p)">
|
||
<div class="tpl-card-head">
|
||
<span class="tpl-name">{{ p.label || '(未命名)' }}</span>
|
||
<el-button type="danger" size="small" text @click.stop="removeTrigger(p)">删除</el-button>
|
||
</div>
|
||
<div class="tpl-meta">
|
||
<span class="tag">{{ p.group }}</span>
|
||
<span class="tpl-meta-item">{{ p.uuid }} {{ skillName(p.uuid) }}</span>
|
||
<span class="tpl-meta-sep">·</span>
|
||
<span class="tpl-meta-item">t_num {{ p.entry.t_num }}</span>
|
||
<span class="tpl-meta-sep" v-if="p.entry.t_chance != null && p.entry.t_chance < 100">·</span>
|
||
<span class="tpl-meta-item" v-if="p.entry.t_chance != null && p.entry.t_chance < 100">概率 {{ p.entry.t_chance }}%</span>
|
||
<span class="tpl-meta-sep">·</span>
|
||
<span class="tpl-meta-item">lv{{ p.entry.lv }}</span>
|
||
</div>
|
||
<div class="tpl-chips" v-if="overrideSummary(p.entry.overrides)">
|
||
<span class="chip">{{ overrideSummary(p.entry.overrides) }}</span>
|
||
</div>
|
||
<div class="tpl-chips" v-if="p.tags?.length">
|
||
<span v-for="t in p.tags" :key="t" class="chip tag-chip">{{ t }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="!filteredTriggerPresets.length" class="tpl-empty-role">
|
||
<span class="muted">该组暂无预设</span>
|
||
<el-button size="small" text type="primary" @click="addTriggerFor(triggerGroup)">+ 补一个</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<!-- 光环预设 -->
|
||
<el-tab-pane label="光环预设" name="field">
|
||
<div class="preset-sub-tabs">
|
||
<span class="muted">共 {{ fieldPresets.length }} 个</span>
|
||
</div>
|
||
<div class="tpl-scroll">
|
||
<div class="tpl-cards">
|
||
<div v-for="p in fieldPresets" :key="p.id" class="tpl-card" @click="openEditField(p)">
|
||
<div class="tpl-card-head">
|
||
<span class="tpl-name">{{ p.label || '(未命名)' }}</span>
|
||
<el-button type="danger" size="small" text @click.stop="removeField(p)">删除</el-button>
|
||
</div>
|
||
<div class="tpl-meta">
|
||
<span class="tag">lv{{ p.lv }}</span>
|
||
<span class="tpl-meta-item">{{ p.uuids.length }} 个光环</span>
|
||
</div>
|
||
<div class="tpl-chips">
|
||
<span v-for="(u, i) in p.uuids" :key="i" class="chip aura">
|
||
{{ u.name || fieldName(u.uuid) }} {{ u.value }}
|
||
</span>
|
||
</div>
|
||
<div class="tpl-chips" v-if="p.tags?.length">
|
||
<span v-for="t in p.tags" :key="t" class="chip tag-chip">{{ t }}</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="!fieldPresets.length" class="tpl-empty-role">
|
||
<span class="muted">暂无光环预设</span>
|
||
<el-button size="small" text type="primary" @click="addField">+ 补一个</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
|
||
<!-- 触发预设编辑弹窗 -->
|
||
<el-dialog
|
||
v-model="editTriggerVisible"
|
||
:title="`编辑触发预设 ${currentTrigger?.label || ''}`"
|
||
width="70%"
|
||
top="4vh"
|
||
:close-on-click-modal="false"
|
||
>
|
||
<template v-if="currentTrigger">
|
||
<div class="panel">
|
||
<h3>基础</h3>
|
||
<div class="form-grid">
|
||
<el-form-item label="预设名">
|
||
<el-input v-model="currentTrigger.label" placeholder="如:受击3次·全队护盾" />
|
||
</el-form-item>
|
||
<el-form-item label="触发组">
|
||
<el-select v-model="currentTrigger.group">
|
||
<el-option v-for="g in TRIGGER_GROUPS" :key="g.key" :label="g.label" :value="g.key" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="技能底座">
|
||
<el-select v-model="currentTrigger.uuid" filterable placeholder="选择技能">
|
||
<el-option v-for="s in skillOptions" :key="s.uuid" :label="`${s.uuid} ${s.name}`" :value="s.uuid" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="标签(逗号分隔)">
|
||
<el-input v-model="triggerTagsInput" placeholder="伤害,团队" />
|
||
</el-form-item>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h3>触发参数(entry)</h3>
|
||
<div class="form-grid">
|
||
<el-form-item label="解锁等级 lv">
|
||
<el-input-number v-model="currentTrigger.entry.lv" :min="1" :max="20" :controls="false" style="width: 100%" />
|
||
</el-form-item>
|
||
<el-form-item label="SkillLv">
|
||
<el-select v-model="currentTrigger.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">
|
||
<el-input-number v-model="currentTrigger.entry.t_num" :min="1" :controls="false" style="width: 100%" />
|
||
</el-form-item>
|
||
<template v-if="currentTrigger.group === 'atking' || currentTrigger.group === 'atked'">
|
||
<el-form-item label="t_chance 触发概率%(空=100)">
|
||
<el-input-number v-model="currentTrigger.entry.t_chance" :min="0" :max="100" :controls="false" style="width: 100%" />
|
||
</el-form-item>
|
||
<el-form-item label="pity_add 保底叠加%(空=0)">
|
||
<el-input-number v-model="currentTrigger.entry.pity_add" :min="0" :max="100" :controls="false" style="width: 100%" />
|
||
</el-form-item>
|
||
</template>
|
||
<el-form-item label="组名 name">
|
||
<el-input v-model="currentTrigger.entry.name" placeholder="留空回退底座名" />
|
||
</el-form-item>
|
||
</div>
|
||
<el-form-item label="overrides">
|
||
<OverrideEditor v-model="currentTrigger.entry.overrides" :skill-options="skillOptions" :buff-options="buffOptions" :base-skill-uuid="currentTrigger.uuid || 0" />
|
||
</el-form-item>
|
||
<el-form-item label="描述 desc">
|
||
<el-input v-model="currentTrigger.entry.desc" type="textarea" :rows="2" />
|
||
</el-form-item>
|
||
</div>
|
||
</template>
|
||
<template #footer>
|
||
<el-button size="small" @click="editTriggerVisible = false">关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 光环预设编辑弹窗 -->
|
||
<el-dialog
|
||
v-model="editFieldVisible"
|
||
:title="`编辑光环预设 ${currentField?.label || ''}`"
|
||
width="60%"
|
||
top="6vh"
|
||
:close-on-click-modal="false"
|
||
>
|
||
<template v-if="currentField">
|
||
<div class="panel">
|
||
<h3>基础</h3>
|
||
<div class="form-grid">
|
||
<el-form-item label="预设名">
|
||
<el-input v-model="currentField.label" placeholder="如:暴击双光环" />
|
||
</el-form-item>
|
||
<el-form-item label="解锁等级 lv">
|
||
<el-input-number v-model="currentField.lv" :min="1" :max="20" :controls="false" style="width: 100%" />
|
||
</el-form-item>
|
||
<el-form-item label="标签(逗号分隔)" class="grid-span">
|
||
<el-input v-model="fieldTagsInput" placeholder="暴击,增伤" />
|
||
</el-form-item>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h3>光环词条</h3>
|
||
<div v-for="(u, i) in currentField.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="currentField.uuids.splice(i, 1)">删除</el-button>
|
||
</div>
|
||
<el-button size="small" @click="currentField.uuids.push({ uuid: null, value: 0, name: '' })">+ 添加词条</el-button>
|
||
</div>
|
||
</template>
|
||
<template #footer>
|
||
<el-button size="small" @click="editFieldVisible = false">关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { api } from '../api'
|
||
import { TRIGGER_GROUPS, defaultTriggerPreset, defaultFieldPreset } from '../constants'
|
||
import OverrideEditor from './OverrideEditor.vue'
|
||
|
||
const props = defineProps({
|
||
// 由 App 传入并持有,本组件直接原地修改,保存时整体提交
|
||
triggerPresets: { type: Array, required: true },
|
||
fieldPresets: { type: Array, required: true },
|
||
skillOptions: { type: Array, default: () => [] },
|
||
skillMap: { type: Object, default: () => ({}) },
|
||
fieldOptions: { type: Array, default: () => [] },
|
||
buffOptions: { type: Array, default: () => [] },
|
||
})
|
||
|
||
const activeTab = ref('trigger')
|
||
const triggerGroup = ref('atking')
|
||
const editTriggerVisible = ref(false)
|
||
const editFieldVisible = ref(false)
|
||
const activeTriggerId = ref('')
|
||
const activeFieldId = ref('')
|
||
|
||
const currentTrigger = computed(() => props.triggerPresets.find((p) => p.id === activeTriggerId.value) || null)
|
||
const currentField = computed(() => props.fieldPresets.find((p) => p.id === activeFieldId.value) || null)
|
||
|
||
const filteredTriggerPresets = computed(() => props.triggerPresets.filter((p) => p.group === triggerGroup.value))
|
||
|
||
// tags 数组 ↔ 输入框字符串双向桥接
|
||
const triggerTagsInput = computed({
|
||
get: () => (currentTrigger.value?.tags || []).join(','),
|
||
set: (v) => { if (currentTrigger.value) currentTrigger.value.tags = v.split(/[,,]/).map((s) => s.trim()).filter(Boolean) },
|
||
})
|
||
const fieldTagsInput = computed({
|
||
get: () => (currentField.value?.tags || []).join(','),
|
||
set: (v) => { if (currentField.value) currentField.value.tags = v.split(/[,,]/).map((s) => s.trim()).filter(Boolean) },
|
||
})
|
||
|
||
function skillName(uuid) { return props.skillMap[uuid]?.name || String(uuid ?? '') }
|
||
function fieldName(uuid) { return props.fieldOptions.find((f) => f.uuid === uuid)?.name || String(uuid ?? '') }
|
||
|
||
// overrides 摘要(卡片展示用):ap100 / buff 等关键键
|
||
function overrideSummary(ov) {
|
||
if (!ov) return ''
|
||
const keys = ['ap', 'crt', 'stun', 'frz', 'para', 'bck', 'timed_buff_id', 'timed_debuff_id', 'proc_skill']
|
||
const parts = []
|
||
for (const k of keys) {
|
||
const v = ov[k]
|
||
if (v == null) continue
|
||
if (k === 'proc_skill') parts.push(`proc:${v.s_uuid}×${v.chance ?? 100}%`)
|
||
else parts.push(`${k}${v}`)
|
||
}
|
||
return parts.join(' ')
|
||
}
|
||
|
||
function openEditTrigger(p) { activeTriggerId.value = p.id; editTriggerVisible.value = true }
|
||
function openEditField(p) { activeFieldId.value = p.id; editFieldVisible.value = true }
|
||
|
||
function addPreset() {
|
||
if (activeTab.value === 'trigger') addTriggerFor(triggerGroup.value)
|
||
else addField()
|
||
}
|
||
function addTriggerFor(group) {
|
||
const p = defaultTriggerPreset(group)
|
||
props.triggerPresets.push(p)
|
||
openEditTrigger(p)
|
||
}
|
||
function addField() {
|
||
const p = defaultFieldPreset()
|
||
props.fieldPresets.push(p)
|
||
openEditField(p)
|
||
}
|
||
function removeTrigger(p) {
|
||
const i = props.triggerPresets.findIndex((x) => x.id === p.id)
|
||
if (i >= 0) props.triggerPresets.splice(i, 1)
|
||
}
|
||
function removeField(p) {
|
||
const i = props.fieldPresets.findIndex((x) => x.id === p.id)
|
||
if (i >= 0) props.fieldPresets.splice(i, 1)
|
||
}
|
||
|
||
async function doSave() {
|
||
try {
|
||
const r = await api.saveTemplates({
|
||
triggerPresets: props.triggerPresets,
|
||
fieldPresets: props.fieldPresets,
|
||
})
|
||
ElMessage.success(`已保存:触发 ${r.triggerCount} / 光环 ${r.fieldCount} → ${r.path}`)
|
||
} catch (e) {
|
||
ElMessage.error('保存失败: ' + (e?.response?.data?.error || e.message))
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.tpl-manager {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 顶部工具栏 */
|
||
.tpl-toolbar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 10px 16px;
|
||
background: var(--panel);
|
||
border-bottom: 1px solid var(--border);
|
||
flex-shrink: 0;
|
||
gap: 12px;
|
||
}
|
||
.tpl-toolbar-left { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
|
||
.tpl-title { font-size: 13px; font-weight: 600; color: var(--text); }
|
||
.tpl-toolbar-actions { display: flex; gap: 8px; flex-shrink: 0; }
|
||
|
||
/* tabs 撑满剩余高度 */
|
||
.tpl-tabs {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
padding: 0 16px;
|
||
}
|
||
.tpl-tabs :deep(.el-tabs__header) { margin-bottom: 4px; flex-shrink: 0; }
|
||
.tpl-tabs :deep(.el-tabs__content) { flex: 1; overflow: hidden; display: flex; flex-direction: column; }
|
||
.tpl-tabs :deep(.el-tab-pane) { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
||
|
||
/* 触发组二级筛选 */
|
||
.preset-sub-tabs {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 4px 0 8px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* 滚动区 */
|
||
.tpl-scroll {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding-bottom: 40px;
|
||
}
|
||
|
||
/* 预设卡片网格 */
|
||
.tpl-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||
gap: 10px;
|
||
}
|
||
.tpl-card {
|
||
background: var(--panel);
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
padding: 10px 12px;
|
||
cursor: pointer;
|
||
transition: box-shadow 0.15s, border-color 0.15s;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
.tpl-card:hover {
|
||
box-shadow: 0 3px 12px rgba(46, 91, 160, 0.15);
|
||
border-color: var(--brand-light);
|
||
}
|
||
.tpl-card-head {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.tpl-name { font-size: 13px; font-weight: 600; }
|
||
|
||
.tpl-meta { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--text-light); flex-wrap: wrap; }
|
||
.tpl-meta-sep { color: var(--border); }
|
||
|
||
.tpl-chips { display: flex; flex-wrap: wrap; gap: 4px; }
|
||
.chip {
|
||
background: #eef4fc;
|
||
color: var(--brand);
|
||
border-radius: 4px;
|
||
padding: 0 6px;
|
||
font-size: 11px;
|
||
line-height: 18px;
|
||
}
|
||
.chip.aura { background: #f4f0fa; color: #7c5cbf; }
|
||
.chip.tag-chip { background: var(--el-fill-color); color: var(--text-light); }
|
||
|
||
.tag {
|
||
display: inline-block;
|
||
background: #eef4fc;
|
||
color: var(--brand);
|
||
border-radius: 4px;
|
||
padding: 0 5px;
|
||
font-size: 11px;
|
||
line-height: 18px;
|
||
font-family: Menlo, Consolas, monospace;
|
||
}
|
||
|
||
.tpl-empty-role {
|
||
border: 1px dashed var(--border);
|
||
border-radius: 8px;
|
||
padding: 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 16px; }
|
||
.form-grid .grid-span { grid-column: 1 / -1; }
|
||
|
||
@media (max-width: 600px) {
|
||
.tpl-cards { grid-template-columns: 1fr; }
|
||
.form-grid { grid-template-columns: 1fr; }
|
||
}
|
||
</style>
|