Files
90CNServer/gmweb/app.js
T
hanfuit e9a3952a9d GM 页:修复内嵌 IE 输入法 + 物品分类浏览
- 90CN.dll:给 IE 控件窗口链重新关联 IME 上下文,中文输入不再只能打字母
- gmweb:页面加 ime-mode:active,focus 时再主动要一次
- 物品页加分组/类型/等级筛选,不依赖中文输入也能定位物品
- 服务端指纹回绑新 DLL(1223680 / 26633404…)
2026-11-05 12:37:03 +08:00

491 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
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');
}
/* 分组 -> 类型选项。装备/时装/宠物走 tag(PVF 的 equipment type),
道具走 segment(服务端按 stackable type 归类成中文段)。
有了这两级下拉,不敲键盘也能把范围缩到十几页。 */
var TYPE_OPTIONS = {
equipment: [
['tag:weapon', '武器'], ['tag:coat', '上衣'], ['tag:shoulder', '头肩'],
['tag:pants', '下装'], ['tag:shoes', '鞋'], ['tag:waist', '腰带'],
['tag:amulet', '项链'], ['tag:wrist', '手镯'], ['tag:ring', '戒指'],
['tag:support', '辅助装备'], ['tag:magic stone', '魔法石'],
['tag:earring', '耳环'], ['tag:support weapon', '辅助武器'],
['tag:title name', '称号'], ['tag:name tag', '名牌']
],
avatar: [
['tag:hat avatar', '帽子'], ['tag:hair avatar', '头发'], ['tag:face avatar', '脸部'],
['tag:coat avatar', '上衣'], ['tag:breast avatar', '胸部'], ['tag:waist avatar', '腰部'],
['tag:pants avatar', '下装'], ['tag:shoes avatar', '鞋'], ['tag:skin avatar', '皮肤'],
['tag:aurora avatar', '光环'], ['tag:weapon avatar', '武器装扮']
],
pet: [
['tag:creature', '宠物'], ['tag:artifact red', '神器·红'],
['tag:artifact blue', '神器·蓝'], ['tag:artifact green', '神器·绿']
],
stackable: [
['seg:消耗品', '消耗品'], ['seg:材料', '材料'], ['seg:特殊材料', '特殊材料'],
['seg:任务品', '任务品'], ['seg:徽章', '徽章'], ['seg:副职业材料', '副职业材料']
]
};
function fillTypeOptions() {
var opts = TYPE_OPTIONS[$('in-group').value] || [];
var h = ['<option value="">不限</option>'];
for (var i = 0; i < opts.length; i++) {
h.push('<option value="' + esc(opts[i][0]) + '">' + esc(opts[i][1]) + '</option>');
}
$('in-tag').innerHTML = h.join('');
}
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 group = $('in-group').value;
var picked = $('in-tag').value;
var minLv = parseInt($('in-lv-min').value, 10);
var maxLv = parseInt($('in-lv-max').value, 10);
if (isNaN(minLv)) { minLv = 0; }
if (isNaN(maxLv)) { maxLv = 0; }
var qs = 'limit=' + size + '&offset=' + ((p - 1) * size) +
'&q=' + encodeURIComponent(q || '');
if (group) { qs += '&group=' + encodeURIComponent(group); }
if (picked) {
if (picked.indexOf('tag:') === 0) {
qs += '&tag=' + encodeURIComponent(picked.substring(4));
} else if (picked.indexOf('seg:') === 0) {
qs += '&segment=' + encodeURIComponent(picked.substring(4));
}
}
if (minLv > 0) { qs += '&min_level=' + minLv; }
if (maxLv > 0) { qs += '&max_level=' + maxLv; }
api('/dnf/gm/items/page?' + qs, '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;
}
/* 增幅属性编号取自 etc/amplifyitem.etc 的 [option mapping table]。 */
var AMP_LABEL = { 1: '物防', 2: '魔防', 3: '物攻', 4: '魔攻' };
function optText(o) {
var parts = [];
if (o.amplify_type > 0) {
parts.push('增幅' + (AMP_LABEL[o.amplify_type] || 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; }
}
/* 内嵌 IE 控件的输入法常被游戏主窗口占着,focus 时再主动要一次。
ime-mode:active 是 IE 私有属性,别的浏览器会忽略,不影响兼容。 */
function forceIME() {
var els = document.getElementsByTagName('input');
for (var i = 0; i < els.length; i++) {
els[i].onfocus = function () {
try { this.style.imeMode = 'active'; } catch (e) { }
};
}
}
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); };
$('in-group').onchange = function () { fillTypeOptions(); loadItems(1); };
$('in-tag').onchange = function () { loadItems(1); };
/* 等级快捷档:点一下就把区间填好并重新查,省得手敲。 */
var lvBtns = document.getElementsByTagName('button');
for (var b = 0; b < lvBtns.length; b++) {
var spec = lvBtns[b].getAttribute('data-lv');
if (!spec) { continue; }
lvBtns[b].onclick = function () {
var pair = this.getAttribute('data-lv').split('-');
$('in-lv-min').value = pair[0];
$('in-lv-max').value = pair[1];
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); } };
fillTypeOptions();
renderPicked();
forceIME();
log('GM 控制台已加载', 'tip');
loadStatus();
loadChars();
setInterval(loadStatus, 15000);
}
if (window.addEventListener) { window.addEventListener('load', init, false); }
else { window.attachEvent('onload', init); }
})();