diff --git a/put/server/data/templates.json b/put/server/data/templates.json
new file mode 100644
index 00000000..4bc3acd9
--- /dev/null
+++ b/put/server/data/templates.json
@@ -0,0 +1,132 @@
+{
+ "triggerPresets": [
+ {
+ "id": "tp-atking-fenli",
+ "label": "每3次普攻·奋力一击",
+ "group": "atking",
+ "uuid": 6004,
+ "tags": ["伤害"],
+ "entry": { "lv": 4, "s_lv": 1, "name": "风暴打击", "t_num": 3, "overrides": { "ap": 300 }, "desc": "立刻打击{target},造成攻击力{ap}%伤害" }
+ },
+ {
+ "id": "tp-atking-poison",
+ "label": "每3次普攻·蚀骨毒箭",
+ "group": "atking",
+ "uuid": 6007,
+ "tags": ["伤害", "持续"],
+ "entry": { "lv": 4, "s_lv": 1, "name": "蚀骨毒箭", "t_num": 3, "overrides": { "ap": 80, "timed_debuff_id": 7005, "debuff_value": 40, "debuff_chance": 100 }, "desc": "追加射出蚀骨毒箭,对{target}造成攻击力{ap}%伤害,必定使目标中毒" }
+ },
+ {
+ "id": "tp-atking-heavy-arrow",
+ "label": "每3次普攻·追加重箭",
+ "group": "atking",
+ "uuid": 6008,
+ "tags": ["伤害", "击退"],
+ "entry": { "lv": 4, "s_lv": 1, "name": "", "t_num": 3, "overrides": { "ap": 120, "bck_chance": 50 }, "desc": "" }
+ },
+ {
+ "id": "tp-atking-shield",
+ "label": "每3次普攻·附加护盾",
+ "group": "atking",
+ "uuid": 6301,
+ "tags": ["防御", "团队"],
+ "entry": { "lv": 4, "s_lv": 1, "name": "圣愈之盾", "t_num": 3, "overrides": { "ap": 30 }, "desc": "{target}获得生命最大值{ap}%的护盾" }
+ },
+ {
+ "id": "tp-atked-shield-team",
+ "label": "受击3次·全队护盾",
+ "group": "atked",
+ "uuid": 6301,
+ "tags": ["防御", "团队"],
+ "entry": { "lv": 4, "s_lv": 1, "name": "全体圣盾", "t_num": 3, "overrides": { "ap": 10 }, "desc": "全队获得本英雄生命最大值{ap}%的护盾" }
+ },
+ {
+ "id": "tp-atked-heal-self",
+ "label": "受击3次·自我治疗",
+ "group": "atked",
+ "uuid": 6302,
+ "tags": ["治疗"],
+ "entry": { "lv": 4, "s_lv": 1, "name": "", "t_num": 3, "overrides": { "ap": 5 }, "desc": "为自己释放治疗,恢复生命值的{ap}%" }
+ },
+ {
+ "id": "tp-atked-thorns",
+ "label": "受击3次·荆棘反伤",
+ "group": "atked",
+ "uuid": 6516,
+ "tags": ["反伤", "防御"],
+ "entry": { "lv": 4, "s_lv": 1, "name": "荆棘回应", "t_num": 3, "overrides": { "timed_buff_id": 7028, "buff_value": 10 }, "desc": "凝聚荆棘护体,受击时反弹{buff_value}%所受伤害" }
+ },
+ {
+ "id": "tp-fstart-crit-burst",
+ "label": "开场·团队暴击",
+ "group": "fstart",
+ "uuid": 6504,
+ "tags": ["增益", "团队", "暴击"],
+ "entry": { "lv": 4, "s_lv": 1, "name": "猎杀时刻", "t_num": 1, "overrides": { "buff_value": 30 }, "desc": "战斗开始时全体友方暴击率提升{buff_value}%" }
+ },
+ {
+ "id": "tp-fstart-armor-break",
+ "label": "开场·先手破甲",
+ "group": "fstart",
+ "uuid": 6012,
+ "tags": ["减益", "破甲"],
+ "entry": { "lv": 4, "s_lv": 1, "name": "先手蚀甲", "t_num": 1, "overrides": { "ap": 150, "timed_debuff_id": 7008 }, "desc": "战斗开始掷出蚀甲酸瓶,必定使其破甲" }
+ },
+ {
+ "id": "tp-fstart-lightning",
+ "label": "开场·雷击先手",
+ "group": "fstart",
+ "uuid": 6203,
+ "tags": ["伤害", "麻痹"],
+ "entry": { "lv": 4, "s_lv": 1, "name": "雷击先手", "t_num": 1, "overrides": { "ap": 150, "para": 30 }, "desc": "战斗开始释放雷击,对{target}造成{ap}%伤害,{para}%概率麻痹" }
+ }
+ ],
+ "fieldPresets": [
+ {
+ "id": "fp-crit-master",
+ "label": "暴击双光环(暴击率+暴伤)",
+ "tags": ["暴击"],
+ "lv": 4,
+ "uuids": [
+ { "uuid": 7004, "value": 0.12, "name": "剑刃风暴" },
+ { "uuid": 7005, "value": 0.2, "name": "斩魂利刃" }
+ ]
+ },
+ {
+ "id": "fp-atk-aura",
+ "label": "攻击光环 15%",
+ "tags": ["增伤"],
+ "lv": 4,
+ "uuids": [
+ { "uuid": 7002, "value": 0.15, "name": "" }
+ ]
+ },
+ {
+ "id": "fp-atk-speed",
+ "label": "攻速光环 15%",
+ "tags": ["攻速"],
+ "lv": 4,
+ "uuids": [
+ { "uuid": 7006, "value": 0.15, "name": "" }
+ ]
+ },
+ {
+ "id": "fp-armor-pierce",
+ "label": "穿透光环 10%",
+ "tags": ["破甲"],
+ "lv": 4,
+ "uuids": [
+ { "uuid": 7009, "value": 0.1, "name": "锐矛之赐" }
+ ]
+ },
+ {
+ "id": "fp-thorns-aura",
+ "label": "荆棘领域 8",
+ "tags": ["反伤", "防御"],
+ "lv": 4,
+ "uuids": [
+ { "uuid": 7022, "value": 8, "name": "荆棘领域" }
+ ]
+ }
+ ]
+}
diff --git a/put/server/index.js b/put/server/index.js
index d41fafa2..b4f3ebc1 100644
--- a/put/server/index.js
+++ b/put/server/index.js
@@ -103,6 +103,47 @@ app.post('/api/heroes', (req, res) => {
}
})
+const TEMPLATES_PATH = path.join(DATA_DIR, 'templates.json')
+
+/** 预设库持久化:GET 读取(无文件时返回空池) */
+app.get('/api/templates', (_req, res) => {
+ try {
+ if (!fs.existsSync(TEMPLATES_PATH)) return res.json({ triggerPresets: [], fieldPresets: [] })
+ const data = JSON.parse(fs.readFileSync(TEMPLATES_PATH, 'utf8'))
+ // 兼容旧格式:若根上是 templates 数组,返回空池让前端走默认
+ if (Array.isArray(data.templates)) return res.json({ triggerPresets: [], fieldPresets: [] })
+ res.json({
+ triggerPresets: Array.isArray(data.triggerPresets) ? data.triggerPresets : [],
+ fieldPresets: Array.isArray(data.fieldPresets) ? data.fieldPresets : [],
+ })
+ } catch (e) {
+ res.status(500).json({ error: String(e?.message || e) })
+ }
+})
+
+/** 职业模板持久化:POST 全量覆盖保存(结构:{ triggerPresets: [], fieldPresets: [] }) */
+app.post('/api/templates', (req, res) => {
+ const body = req.body || {}
+ const triggerPresets = body.triggerPresets
+ const fieldPresets = body.fieldPresets
+ if (!Array.isArray(triggerPresets) || !Array.isArray(fieldPresets)) {
+ return res.status(400).json({ error: 'triggerPresets 与 fieldPresets 必须为数组' })
+ }
+ // 触发预设必须绑定合法触发组
+ const groups = ['atking', 'atked', 'fstart', 'dead']
+ const bad = triggerPresets.find((p) => !groups.includes(p.group))
+ if (bad) {
+ return res.status(400).json({ error: `触发预设 ${bad.id || bad.label} 的 group 非法:${bad.group}` })
+ }
+ try {
+ fs.mkdirSync(DATA_DIR, { recursive: true })
+ fs.writeFileSync(TEMPLATES_PATH, JSON.stringify({ triggerPresets, fieldPresets }, null, 2), 'utf8')
+ res.json({ ok: true, path: TEMPLATES_PATH, triggerCount: triggerPresets.length, fieldCount: fieldPresets.length })
+ } catch (e) {
+ res.status(500).json({ error: String(e?.message || e) })
+ }
+})
+
/** 序列化 overrides:TGroup 以字符串存储(如 "Team"),导出时还原为枚举表达式 TGroup.Team;
* buff_target 以数值存储(0-3),导出时还原为 BuffTarget.X 枚举表达式提升可读性 */
const BUFF_TARGET_NAMES = ['Hit', 'Self', 'Ally', 'Enemy']
diff --git a/put/web/src/App.vue b/put/web/src/App.vue
index 91615175..1b93b228 100644
--- a/put/web/src/App.vue
+++ b/put/web/src/App.vue
@@ -3,6 +3,10 @@
英雄配置编辑器