update 2026-07-16 21:39:02

This commit is contained in:
action 2026-07-16 21:39:02 +08:00
parent adc73c928c
commit 8c5da3fb13
14 changed files with 203 additions and 485 deletions

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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"))

View File

@ -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) {

View File

@ -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) {

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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) {

View File

@ -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) {