- 90CN.dll 新增 IE 控件接管,把腾讯小助手页面重定向到本地 GM 页 - gmweb:物品列表改分页(每页 10/20/50,列表固定 6 行高) - 发放走 grant-options,支持强化 0-31、增幅属性+等级、锻造 0-8、品质 - 服务端补齐 amplify_level:等级统一写入 ext_data0/packed_flag_byte/reinforce/upgrade_level - 增幅与强化互斥校验,避免写出客户端无法创建的装备
403 lines
15 KiB
JavaScript
403 lines
15 KiB
JavaScript
/* GM 控制台前端 —— 严格 ES5 + XMLHttpRequest,兼容 IE11 内嵌浏览器。
|
||
所有请求走同源 /api/*,由本地 serve.py 代理到服务端 admin 口并注入 token。 */
|
||
(function () {
|
||
'use strict';
|
||
|
||
var S = {
|
||
charId: '', charName: '', chars: [],
|
||
itemId: 0, itemName: '', picked: null, items: [],
|
||
page: 1, pageSize: 20, total: 0
|
||
};
|
||
|
||
/* ---------------------------------------------------------------- 基础 */
|
||
|
||
function $(id) { return document.getElementById(id); }
|
||
|
||
function log(msg, cls) {
|
||
var box = $('log');
|
||
var d = document.createElement('div');
|
||
if (cls) { d.className = cls; }
|
||
var t = new Date();
|
||
var p = function (n) { return (n < 10 ? '0' : '') + n; };
|
||
d.innerHTML = '[' + p(t.getHours()) + ':' + p(t.getMinutes()) + ':' + p(t.getSeconds()) + '] ' + esc(msg);
|
||
box.appendChild(d);
|
||
box.scrollTop = box.scrollHeight;
|
||
while (box.childNodes.length > 120) { box.removeChild(box.firstChild); }
|
||
}
|
||
|
||
function esc(s) {
|
||
return String(s == null ? '' : s)
|
||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||
}
|
||
|
||
function api(path, method, body, cb) {
|
||
var xhr = new XMLHttpRequest();
|
||
xhr.open(method || 'GET', '/api' + path, true);
|
||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||
xhr.onreadystatechange = function () {
|
||
if (xhr.readyState !== 4) { return; }
|
||
if (xhr.status === 0) { log('连不上本地代理(serve.py 没启动?)', 'err'); cb(null); return; }
|
||
var data = null;
|
||
try { data = JSON.parse(xhr.responseText); } catch (e) { data = null; }
|
||
if (!data) { log('响应不是 JSON:' + xhr.responseText.substring(0, 120), 'err'); cb(null); return; }
|
||
if (data.success === false || data.ok === false) {
|
||
log('失败:' + (data.message || data.error || '未知错误'), 'err');
|
||
cb(null);
|
||
return;
|
||
}
|
||
cb(data);
|
||
};
|
||
xhr.send(body ? JSON.stringify(body) : null);
|
||
}
|
||
|
||
function setOnline(on, text) {
|
||
$('dot').className = 'dot ' + (on ? 'on' : 'off');
|
||
$('stat').innerHTML = esc(text);
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- 标签页 */
|
||
|
||
function switchTab(name) {
|
||
var tabs = document.getElementsByClassName('tab');
|
||
for (var i = 0; i < tabs.length; i++) {
|
||
tabs[i].className = 'tab' + (tabs[i].getAttribute('data-tab') === name ? ' on' : '');
|
||
}
|
||
var panes = document.getElementsByClassName('pane');
|
||
for (var j = 0; j < panes.length; j++) {
|
||
panes[j].className = 'pane' + (panes[j].id === 'pane-' + name ? ' on' : '');
|
||
}
|
||
}
|
||
|
||
function bindTabs() {
|
||
var tabs = document.getElementsByClassName('tab');
|
||
for (var i = 0; i < tabs.length; i++) {
|
||
tabs[i].onclick = function () { switchTab(this.getAttribute('data-tab')); };
|
||
}
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- 状态页 */
|
||
|
||
function loadStatus() {
|
||
api('/dnf/gm/status', 'GET', null, function (d) {
|
||
if (!d) { setOnline(false, '服务端未响应'); return; }
|
||
var v = d.data || {};
|
||
$('v-online').innerHTML = esc(v.online_characters == null ? '-' : v.online_characters);
|
||
$('v-repo').innerHTML = v.repository_ready ? '就绪' : '未就绪';
|
||
$('v-items').innerHTML = v.item_catalog_ready ? '就绪' : '未就绪';
|
||
$('v-cap').innerHTML = esc(v.level_cap == null ? '-' : v.level_cap);
|
||
setOnline(true, '已连接 127.0.0.1:18111');
|
||
});
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- 角色 */
|
||
|
||
function renderChars() {
|
||
var box = $('char-list');
|
||
if (!S.chars.length) { box.innerHTML = '<div class="empty">没有角色</div>'; return; }
|
||
var h = [];
|
||
for (var i = 0; i < S.chars.length; i++) {
|
||
var c = S.chars[i];
|
||
h.push('<div class="li' + (String(c.character_id) === String(S.charId) ? ' on' : '') +
|
||
'" data-id="' + esc(c.character_id) + '" data-name="' + esc(c.name) + '" data-gold="' + esc(c.gold) +
|
||
'" data-level="' + esc(c.level) + '"><span class="nm">' + esc(c.name) +
|
||
'</span><span class="meta">Lv.' + esc(c.level) + ' · 金币 ' + esc(c.gold) +
|
||
(c.online ? ' · 在线' : '') + '</span></div>');
|
||
}
|
||
box.innerHTML = h.join('');
|
||
var lis = box.getElementsByClassName('li');
|
||
for (var k = 0; k < lis.length; k++) {
|
||
lis[k].onclick = function () { pickChar(this); };
|
||
}
|
||
}
|
||
|
||
function pickChar(el) {
|
||
S.charId = el.getAttribute('data-id');
|
||
S.charName = el.getAttribute('data-name');
|
||
$('in-gold').value = el.getAttribute('data-gold');
|
||
$('in-level').value = el.getAttribute('data-level');
|
||
$('cur').innerHTML = esc(S.charName) + ' <span style="color:#7c8496">(id=' + esc(S.charId) + ')</span>';
|
||
renderChars();
|
||
log('已选角色:' + S.charName, 'tip');
|
||
}
|
||
|
||
function loadChars() {
|
||
var q = $('q-char').value;
|
||
var path = '/local/dnf-gm/characters' + (q ? '?character_name=' + encodeURIComponent(q) : '');
|
||
api(path, 'GET', null, function (d) {
|
||
if (!d) { return; }
|
||
S.chars = d.data || [];
|
||
renderChars();
|
||
log('角色 ' + S.chars.length + ' 个');
|
||
if (S.chars.length === 1) { pickChar($('char-list').getElementsByClassName('li')[0]); }
|
||
});
|
||
}
|
||
|
||
function setGold() {
|
||
if (!requireChar()) { return; }
|
||
var v = parseInt($('in-gold').value, 10);
|
||
if (isNaN(v) || v < 0) { log('金币要填非负整数', 'err'); return; }
|
||
api('/dnf/gm/characters/' + S.charId + '/gold', 'POST', { value: v }, function (d) {
|
||
if (d) { log('金币已改为 ' + v, 'ok'); loadChars(); }
|
||
});
|
||
}
|
||
|
||
function setLevel() {
|
||
if (!requireChar()) { return; }
|
||
var v = parseInt($('in-level').value, 10);
|
||
if (isNaN(v) || v < 1) { log('等级要填正整数', 'err'); return; }
|
||
api('/dnf/gm/characters/' + S.charId + '/level', 'POST', { level: v }, function (d) {
|
||
if (d) { log('等级已改为 ' + v, 'ok'); loadChars(); }
|
||
});
|
||
}
|
||
|
||
function requireChar() {
|
||
if (!S.charId) { log('先在「角色」页选一个角色', 'err'); return false; }
|
||
return true;
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- 物品 */
|
||
|
||
/* 装备类型标签:PVF 里是 "[weapon]" 这种,去掉括号后映射成中文。 */
|
||
var EQUIP_LABEL = {
|
||
'weapon': '武器', 'coat': '上衣', 'shoulder': '头肩', 'pants': '下装',
|
||
'shoes': '鞋', 'waist': '腰带', 'amulet': '项链', 'wrist': '手镯',
|
||
'ring': '戒指', 'support': '辅助装备', 'magic stone': '魔法石',
|
||
'earring': '耳环', 'support weapon': '辅助武器', 'avatar': '时装',
|
||
'creature': '宠物', 'artifact': '神器'
|
||
};
|
||
|
||
/* 与服务器 gmItemSupportsEquipmentAttributes 保持一致:只有这些类型吃得下强化/增幅。 */
|
||
var UPGRADABLE = {
|
||
'weapon': 1, 'coat': 1, 'shoulder': 1, 'pants': 1, 'shoes': 1, 'waist': 1,
|
||
'amulet': 1, 'wrist': 1, 'ring': 1, 'support': 1, 'magic stone': 1,
|
||
'earring': 1, 'support weapon': 1
|
||
};
|
||
|
||
function equipTag(value) {
|
||
return String(value == null ? '' : value)
|
||
.replace(/[\[\]`]/g, '').replace(/^\s+|\s+$/g, '').toLowerCase();
|
||
}
|
||
|
||
function kindText(it) {
|
||
if (!it) { return ''; }
|
||
if (it.kind !== 'equipment') { return '道具'; }
|
||
var tag = equipTag(it.equipment_type);
|
||
return '装备 · ' + (EQUIP_LABEL[tag] || tag || '未知');
|
||
}
|
||
|
||
function canUpgrade(it) {
|
||
if (!it || it.kind !== 'equipment') { return false; }
|
||
return UPGRADABLE[equipTag(it.equipment_type)] === 1;
|
||
}
|
||
|
||
function clamp(v, lo, hi) {
|
||
if (isNaN(v)) { return lo; }
|
||
return v < lo ? lo : (v > hi ? hi : v);
|
||
}
|
||
|
||
function renderItems() {
|
||
var box = $('item-list');
|
||
var pages = S.total > 0 ? Math.ceil(S.total / S.pageSize) : 1;
|
||
$('page-info').innerHTML = '第 ' + S.page + ' / ' + pages + ' 页 · 共 ' + S.total + ' 条';
|
||
$('btn-prev').disabled = S.page <= 1;
|
||
$('btn-next').disabled = S.page >= pages;
|
||
|
||
if (!S.items.length) {
|
||
box.innerHTML = '<div class="empty">没有匹配的物品</div>';
|
||
return;
|
||
}
|
||
var h = [];
|
||
for (var i = 0; i < S.items.length; i++) {
|
||
var it = S.items[i];
|
||
h.push('<div class="li' + (it.item_id === S.itemId ? ' on' : '') +
|
||
'" data-i="' + i + '">' +
|
||
'<span class="nm">' + esc(it.name) + '</span>' +
|
||
'<span class="meta">id ' + esc(it.item_id) + ' · ' + esc(kindText(it)) +
|
||
(canUpgrade(it) ? '' : '') + '</span></div>');
|
||
}
|
||
box.innerHTML = h.join('');
|
||
var lis = box.getElementsByClassName('li');
|
||
for (var k = 0; k < lis.length; k++) {
|
||
lis[k].onclick = function () {
|
||
var index = parseInt(this.getAttribute('data-i'), 10);
|
||
pickItem(S.items[index]);
|
||
};
|
||
}
|
||
}
|
||
|
||
function renderPicked() {
|
||
var box = $('picked');
|
||
if (!S.itemId) {
|
||
box.innerHTML = '未选择物品 —— 先搜索,再点一行';
|
||
} else {
|
||
var it = S.picked;
|
||
var hint = '';
|
||
if (it && it.kind === 'equipment') {
|
||
hint = canUpgrade(it) ? '可强化 / 增幅' : '该类型不支持强化(时装类)';
|
||
} else {
|
||
hint = '普通道具,属性值会被忽略';
|
||
}
|
||
box.innerHTML = esc(S.itemName) + ' <span class="dim">(id ' + esc(S.itemId) + ')</span> · ' + esc(hint);
|
||
}
|
||
var off = !canUpgrade(S.picked);
|
||
$('in-reinforce').disabled = off;
|
||
$('in-amplify').disabled = off;
|
||
$('in-amplify-level').disabled = off;
|
||
$('in-forging').disabled = off;
|
||
$('in-quality').disabled = off;
|
||
}
|
||
|
||
function pickItem(it) {
|
||
if (!it) { return; }
|
||
S.itemId = it.item_id;
|
||
S.itemName = it.name;
|
||
S.picked = it;
|
||
renderItems();
|
||
renderPicked();
|
||
log('已选物品:' + it.name + '(id ' + it.item_id + ')', 'tip');
|
||
}
|
||
|
||
function loadItems(page) {
|
||
var q = $('q-item').value;
|
||
var size = parseInt($('in-page-size').value, 10) || 20;
|
||
var p = page || 1;
|
||
if (p < 1) { p = 1; }
|
||
S.pageSize = size;
|
||
var offset = (p - 1) * size;
|
||
api('/dnf/gm/items/page?limit=' + size + '&offset=' + offset +
|
||
'&q=' + encodeURIComponent(q || ''), 'GET', null, function (d) {
|
||
if (!d) { return; }
|
||
var pageData = d.data || {};
|
||
S.items = pageData.items || [];
|
||
S.total = pageData.total || 0;
|
||
S.page = p;
|
||
renderItems();
|
||
log('物品 ' + S.items.length + ' 条 / 共 ' + S.total + ' 条');
|
||
});
|
||
}
|
||
|
||
/* 组装强化/增幅/锻造/品质:增幅优先,红字装不再叠加白字强化。 */
|
||
function readOptions() {
|
||
var out = { reinforce: 0, amplify_type: 0, amplify_level: 0, forging: 0, equipment_quality: '' };
|
||
if (!canUpgrade(S.picked)) { return out; }
|
||
var amp = parseInt($('in-amplify').value, 10) || 0;
|
||
var ampLevel = parseInt($('in-amplify-level').value, 10) || 0;
|
||
var rein = parseInt($('in-reinforce').value, 10) || 0;
|
||
var forge = parseInt($('in-forging').value, 10) || 0;
|
||
if (amp > 0) {
|
||
out.amplify_type = clamp(amp, 1, 4);
|
||
out.amplify_level = clamp(ampLevel, 0, 31);
|
||
out.forging = 0;
|
||
} else {
|
||
out.reinforce = clamp(rein, 0, 31);
|
||
out.forging = clamp(forge, 0, 8);
|
||
}
|
||
out.equipment_quality = $('in-quality').value || '';
|
||
return out;
|
||
}
|
||
|
||
function optText(o) {
|
||
var parts = [];
|
||
if (o.amplify_type > 0) {
|
||
parts.push('增幅' + o.amplify_type + ' +' + o.amplify_level);
|
||
} else if (o.reinforce > 0) {
|
||
parts.push('强化 +' + o.reinforce);
|
||
}
|
||
if (o.forging > 0) { parts.push('锻造 +' + o.forging); }
|
||
if (o.equipment_quality) {
|
||
parts.push('品质' + (o.equipment_quality === 'top' ? '最上级' : '随机'));
|
||
}
|
||
return parts.length ? '(' + parts.join(',') + ')' : '';
|
||
}
|
||
|
||
function grantItem() {
|
||
if (!requireChar()) { return; }
|
||
if (!S.itemId) { log('先搜一个物品并选中', 'err'); return; }
|
||
var n = parseInt($('in-count').value, 10) || 1;
|
||
var o = readOptions();
|
||
api('/local/dnf-gm/item/grant-options', 'POST', {
|
||
character_id: String(S.charId), item_id: S.itemId, count: n,
|
||
reinforce: o.reinforce, amplify_type: o.amplify_type, amplify_level: o.amplify_level,
|
||
forging: o.forging, equipment_quality: o.equipment_quality
|
||
}, function (d) {
|
||
if (!d) { return; }
|
||
var data = d.data || {};
|
||
log('已发 ' + S.itemName + ' ×' + n + optText(o) +
|
||
(data.slot != null ? ',格位 ' + data.list_type + ':' + data.slot : ''), 'ok');
|
||
});
|
||
}
|
||
|
||
function mailItem() {
|
||
if (!requireChar()) { return; }
|
||
if (!S.itemId) { log('先搜一个物品并选中', 'err'); return; }
|
||
var n = parseInt($('in-count').value, 10) || 1;
|
||
var o = readOptions();
|
||
api('/dnf/gm/characters/' + S.charId + '/mail', 'POST', {
|
||
item_id: S.itemId, count: n, title: 'GM 物品', body: 'GM 发放',
|
||
expire_days: 7, reinforce: o.reinforce, amplify_type: o.amplify_type,
|
||
amplify_level: o.amplify_level, equipment_quality: o.equipment_quality,
|
||
forging: o.forging, emblem_item_ids: [], enchant_bead_item_id: 0
|
||
}, function (d) {
|
||
if (d) { log('已邮寄 ' + S.itemName + ' ×' + n + optText(o), 'ok'); }
|
||
});
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- 邮件 */
|
||
|
||
function sendMail() {
|
||
if (!requireChar()) { return; }
|
||
var gold = parseInt($('in-goldmail').value, 10) || 0;
|
||
api('/local/dnf-gm/mail', 'POST', {
|
||
character_name: S.charName,
|
||
title: $('in-title').value || 'GM 邮件',
|
||
body: $('in-body').value || '',
|
||
gold: gold,
|
||
attachments: []
|
||
}, function (d) {
|
||
if (d) { log('邮件已发送给 ' + S.charName, 'ok'); }
|
||
});
|
||
}
|
||
|
||
/* ---------------------------------------------------------------- 启动 */
|
||
|
||
function bind(id, fn) {
|
||
var el = $(id);
|
||
if (el) { el.onclick = fn; }
|
||
}
|
||
|
||
function init() {
|
||
bindTabs();
|
||
|
||
bind('btn-refresh', function () { loadStatus(); log('刷新状态'); });
|
||
bind('btn-resync', function () {
|
||
if (!requireChar()) { return; }
|
||
api('/local/quest-resync', 'POST', { character_id: parseInt(S.charId, 10) }, function (d) {
|
||
if (d) { log('角色数据已同步', 'ok'); }
|
||
});
|
||
});
|
||
bind('btn-search-char', loadChars);
|
||
bind('btn-gold', setGold);
|
||
bind('btn-level', setLevel);
|
||
bind('btn-search-item', function () { loadItems(1); });
|
||
bind('btn-prev', function () { if (S.page > 1) { loadItems(S.page - 1); } });
|
||
bind('btn-next', function () { loadItems(S.page + 1); });
|
||
bind('btn-grant', grantItem);
|
||
bind('btn-mailitem', mailItem);
|
||
bind('btn-mail', sendMail);
|
||
|
||
$('in-page-size').onchange = function () { loadItems(1); };
|
||
|
||
$('q-char').onkeydown = function (e) { if ((e || window.event).keyCode === 13) { loadChars(); } };
|
||
$('q-item').onkeydown = function (e) { if ((e || window.event).keyCode === 13) { loadItems(1); } };
|
||
|
||
renderPicked();
|
||
log('GM 控制台已加载', 'tip');
|
||
loadStatus();
|
||
loadChars();
|
||
setInterval(loadStatus, 15000);
|
||
}
|
||
|
||
if (window.addEventListener) { window.addEventListener('load', init, false); }
|
||
else { window.attachEvent('onload', init); }
|
||
})();
|