mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-07-27 08:31:44 +08:00
update 2026-07-16 21:39:02
This commit is contained in:
parent
adc73c928c
commit
8c5da3fb13
@ -5,12 +5,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=dae
|
||||
PKG_VERSION:=2026.07.13
|
||||
PKG_VERSION:=2026.07.16
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_VERSION:=f303e94cebeda73de63a5ab00652009a12f2384e
|
||||
PKG_SOURCE_VERSION:=eee7c94db84bca2a0384c7bb5d4f6d7d8a539ec7
|
||||
PKG_SOURCE_URL:=https://github.com/olicesx/dae.git
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
|
||||
@ -15,7 +15,8 @@ o.rmempty = false
|
||||
o.default = false
|
||||
|
||||
-- [[ ACLs Settings ]]--
|
||||
s = m:section(TypedSection, "acl_rule")
|
||||
local cfgname = "acl_rule"
|
||||
s = m:section(TypedSection, cfgname)
|
||||
s.template = "cbi/tblsection"
|
||||
s.sortable = true
|
||||
s.anonymous = true
|
||||
@ -103,4 +104,9 @@ o:value("1:65535", translate("All"))
|
||||
o:value("53", "53")
|
||||
]]--
|
||||
|
||||
local sortable = Template(appname .. "/cbi/sortable")
|
||||
sortable.api = api
|
||||
sortable.target_cfgname = cfgname
|
||||
m:append(sortable)
|
||||
|
||||
return api.return_map(m)
|
||||
|
||||
@ -103,7 +103,8 @@ end
|
||||
o:depends("health_check_type", "passwall_logic")
|
||||
|
||||
-- [[ Balancing Settings ]]--
|
||||
s = m:section(TypedSection, "haproxy_config", translate("Node List"))
|
||||
local cfgname = "haproxy_config"
|
||||
s = m:section(TypedSection, cfgname, translate("Node List"))
|
||||
s.description = "<font color='red'>" ..
|
||||
translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") .. "<br>" ..
|
||||
translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") .. "</font>"
|
||||
@ -179,6 +180,11 @@ o:value(0, translate("Primary"))
|
||||
o:value(1, translate("Standby"))
|
||||
o.rmempty = false
|
||||
|
||||
local sortable = Template(appname .. "/cbi/sortable")
|
||||
sortable.api = api
|
||||
sortable.target_cfgname = cfgname
|
||||
m:append(sortable)
|
||||
|
||||
m:append(Template(appname .. "/haproxy/js"))
|
||||
|
||||
return api.return_map(m)
|
||||
|
||||
@ -129,7 +129,8 @@ o.cfgvalue = function(self, section)
|
||||
translate("Manual subscription All"))
|
||||
end
|
||||
|
||||
s = m:section(TypedSection, "subscribe_list", "", "<font color='red'>" .. translate("When adding a new subscription, please save and apply before manually subscribing. If you only change the subscription URL, you can subscribe manually, and the system will save it automatically.") .. "</font>")
|
||||
local cfgname = "subscribe_list"
|
||||
s = m:section(TypedSection, cfgname, "", "<font color='red'>" .. translate("When adding a new subscription, please save and apply before manually subscribing. If you only change the subscription URL, you can subscribe manually, and the system will save it automatically.") .. "</font>")
|
||||
s.addremove = true
|
||||
s.anonymous = true
|
||||
s.sortable = true
|
||||
@ -150,7 +151,7 @@ o.validate = function(self, value, section)
|
||||
return nil, translate("Remark cannot be empty.")
|
||||
end
|
||||
local duplicate = false
|
||||
m.uci:foreach(appname, "subscribe_list", function(e)
|
||||
m.uci:foreach(appname, cfgname, function(e)
|
||||
if e[".name"] ~= section and e["remark"] and e["remark"]:lower() == value:lower() then
|
||||
duplicate = true
|
||||
return false
|
||||
@ -225,6 +226,11 @@ o.cfgvalue = function(self, section)
|
||||
section, translate("Manual subscription"))
|
||||
end
|
||||
|
||||
local sortable = Template(appname .. "/cbi/sortable")
|
||||
sortable.api = api
|
||||
sortable.target_cfgname = cfgname
|
||||
m:append(sortable)
|
||||
|
||||
m:append(Template(appname .. "/node_subscribe/js"))
|
||||
|
||||
return api.return_map(m)
|
||||
|
||||
@ -142,8 +142,10 @@ end
|
||||
|
||||
s:append(Template(appname .. "/rule/rule_version"))
|
||||
|
||||
local cfgname = "shunt_rules"
|
||||
|
||||
if has_xray or has_singbox then
|
||||
s = m:section(TypedSection, "shunt_rules", "Sing-Box/Xray " .. translate("Shunt Rule"), "<a style='color: red'>" .. translate("Please note attention to the priority, the higher the order, the higher the priority.") .. "</a>")
|
||||
s = m:section(TypedSection, cfgname, "Sing-Box/Xray " .. translate("Shunt Rule"), "<a style='color: red'>" .. translate("Please note attention to the priority, the higher the order, the higher the priority.") .. "</a>")
|
||||
s.template = "cbi/tblsection"
|
||||
s.anonymous = false
|
||||
s.addremove = true
|
||||
@ -165,4 +167,9 @@ if has_xray or has_singbox then
|
||||
o = s:option(DummyValue, "remarks", translate("Remarks"))
|
||||
end
|
||||
|
||||
local sortable = Template(appname .. "/cbi/sortable")
|
||||
sortable.api = api
|
||||
sortable.target_cfgname = cfgname
|
||||
m:append(sortable)
|
||||
|
||||
return api.return_map(m)
|
||||
|
||||
@ -13,7 +13,8 @@ t.addremove = false
|
||||
e = t:option(Flag, "enable", translate("Enable"))
|
||||
e.rmempty = false
|
||||
|
||||
t = m:section(TypedSection, "user", translate("Users Manager"))
|
||||
local cfgname = "user"
|
||||
t = m:section(TypedSection, cfgname, translate("Users Manager"))
|
||||
t.anonymous = true
|
||||
t.addremove = true
|
||||
t.sortable = true
|
||||
@ -88,6 +89,12 @@ e = t:option(Flag, "log", translate("Log"))
|
||||
e.default = "1"
|
||||
e.rmempty = false
|
||||
|
||||
local sortable = Template(appname .. "/cbi/sortable")
|
||||
sortable.api = api
|
||||
sortable.appname = m.config
|
||||
sortable.target_cfgname = cfgname
|
||||
m:append(sortable)
|
||||
|
||||
m:append(Template(appname .. "/server/log"))
|
||||
|
||||
m:append(Template(appname .. "/server/users_list_status"))
|
||||
|
||||
@ -403,7 +403,7 @@
|
||||
function lv_expandGroupOfCurrent(cbid, listContainer, hiddenSelect) {
|
||||
const key = hiddenSelect.options[0].value;
|
||||
if (!key) return;
|
||||
const targetLi = listContainer.querySelector("li[data-key='" + key.replace(/'/g,"\\'") + "']");
|
||||
const targetLi = listContainer.querySelector(`li[data-key='${CSS.escape(key)}']`);
|
||||
if (!targetLi) return;
|
||||
let parentGroup = targetLi.closest(".lv-group");
|
||||
if (parentGroup) {
|
||||
|
||||
@ -402,7 +402,7 @@
|
||||
function v_expandGroupOfCurrent(cbid, listContainer, hiddenInput) {
|
||||
const key = hiddenInput.value;
|
||||
if (!key) return;
|
||||
const targetLi = listContainer.querySelector("li[data-key='" + key.replace(/'/g,"\\'") + "']");
|
||||
const targetLi = listContainer.querySelector(`li[data-key='${CSS.escape(key)}']`);
|
||||
if (!targetLi) return;
|
||||
let parentGroup = targetLi.closest(".v-group");
|
||||
if (parentGroup) {
|
||||
|
||||
159
luci-app-passwall/luasrc/view/passwall/cbi/sortable.htm
Normal file
159
luci-app-passwall/luasrc/view/passwall/cbi/sortable.htm
Normal file
@ -0,0 +1,159 @@
|
||||
<%
|
||||
local api = self.api
|
||||
local appname = self.appname or api.appname
|
||||
local target_cfgname = self.target_cfgname
|
||||
-%>
|
||||
<script src="<%=resource%>/view/<%=api.appname%>/Sortable.min.js"></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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1152px) {
|
||||
#cbi-<%=appname%>-<%=target_cfgname%> .cbi-section-table-titles {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
//<![CDATA[
|
||||
//Add drag-and-drop sorting
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
function initSortableForTable() {
|
||||
var section = document.getElementById("cbi-<%=appname%>-<%=target_cfgname%>");
|
||||
if (!section) return;
|
||||
|
||||
// === insert 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);
|
||||
});
|
||||
|
||||
// === init 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;
|
||||
|
||||
// save origin order
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// get table current line order
|
||||
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-<%=appname%>-"))
|
||||
id = id.replace("cbi-<%=appname%>-", "");
|
||||
order.push(id);
|
||||
});
|
||||
return order;
|
||||
}
|
||||
|
||||
// update after drag and drop 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(" ") : "";
|
||||
}
|
||||
}
|
||||
|
||||
// hide lua luci version 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";
|
||||
});
|
||||
}
|
||||
|
||||
// === wait 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>
|
||||
@ -2,46 +2,8 @@
|
||||
local api = require "luci.passwall.api"
|
||||
local appname = api.appname
|
||||
-%>
|
||||
<script src="<%=resource%>/view/<%=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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1152px) {
|
||||
#cbi-<%=appname%>-haproxy_config .cbi-section-table-titles {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1152px) {
|
||||
#cbi-<%=appname%>-haproxy_config input[id*=".<%=appname%>."][id$=".haproxy_port"],
|
||||
#cbi-<%=appname%>-haproxy_config input[id*=".<%=appname%>."][id$=".lbweight"] {
|
||||
@ -110,129 +72,5 @@ table .cbi-button-down,
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
|
||||
//节点列表添加拖拽排序
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
function initSortableForTable() {
|
||||
var section = document.getElementById("cbi-<%=appname%>-haproxy_config");
|
||||
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-<%=appname%>-"))
|
||||
id = id.replace("cbi-<%=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.querySelector("table");
|
||||
if (!table) return;
|
||||
var ths = Array.prototype.slice.call(table.querySelectorAll("tr.cbi-section-table-titles > th"));
|
||||
var dataRows = table.querySelectorAll("tr.cbi-section-table-row");
|
||||
if (!ths.length || !dataRows.length) return;
|
||||
var sortCol = -1;
|
||||
for (var i = 0; i < ths.length; i++) {
|
||||
var hasSort = false, invalid = false;
|
||||
dataRows.forEach(function(tr) {
|
||||
var td = tr.querySelectorAll(":scope > td")[i];
|
||||
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 = i; break; }
|
||||
}
|
||||
if (sortCol === -1) return;
|
||||
var rows = [table.querySelector("tr.cbi-section-table-titles")].concat(
|
||||
Array.prototype.slice.call(dataRows),
|
||||
Array.prototype.slice.call(table.querySelectorAll("tr.cbi-section-table-descr"))
|
||||
);
|
||||
rows.forEach(function(tr) {
|
||||
var cells = Array.prototype.filter.call(tr.children, function(el) {
|
||||
return el.tagName === "TH" || el.tagName === "TD";
|
||||
});
|
||||
if (cells[sortCol]) cells[sortCol].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>
|
||||
|
||||
@ -1,46 +1,6 @@
|
||||
<%
|
||||
local api = require "luci.passwall.api"
|
||||
-%>
|
||||
<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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1152px) {
|
||||
#cbi-<%=api.appname%>-subscribe_list .cbi-section-table-titles {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
@ -148,129 +108,5 @@ table .cbi-button-down,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//订阅列表添加拖拽排序
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
function initSortableForTable() {
|
||||
var section = document.getElementById("cbi-<%=api.appname%>-subscribe_list");
|
||||
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.querySelector("table");
|
||||
if (!table) return;
|
||||
var ths = Array.prototype.slice.call(table.querySelectorAll("tr.cbi-section-table-titles > th"));
|
||||
var dataRows = table.querySelectorAll("tr.cbi-section-table-row");
|
||||
if (!ths.length || !dataRows.length) return;
|
||||
var sortCol = -1;
|
||||
for (var i = 0; i < ths.length; i++) {
|
||||
var hasSort = false, invalid = false;
|
||||
dataRows.forEach(function(tr) {
|
||||
var td = tr.querySelectorAll(":scope > td")[i];
|
||||
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 = i; break; }
|
||||
}
|
||||
if (sortCol === -1) return;
|
||||
var rows = [table.querySelector("tr.cbi-section-table-titles")].concat(
|
||||
Array.prototype.slice.call(dataRows),
|
||||
Array.prototype.slice.call(table.querySelectorAll("tr.cbi-section-table-descr"))
|
||||
);
|
||||
rows.forEach(function(tr) {
|
||||
var cells = Array.prototype.filter.call(tr.children, function(el) {
|
||||
return el.tagName === "TH" || el.tagName === "TD";
|
||||
});
|
||||
if (cells[sortCol]) cells[sortCol].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>
|
||||
|
||||
@ -4,16 +4,8 @@ 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"],
|
||||
@ -22,30 +14,6 @@ local has_old_geosite = fs.access("/tmp/bak_v2ray/geosite.dat")
|
||||
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">
|
||||
@ -219,120 +187,5 @@ local has_old_geosite = fs.access("/tmp/bak_v2ray/geosite.dat")
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
//分流规则添加拖拽排序
|
||||
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>
|
||||
|
||||
@ -403,7 +403,7 @@
|
||||
function lv_expandGroupOfCurrent(cbid, listContainer, hiddenSelect) {
|
||||
const key = hiddenSelect.options[0].value;
|
||||
if (!key) return;
|
||||
const targetLi = listContainer.querySelector("li[data-key='" + key.replace(/'/g,"\\'") + "']");
|
||||
const targetLi = listContainer.querySelector(`li[data-key='${CSS.escape(key)}']`);
|
||||
if (!targetLi) return;
|
||||
let parentGroup = targetLi.closest(".lv-group");
|
||||
if (parentGroup) {
|
||||
|
||||
@ -402,7 +402,7 @@
|
||||
function v_expandGroupOfCurrent(cbid, listContainer, hiddenInput) {
|
||||
const key = hiddenInput.value;
|
||||
if (!key) return;
|
||||
const targetLi = listContainer.querySelector("li[data-key='" + key.replace(/'/g,"\\'") + "']");
|
||||
const targetLi = listContainer.querySelector(`li[data-key='${CSS.escape(key)}']`);
|
||||
if (!targetLi) return;
|
||||
let parentGroup = targetLi.closest(".v-group");
|
||||
if (parentGroup) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user