small-packages/luci-app-passwall/luasrc/view/passwall/rule/rule_version.htm
2026-05-10 09:48:30 +08:00

339 lines
10 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<%
local api = require "luci.passwall.api"
local fs = api.fs
local has_old_geoip = fs.access("/tmp/bak_v2ray/geoip.dat")
local has_old_geosite = fs.access("/tmp/bak_v2ray/geosite.dat")
-%>
<script src="<%=resource%>/view/<%=api.appname%>/Sortable.min.js?v=1.15.7"></script>
<style>
table .cbi-button-up,
table .cbi-button-down,
.td.cbi-section-actions .cbi-button-up,
.td.cbi-section-actions .cbi-button-down {
display: none !important;
}
div.cbi-value[id$="-gfwlist_update"],
div.cbi-value[id$="-chnroute_update"],
div.cbi-value[id$="-chnroute6_update"],
div.cbi-value[id$="-chnlist_update"],
div.cbi-value[id$="-geoip_update"],
div.cbi-value[id$="-geosite_update"] {
display: none !important;
}
.drag-handle {
vertical-align: middle;
cursor: grab !important;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 100;
padding: 0 !important;
line-height: inherit;
user-select: none;
align-self: stretch;
background-color: transparent;
}
.drag-handle:hover {
background: transparent;
}
.dragging-row {
background-color: rgba(131, 191, 255, 0.7) !important;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
</style>
<div class="cbi-value" id="_rule_div">
<label class="cbi-value-title">
<%:Update Options%>
</label>
<div class="cbi-value-field">
<div>
<label>
<input class="cbi-input-checkbox" type="checkbox" name="gfwlist" value="1" />
gfwlist
</label>
<label>
<input class="cbi-input-checkbox" type="checkbox" name="chnroute" value="1" />
chnroute
</label>
<label>
<input class="cbi-input-checkbox" type="checkbox" name="chnroute6" value="1" />
chnroute6
</label>
<label>
<input class="cbi-input-checkbox" type="checkbox" name="chnlist" value="1" />
chnlist
</label>
<label>
<input class="cbi-input-checkbox" type="checkbox" name="geoip" value="1" />
geoip
</label>
<label>
<input class="cbi-input-checkbox" type="checkbox" name="geosite" value="1" />
geosite
</label>
<br><br>
<input class="btn cbi-button cbi-button-apply" type="button" id="update_rules_btn" onclick="update_rules(this)" value="<%:Manually update%>" />
<% if has_old_geoip then %>
<input class="btn cbi-button cbi-button-reset" type="button" onclick="rollback_rules(this, 'geoip')" value="<%:Rollback%> GeoIP" />
<% end %>
<% if has_old_geosite then %>
<input class="btn cbi-button cbi-button-reset" type="button" onclick="rollback_rules(this, 'geosite')" value="<%:Rollback%> Geosite" />
<% end %>
</div>
</div>
</div>
<script type="text/javascript">
//<![CDATA[
document.addEventListener('DOMContentLoaded', function () {
const flags = [
"gfwlist_update","chnroute_update","chnroute6_update",
"chnlist_update","geoip_update","geosite_update"
];
const bindFlags = () => {
let allBound = true;
flags.forEach(flag => {
const orig = Array.from(document.querySelectorAll(`input[name$=".${flag}"]`)).find(i => i.type === 'checkbox');
if (!orig) { allBound = false; return; }
// 隐藏最外层 div
const wrapper = orig.closest('.cbi-value');
if (wrapper && wrapper.style.display !== 'none') {
wrapper.style.display = 'none';
}
const custom = document.querySelector(`.cbi-input-checkbox[name="${flag.replace('_update','')}"]`);
if (!custom) { allBound = false; return; }
custom.checked = orig.checked;
// 自定义选择框与原生Flag双向绑定
if (!custom._binded) {
custom._binded = true;
orig.addEventListener('change', () => {
custom.checked = orig.checked;
});
custom.addEventListener('change', () => {
orig.checked = custom.checked;
orig.dispatchEvent(new Event('change', { bubbles: true }));
});
}
});
return allBound;
};
const geo2ruleFlag = () => {
const geoEl = document.querySelector('input[type="checkbox"][name*="passwall"][name*="geo2rule"]');
if (!geoEl) return;
const updateCheckboxes = (disabled) => {
flags.filter(flag => !flag.includes('geo')).forEach(flag => {
const custom = document.querySelector(`.cbi-input-checkbox[name="${flag.replace('_update','')}"]`);
if (custom) {
//if (disabled) custom.checked = true;
custom.parentElement.style.display = disabled ? 'none' : '';
}
});
};
updateCheckboxes(geoEl.checked);
geoEl.addEventListener("change", () => {
geoEl.blur();
updateCheckboxes(geoEl.checked);
});
};
const bindAll = () => {
bindFlags();
geo2ruleFlag();
};
const target = document.querySelector('form') || document.body;
const observer = new MutationObserver(() => bindAll() ? observer.disconnect() : 0);
observer.observe(target, { childList: true, subtree: true });
const timer = setInterval(() => bindAll() ? (clearInterval(timer), observer.disconnect()) : 0, 300);
setTimeout(() => { clearInterval(timer); observer.disconnect(); }, 5000);
});
function update_rules(btn) {
btn.disabled = true;
btn.value = '<%:Updating...%>';
var div = document.getElementById('_rule_div');
var domList = div.getElementsByTagName('input');
var checkBoxList = [];
var len = domList.length;
while(len--) {
var dom = domList[len];  
if(dom.type == 'checkbox' && dom.checked) {
checkBoxList.push(dom.name);
}
}
XHR.get('<%=api.url("update_rules")%>', {
update: checkBoxList.join(",")
},
function(x, data) {
if(x && x.status == 200) {
window.location.href = '<%=api.url("log")%>';
} else {
alert("<%:Error%>");
btn.disabled = false;
btn.value = '<%:Manually update%>';
}
}
);
}
function rollback_rules(btn, type) {
let ori_val = btn.value;
btn.disabled = true;
btn.value = '<%:Rollbacking...%>';
var div = document.getElementById('_rule_div');
var domList = div.getElementsByTagName('input');
var checkBoxList = [];
var len = domList.length;
while(len--) {
var dom = domList[len];  
if(dom.type == 'checkbox' && dom.checked) {
if(type == 'geoip' && (dom.name === 'chnroute' || dom.name === 'chnroute6')) {
checkBoxList.push(dom.name);
}
if(type == 'geosite' && (dom.name === 'gfwlist' || dom.name === 'chnlist')) {
checkBoxList.push(dom.name);
}
}
}
XHR.get('<%=api.url("rollback_rules")%>', {
type: type,
rules: checkBoxList.join(",")
},
function(x, data) {
if(x && x.status == 200 && data.code == 1) {
alert("<%:Success%>");
btn.style.display = "none";
} else {
alert("<%:Error%>");
btn.disabled = false;
btn.value = ori_val;
}
}
);
}
//分流规则添加拖拽排序
document.addEventListener("DOMContentLoaded", function () {
function initSortableForTable() {
var section = document.getElementById("cbi-<%=api.appname%>-shunt_rules");
if (!section) return;
hideSortColumn(section);
// === 插入 drag handle ===
var rows = section.querySelectorAll("tr");
rows.forEach(function(row) {
var btn = row.querySelector(".cbi-button-remove:last-of-type");
if (!btn) return;
if (btn.nextElementSibling && btn.nextElementSibling.classList.contains("drag-handle")) return;
var handle = document.createElement("span");
handle.className = "drag-handle center";
handle.title = "<%:Drag to reorder%>";
handle.innerHTML = "⠿";
btn.after(handle);
});
// === 初始化 Sortable ===
var table = section.getElementsByTagName("table")[0];
if (!table) return;
var root = table.tBodies[0] || table;
if (root._sortable_initialized) return root._sortable_instance;
root._sortable_initialized = true;
// 保存原始顺序
root._origOrder = getCurrentOrder(root);
try {
root._sortable_instance = Sortable.create(root, {
handle: ".drag-handle",
draggable: "tr.cbi-section-table-row",
animation: 150,
ghostClass: "dragging-row",
fallbackOnBody: true,
forceFallback: false,
swapThreshold: 0.65,
onEnd: function (evt) {
updateHiddenInput(root, section);
}
});
return root._sortable_instance;
} catch (e) {
root._sortable_initialized = false;
console.error("Sortable init failed:", e);
}
}
// 获取 table 当前行顺序
function getCurrentOrder(root) {
var order = [];
var rows = root.querySelectorAll("tr.cbi-section-table-row");
rows.forEach(function (tr) {
var id = tr.id || "";
if (id.startsWith("cbi-<%=api.appname%>-"))
id = id.replace("cbi-<%=api.appname%>-", "");
order.push(id);
});
return order;
}
// 拖拽完成后更新 hidden input
function updateHiddenInput(root, section) {
var newOrder = getCurrentOrder(root);
var changed = newOrder.join(" ") !== root._origOrder.join(" ");
var hiddenInput = section.querySelector('input[type="hidden"][id^="cbi.sts."]');
if (hiddenInput) {
hiddenInput.value = changed ? newOrder.join(" ") : "";
}
}
// 隐藏18.06 up/down 列
function hideSortColumn(section) {
var table = section.getElementsByTagName("table")[0];
if(!table) return;
var rows = table.querySelectorAll("tr.cbi-section-table-row");
if(!rows.length) return;
var colCount = rows[0].children.length, sortCol = -1;
for(var col=0; col<colCount; col++){
var hasSort=false, invalid=false;
rows.forEach(function(tr){
var td=tr.children[col]; if(!td) return;
if(td.querySelector(".cbi-button-edit, .cbi-button-remove")) invalid=true;
if(td.querySelector(".cbi-button-up, .cbi-button-down")) hasSort=true;
});
if(!invalid && hasSort){ sortCol=col; break; }
}
if(sortCol===-1) return;
table.querySelectorAll("tr").forEach(function(tr){
var c=tr.children[sortCol]; if(c) c.style.display="none";
});
}
// === 等待 TypedSection 行稳定 ===
(function waitStable() {
var last = 0, stable = 0;
var THRESHOLD = 5;
function tick() {
var count = document.querySelectorAll("tr.cbi-section-table-row").length;
if (count && count === last) stable++;
else stable = 0;
last = count;
if (stable >= THRESHOLD)
setTimeout(initSortableForTable, 200);
else
requestAnimationFrame(tick);
}
tick();
})();
});
//]]>
</script>