Files

376 lines
16 KiB
JavaScript
Raw Permalink Normal View History

/* 简航 SimpleNav —— 后台交互 */
(function () {
'use strict';
var $ = function (s, r) { return (r || document).querySelector(s); };
var $$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); };
/* ---------- 显示图标预览 ---------- */
function setPreview(src) {
var box = $('#f-icon-preview');
if (!box) return;
box.innerHTML = src ? '<img src="' + src + '" alt="">' : '<span class="ph">无</span>';
}
/* ---------- 自动获取网站信息 ---------- */
var fetchBtn = $('#btn-fetch');
if (fetchBtn) {
fetchBtn.addEventListener('click', function () {
var url = ($('#f-url') || {}).value || '';
var hint = $('#fetch-hint');
if (!url) { if (hint) hint.textContent = '请先填写网址'; return; }
if (hint) hint.textContent = '正在获取标题与高清图标…';
fetchBtn.disabled = true;
fetch(fetchBtn.getAttribute('data-url') + '?url=' + encodeURIComponent(url))
.then(function (r) { return r.json(); })
.then(function (d) {
fetchBtn.disabled = false;
if (!d.ok) { if (hint) hint.textContent = d.msg || '获取失败'; return; }
if ($('#f-name')) $('#f-name').value = d.name || '';
if ($('#f-short') && !$('#f-short').value) $('#f-short').value = d.short || d.name || '';
var auto = $('#f-icon-auto');
var locked = auto && !auto.checked;
if (!locked) {
if ($('#f-icon')) $('#f-icon').value = d.icon || '';
setPreview(d.icon_url || '');
}
var parts = ['已获取名称'];
parts.push(d.icon ? ('已获取图标(候选 ' + (d.candidates || 1) + ' 个)') : '未找到图标,将回退为首字母块');
if (locked) parts.push('(当前为自定义图标,未覆盖)');
if (hint) hint.textContent = parts.join(',');
})
.catch(function () { fetchBtn.disabled = false; if (hint) hint.textContent = '网络错误'; });
});
}
/* ---------- 图标:上传文件 / 自定义地址 ---------- */
var fileInput = $('#f-icon-file');
if (fileInput) {
fileInput.addEventListener('change', function () {
var f = fileInput.files && fileInput.files[0];
if (!f) return;
var auto = $('#f-icon-auto');
if (auto) auto.checked = false; // 一旦自定义,自动取消「允许自动更新」
if (window.URL && URL.createObjectURL) { setPreview(URL.createObjectURL(f)); }
var hint = $('#fetch-hint');
if (hint) hint.textContent = '已选择本地图标:' + f.name + '(保存后生效,并将锁定不再自动更新)';
});
}
var urlInput = $('#f-icon-url');
if (urlInput) {
urlInput.addEventListener('change', function () {
var v = urlInput.value.trim();
if (!v) return;
var auto = $('#f-icon-auto');
if (auto) auto.checked = false;
setPreview(v);
});
}
var autoBox = $('#f-icon-auto');
if (autoBox) {
autoBox.addEventListener('change', function () {
var hint = $('#fetch-hint');
if (!hint) return;
hint.textContent = autoBox.checked
? '已开启自动更新:该网站图标会在「全部更新」时刷新'
: '已关闭自动更新:该网站图标将被锁定';
});
}
/* ---------- 清空表单 ---------- */
var resetBtn = $('#btn-reset');
if (resetBtn) {
resetBtn.addEventListener('click', function () {
['f-id', 'f-url', 'f-name', 'f-short', 'f-icon', 'f-icon-url'].forEach(function (id) {
var el = $('#' + id); if (el) el.value = id === 'f-id' ? '0' : '';
});
var fi = $('#f-icon-file'); if (fi) fi.value = '';
var v = $('#f-visible'); if (v) v.checked = true;
var a = $('#f-icon-auto'); if (a) a.checked = true;
var h = $('#fetch-hint'); if (h) h.textContent = '';
setPreview('');
});
}
/* ---------- 编辑网站:回填表单 ---------- */
$$('.edit-site').forEach(function (btn) {
btn.addEventListener('click', function () {
var d = btn.dataset;
$('#f-id').value = d.id;
$('#f-group').value = d.group;
$('#f-url').value = d.url;
$('#f-name').value = d.name;
$('#f-short').value = d.short;
$('#f-icon').value = d.icon;
if ($('#f-icon-url')) $('#f-icon-url').value = '';
if ($('#f-icon-file')) $('#f-icon-file').value = '';
if ($('#f-icon-auto')) $('#f-icon-auto').checked = d.custom !== '1';
$('#f-visible').checked = d.visible === '1';
setPreview(d.iconUrl || '');
var hint = $('#fetch-hint');
if (hint) hint.textContent = d.custom === '1' ? '该网站图标为自定义,不会被「全部更新」覆盖' : '';
window.scrollTo({ top: 0, behavior: 'smooth' });
});
});
/* ---------- 编辑板块 / 引擎 ---------- */
$$('.edit-group').forEach(function (btn) {
btn.addEventListener('click', function () {
var d = btn.dataset;
$('#g-id').value = d.id; $('#g-name').value = d.name;
$('#g-display').value = d.display; $('#g-visible').checked = d.visible === '1';
window.scrollTo({ top: 0, behavior: 'smooth' });
});
});
var gReset = $('#g-reset');
if (gReset) gReset.addEventListener('click', function () { $('#g-id').value = '0'; $('#g-name').value = ''; $('#g-display').value = 'grid'; $('#g-visible').checked = true; });
$$('.edit-engine').forEach(function (btn) {
btn.addEventListener('click', function () {
var d = btn.dataset;
$('#e-id').value = d.id; $('#e-name').value = d.name; $('#e-slug').value = d.slug;
$('#e-search').value = d.search; $('#e-param').value = d.param; $('#e-sug').value = d.sug;
$('#e-sugtype').value = d.sugtype; $('#e-color').value = d.color; $('#e-active').checked = d.active === '1';
window.scrollTo({ top: 0, behavior: 'smooth' });
});
});
var eReset = $('#e-reset');
if (eReset) eReset.addEventListener('click', function () {
['e-id', 'e-name', 'e-slug', 'e-search', 'e-sug', 'e-color'].forEach(function (id) { var el = $('#' + id); if (el) el.value = id === 'e-id' ? '0' : ''; });
$('#e-param').value = 'q'; $('#e-active').checked = false;
});
/* ---------- 列表内「显示 / 隐藏」原地切换(不跳页) ---------- */
$$('form.js-toggle').forEach(function (form) {
form.addEventListener('submit', function (ev) {
var btn = $('button.toggle', form);
if (btn) btn.disabled = true;
ev.preventDefault();
fetch(form.getAttribute('action'), {
method: 'POST',
body: new FormData(form),
credentials: 'same-origin',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(function (r) { return r.json(); })
.then(function (d) {
if (btn) btn.disabled = false;
if (!d || !d.ok) { alert((d && d.msg) || '切换失败'); return; }
applyToggleState(form, d.state);
})
.catch(function () {
if (btn) btn.disabled = false;
// AJAX 失败时退回普通提交,保证功能可用(表单已带 p,会回到当前页)
var ajax = form.querySelector('input[name="ajax"]');
if (ajax) { ajax.parentNode.removeChild(ajax); }
form.submit();
});
});
});
function applyToggleState(form, state) {
var on = state === 1;
var btn = $('button.toggle', form);
if (btn) {
btn.classList.toggle('on', on);
btn.setAttribute('data-on', on ? '1' : '0');
btn.setAttribute('title', on ? '当前:显示中(点击隐藏)' : '当前:已隐藏(点击显示)');
var svg = btn.querySelector('path[stroke]');
var circle = btn.querySelector('circle');
if (on) {
if (svg) svg.remove();
} else if (circle && !svg) {
var p = document.createElementNS('http://www.w3.org/2000/svg', 'path');
p.setAttribute('d', 'M3 3l18 18');
p.setAttribute('stroke', 'currentColor');
p.setAttribute('stroke-width', '2');
p.setAttribute('stroke-linecap', 'round');
btn.querySelector('svg').appendChild(p);
}
}
// 同行的状态文字(如搜索引擎的「已显示 / 已隐藏」)
var cell = form.closest('td') || form.parentNode;
var st = cell ? $('.js-state', cell) : null;
if (st) {
st.textContent = on ? (st.dataset.onText || '已显示') : (st.dataset.offText || '已隐藏');
st.classList.toggle('on', on);
st.classList.toggle('off', !on);
}
var row = form.closest('tr');
if (row) {
row.classList.toggle('row-off', !on);
}
}
/* ---------- 网站列表:板块标签页 + 拖拽排序 ---------- */
initSitesTabs();
initDragSort();
/**
* 板块标签页:点标签切换对应面板。
* 也支持 URL 锚点 `#sgroup-<gid>`(上移/下移按钮提交后回跳时用),
* 以及无 hash 时默认激活第一个。
*/
function initSitesTabs() {
var wrap = $('#sites-tabs');
if (!wrap) return;
var tabs = $$('.tab', wrap);
var panels = $$('.sites-panel');
function activate(targetId) {
var found = false;
panels.forEach(function (p) {
var on = p.id === targetId;
p.classList.toggle('active', on);
if (on) found = true;
});
if (!found) return false;
tabs.forEach(function (t) {
var on = t.getAttribute('data-target') === targetId;
t.classList.toggle('active', on);
t.setAttribute('aria-selected', on ? 'true' : 'false');
});
return true;
}
tabs.forEach(function (t) {
t.addEventListener('click', function () { activate(t.getAttribute('data-target')); });
});
// 载入时:优先用 hash(上移/下移回来时定位到原板块),否则保持服务端渲染的第一个
var hash = (location.hash || '').replace(/^#/, '');
if (hash) { activate(hash); }
}
/**
* 拖拽排序:按住某一行拖动即可改顺序,松手即保存。
* 用于「网站列表」(每板块一个 tbody)与「板块列表」(整表一个 tbody)。
*
* 实现要点:
* · 用**原生 HTML5 拖拽**(draggable=true + dragstart/dragover/drop),
* 不引任何第三方库(本项目零依赖);
* · 拖动过程中实时把被拖行**移动到光标位置**(真·所见即所得),
* 而不是只画一条插入线 —— 个人导航站的列表不长,实时重排体验更直观;
* · 网站列表**只允许在本板块的 tbody 内拖动**:跨板块拖会被忽略(拖到别的板块前先改「板块」再排);
* · 松手后收集该 tbody 的 id 顺序 → POST 给对应 action(tbody 上的 data-sort-action)
* → 服务端整组归一化。
*/
function initDragSort() {
var bodies = $$('tbody[data-sortable]');
if (!bodies.length) return;
var wrap = $('#sites-tabs');
var csrf = (document.querySelector('input[name="csrf"]') || {}).value || '';
bodies.forEach(function (body) {
var dragRow = null;
var action = body.getAttribute('data-sort-action') || 'site_reorder';
var gid = body.getAttribute('data-group-id') || '';
// 落库地址:优先 tbody 自带;网站列表没有时回退到标签容器上的
var url = body.getAttribute('data-reorder-url')
|| (wrap ? wrap.getAttribute('data-reorder-url') : '');
body.addEventListener('dragstart', function (ev) {
var row = ev.target.closest ? ev.target.closest('tr') : null;
if (!row || row.parentNode !== body) { ev.preventDefault(); return; }
dragRow = row;
row.classList.add('dragging');
try {
ev.dataTransfer.effectAllowed = 'move';
ev.dataTransfer.setData('text/plain', row.getAttribute('data-id') || '');
} catch (e) { /* 某些浏览器不允许写 dataTransfer,忽略 */ }
});
body.addEventListener('dragover', function (ev) {
if (!dragRow) return;
ev.preventDefault();
if (ev.dataTransfer) { ev.dataTransfer.dropEffect = 'move'; }
// 找到光标下最近的一行,把被拖行插到它前面/后面
var after = rowAfterPoint(body, ev.clientY);
if (after === null) {
body.appendChild(dragRow);
} else if (after !== dragRow) {
body.insertBefore(dragRow, after);
}
});
body.addEventListener('drop', function (ev) { ev.preventDefault(); });
body.addEventListener('dragend', function () {
if (!dragRow) return;
dragRow.classList.remove('dragging');
dragRow = null;
saveOrder(body, action, gid, url);
});
});
/** 返回「光标位置之后的第一行」;光标在最后一行下方时返回 null(表示追加到末尾) */
function rowAfterPoint(body, y) {
var rows = $$('tr', body);
for (var i = 0; i < rows.length; i++) {
var r = rows[i];
var box = r.getBoundingClientRect();
if (r.classList.contains('dragging')) continue;
if (y < box.top + box.height / 2) return r;
}
return null;
}
/** 收集顺序并提交;成功后更新状态提示(action/gid/url 来自所属 tbody 的 data-*) */
function saveOrder(body, action, gid, url) {
if (!url) return;
var ids = $$('tr', body).map(function (r) { return r.getAttribute('data-id'); }).filter(Boolean);
var scope = body.closest('.sites-panel') || body.closest('.card') || document;
var status = $('.sort-status', scope);
var fd = new FormData();
fd.append('action', action);
if (gid) { fd.append('group_id', gid); }
fd.append('order', ids.join(','));
fd.append('ajax', '1');
fd.append('csrf', csrf);
if (status) status.textContent = '正在保存顺序…';
fetch(url, { method: 'POST', body: fd, credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then(function (r) { return r.json(); })
.then(function (d) {
if (!d || !d.ok) {
if (status) status.textContent = '保存失败:' + ((d && d.msg) || '未知错误') + ',请刷新后重试';
return;
}
if (status) status.textContent = '顺序已保存(' + (d.count || ids.length) + ' 项)';
})
.catch(function () {
if (status) status.textContent = '保存失败(网络错误),请刷新后重试';
});
}
}
/* ---------- 一键「全部更新」(分批循环) ---------- */
var upBtn = $('#btn-update-all');
if (upBtn) {
upBtn.addEventListener('click', function () {
if (!confirm('将批量刷新所有网站的名称与自动图标。\n用户自定义简称不受影响;标记为「自定义」的图标会被跳过。\n确定继续?')) return;
var base = upBtn.getAttribute('data-url');
var total = 0, updated = 0, failed = 0, skipped = 0, offset = 0;
upBtn.disabled = true;
function step() {
fetch(base + '?offset=' + offset + '&limit=3', { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (d) {
total = d.total; updated += d.updated; failed += d.failed; skipped += (d.skipped || 0); offset = d.offset;
upBtn.textContent = '⟳ 更新中 ' + Math.min(offset, total) + '/' + total;
if (!d.done) { step(); }
else {
upBtn.disabled = false;
upBtn.textContent = '⟳ 全部更新(名称 + 自动图标)';
alert('更新完成:\n成功 ' + updated + ' 个\n跳过(自定义图标)' + skipped + ' 个\n失败/无变化 ' + failed + ' 个\n共 ' + total + ' 个网站。');
location.reload();
}
})
.catch(function () { upBtn.disabled = false; upBtn.textContent = '⟳ 全部更新(名称 + 自动图标)'; alert('更新中断,请重试。'); });
}
step();
});
}
})();