菜单修改优化

This commit is contained in:
supernijia
2026-07-28 13:40:19 +08:00
parent 4ddb8b47b0
commit 7c3c9e53a0
90 changed files with 9388 additions and 1545 deletions
+345 -211
View File
@@ -382,115 +382,322 @@
});
}
var allColumnsList = [];
var columnPermissionCatalogReady = false;
var columnCardsContainerMap = { createColumnPermissionWrap: 'createColumnCards', editColumnPermissionWrap: 'editColumnCards' };
// Hierarchical permission editor: only direct IDs are submitted; descendants are visual inheritance.
function columnParentId(item) {
var value = item && item.parent_id != null ? item.parent_id : item && item.parentId;
var parsed = value == null || value === '' ? null : Number(value);
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}
function columnId(item) { return Number(item && item.id); }
function columnMenuType(item) {
return String(item && item.menu_type || 'app').toLowerCase() === 'admin' ? 'admin' : 'app';
}
function columnDescendantIds(id) {
var result = [], pending = [Number(id)];
while (pending.length) {
var parent = pending.shift();
allColumnsList.forEach(function (item) {
if (columnParentId(item) !== parent) return;
var childId = columnId(item);
if (result.indexOf(childId) < 0) {
result.push(childId);
pending.push(childId);
}
});
}
return result;
}
function columnTreeHasDirectDescendant(id, directIds) {
return columnDescendantIds(id).some(function (descendantId) { return !!directIds[descendantId]; });
}
function columnTreeFullyCovered(id, directIds) {
if (directIds[id]) return true;
var children = allColumnsList.filter(function (item) { return columnParentId(item) === Number(id); });
return children.length > 0 && children.every(function (child) {
return columnTreeFullyCovered(columnId(child), directIds);
});
}
function loadColumnsForPermission() {
var ownedKeys = {};
var ownedRoutes = {};
columnPermissionCatalogReady = false;
var ownedKeys = {}, ownedRoutes = {};
var ownPermissionRequest = (currentUserRole === 'admin' && currentUserId)
? fetch('/api/admin/user/' + currentUserId + '/column-permissions')
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) throw new Error(res.error || '当前用户权限加载失败');
(res.items || []).forEach(function (item) {
if (item.column_key) ownedKeys[String(item.column_key).trim()] = true;
if (item.route_path) ownedRoutes[String(item.route_path).trim()] = true;
});
})
.catch(function () { })
: Promise.resolve();
ownPermissionRequest.then(function () {
return fetch('/api/admin/columns');
}).then(function (r) { return r.json(); })
ownPermissionRequest.then(function () { return fetch('/api/admin/columns'); })
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) return;
allColumnsList = (res.items || []).filter(function (item) {
var availableColumns = (res.items || []).filter(function (item) {
if (item.column_key === 'admin_image_video_task_data') return false;
if (currentUserRole !== 'admin') return true;
return !!ownedKeys[String(item.column_key || '').trim()] ||
!!ownedRoutes[String(item.route_path || '').trim()];
return true;
});
if (currentUserRole === 'admin') {
var columnsById = {}, grantableIds = {}, visibleIds = {};
availableColumns.forEach(function (item) {
var id = columnId(item);
columnsById[id] = item;
if (ownedKeys[String(item.column_key || '').trim()] || ownedRoutes[String(item.route_path || '').trim()]) {
grantableIds[id] = true;
}
});
Object.keys(grantableIds).forEach(function (id) {
var currentId = Number(id), visited = {};
while (currentId > 0 && !visited[currentId] && columnsById[currentId]) {
visited[currentId] = true;
visibleIds[currentId] = true;
currentId = columnParentId(columnsById[currentId]);
}
});
allColumnsList = availableColumns.filter(function (item) {
return !!visibleIds[columnId(item)];
}).map(function (item) {
item._structureOnly = !grantableIds[columnId(item)];
return item;
});
} else {
allColumnsList = availableColumns.map(function (item) {
item._structureOnly = false;
return item;
});
}
columnPermissionCatalogReady = true;
renderColumnPermissionWrap('createColumnPermissionWrap');
renderColumnPermissionWrap('editColumnPermissionWrap');
var selCreate = document.getElementById('createColumnPermissionWrap');
var selEdit = document.getElementById('editColumnPermissionWrap');
if (selCreate && !selCreate._colCardsBound) {
selCreate._colCardsBound = true;
selCreate.onchange = function () { renderColumnCards('createColumnPermissionWrap'); };
}
if (selEdit && !selEdit._colCardsBound) {
selEdit._colCardsBound = true;
selEdit.onchange = function () { renderColumnCards('editColumnPermissionWrap'); };
}
})
.catch(function () { });
populateColumnParentSelects();
}).catch(function () {
columnPermissionCatalogReady = false;
allColumnsList = [];
var editPermissionWrap = document.getElementById('editColumnPermissionWrap');
if (editPermissionWrap) editPermissionWrap._permissionsReady = false;
renderColumnPermissionWrap('createColumnPermissionWrap');
renderColumnPermissionWrap('editColumnPermissionWrap');
});
}
var columnCardsContainerMap = { createColumnPermissionWrap: 'createColumnCards', editColumnPermissionWrap: 'editColumnCards' };
function renderColumnCards(selectId) {
var sel = document.getElementById(selectId);
var cardsId = columnCardsContainerMap[selectId];
var cardsEl = cardsId ? document.getElementById(cardsId) : null;
if (!sel || sel.tagName !== 'SELECT' || !cardsEl) return;
function renderColumnCards(wrapId) {
var wrap = document.getElementById(wrapId);
var cardsEl = document.getElementById(columnCardsContainerMap[wrapId]);
if (!wrap || !cardsEl) return;
var directIds = wrap._directIds || {};
cardsEl.innerHTML = '';
for (var i = 0; i < sel.options.length; i++) {
var opt = sel.options[i];
if (opt.disabled || !opt.selected) continue;
allColumnsList.forEach(function (item) {
var id = columnId(item);
if (!directIds[id] || item._structureOnly) return;
var card = document.createElement('span');
card.className = 'column-permission-card';
card.textContent = opt.textContent;
card.textContent = (item.name || '') + ' (' + (item.column_key || '') + ')';
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'col-card-remove';
btn.setAttribute('aria-label', '移除');
btn.textContent = '×';
(function (option, seldId) {
btn.onclick = function () {
option.selected = false;
renderColumnCards(seldId);
};
})(opt, selectId);
btn.onclick = function () {
delete directIds[id];
renderColumnPermissionWrap(wrapId);
};
card.appendChild(btn);
cardsEl.appendChild(card);
}
});
}
function renderColumnPermissionWrap(wrapId) {
var sel = document.getElementById(wrapId);
if (!sel || sel.tagName !== 'SELECT') return;
sel.innerHTML = '';
allColumnsList.forEach(function (c) {
var opt = document.createElement('option');
opt.value = c.id;
opt.textContent = c.name + ' (' + c.column_key + ')';
sel.appendChild(opt);
});
if (allColumnsList.length === 0) {
var opt = document.createElement('option');
opt.disabled = true;
opt.textContent = '暂无菜单,请先在“栏目权限配置”中新增菜单';
sel.appendChild(opt);
var wrap = document.getElementById(wrapId);
if (!wrap) return;
var directIds = wrap._directIds || {};
var collapsedIds = wrap._collapsedIds || {};
var collapsedMenuTypes = wrap._collapsedMenuTypes || {};
var previousScrollTop = wrap.scrollTop;
wrap._directIds = directIds;
wrap._collapsedIds = collapsedIds;
wrap._collapsedMenuTypes = collapsedMenuTypes;
wrap.innerHTML = '';
if (!allColumnsList.length) {
wrap.textContent = '暂无可分配菜单';
renderColumnCards(wrapId);
return;
}
var byParent = {};
allColumnsList.forEach(function (item) {
var parent = columnParentId(item);
var key = parent == null ? 'root' : String(parent);
(byParent[key] || (byParent[key] = [])).push(item);
});
if (!wrap._collapseStateInitialized) {
allColumnsList.forEach(function (item) {
var id = columnId(item);
if ((byParent[String(id)] || []).length) collapsedIds[id] = true;
});
wrap._collapseStateInitialized = true;
}
if (!wrap._menuTypeCollapseInitialized) {
collapsedMenuTypes.admin = true;
collapsedMenuTypes.app = true;
wrap._menuTypeCollapseInitialized = true;
}
function renderNode(item, depth, inherited) {
var id = columnId(item);
var itemMenuType = columnMenuType(item);
var children = (byParent[String(id)] || []).filter(function (child) {
return columnMenuType(child) === itemMenuType;
});
var structureOnly = item._structureOnly === true;
var effective = inherited || !!directIds[id];
var covered = effective || columnTreeFullyCovered(id, directIds);
var node = document.createElement('div');
node.className = 'column-tree-node';
node.setAttribute('data-depth', String(depth));
var row = document.createElement('div');
row.className = 'column-tree-row';
if (children.length) {
var toggle = document.createElement('button');
var collapsed = !!collapsedIds[id];
toggle.type = 'button';
toggle.className = 'column-tree-toggle';
toggle.setAttribute('aria-expanded', String(!collapsed));
toggle.setAttribute('aria-label', (collapsed ? '展开' : '收起') + (item.name || '菜单'));
toggle.title = collapsed ? '展开子菜单' : '收起子菜单';
toggle.onclick = function () {
if (collapsedIds[id]) delete collapsedIds[id];
else collapsedIds[id] = true;
renderColumnPermissionWrap(wrapId);
};
row.appendChild(toggle);
} else {
var spacer = document.createElement('span');
spacer.className = 'column-tree-toggle-spacer';
spacer.setAttribute('aria-hidden', 'true');
row.appendChild(spacer);
}
var label = document.createElement('label');
var checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = covered;
checkbox.indeterminate = !covered && columnTreeHasDirectDescendant(id, directIds);
checkbox.disabled = inherited || structureOnly;
checkbox.onchange = function () {
if (structureOnly) return;
if (checkbox.checked) {
directIds[id] = true;
columnDescendantIds(id).forEach(function (descendantId) { delete directIds[descendantId]; });
} else {
delete directIds[id];
columnDescendantIds(id).forEach(function (descendantId) { delete directIds[descendantId]; });
}
renderColumnPermissionWrap(wrapId);
};
var text = document.createElement('span');
text.textContent = (item.name || '') + ' (' + (item.column_key || '') + ')';
if (inherited || structureOnly) text.className = 'inherited-label';
label.appendChild(checkbox);
label.appendChild(text);
row.appendChild(label);
node.appendChild(row);
if (children.length) {
var childrenWrap = document.createElement('div');
childrenWrap.className = 'column-tree-children';
childrenWrap.hidden = !!collapsedIds[id];
children.forEach(function (child) {
childrenWrap.appendChild(renderNode(child, depth + 1, effective));
});
node.appendChild(childrenWrap);
}
return node;
}
[
{ key: 'admin', label: '后台菜单' },
{ key: 'app', label: 'APP(软件)菜单' }
].forEach(function (group) {
var groupItems = allColumnsList.filter(function (item) {
return columnMenuType(item) === group.key;
});
if (!groupItems.length) return;
var section = document.createElement('div');
section.className = 'column-tree-group';
var groupToggle = document.createElement('button');
var groupCollapsed = !!collapsedMenuTypes[group.key];
groupToggle.type = 'button';
groupToggle.className = 'column-tree-group-toggle';
groupToggle.setAttribute('aria-expanded', String(!groupCollapsed));
groupToggle.setAttribute('aria-label', (groupCollapsed ? '展开' : '收起') + group.label);
var groupName = document.createElement('span');
groupName.className = 'column-tree-group-name';
groupName.textContent = group.label;
var groupCount = document.createElement('span');
groupCount.className = 'column-tree-group-count';
groupCount.textContent = groupItems.length + ' 项';
groupToggle.appendChild(groupName);
groupToggle.appendChild(groupCount);
groupToggle.onclick = function () {
if (collapsedMenuTypes[group.key]) delete collapsedMenuTypes[group.key];
else collapsedMenuTypes[group.key] = true;
renderColumnPermissionWrap(wrapId);
};
section.appendChild(groupToggle);
var groupBody = document.createElement('div');
groupBody.className = 'column-tree-group-body';
groupBody.hidden = groupCollapsed;
groupItems.filter(function (item) {
var parentId = columnParentId(item);
if (parentId == null) return true;
var parent = allColumnsList.find(function (candidate) {
return columnId(candidate) === parentId;
});
return !parent || columnMenuType(parent) !== group.key;
}).forEach(function (item) {
groupBody.appendChild(renderNode(item, 0, false));
});
section.appendChild(groupBody);
wrap.appendChild(section);
});
renderColumnCards(wrapId);
wrap.scrollTop = previousScrollTop;
}
function getSelectedColumnIds(wrapId) {
var sel = document.getElementById(wrapId);
if (!sel || sel.tagName !== 'SELECT') return [];
var ids = [];
for (var i = 0; i < sel.options.length; i++) {
if (sel.options[i].selected) {
var v = parseInt(sel.options[i].value, 10);
if (!isNaN(v)) ids.push(v);
}
}
return ids;
var wrap = document.getElementById(wrapId);
return wrap && wrap._directIds ? Object.keys(wrap._directIds).map(Number).filter(function (id) {
var item = allColumnsList.find(function (column) { return columnId(column) === id; });
return id > 0 && item && !item._structureOnly;
}) : [];
}
function setColumnPermissionCheckboxes(wrapId, columnIds) {
var sel = document.getElementById(wrapId);
if (!sel || sel.tagName !== 'SELECT') return;
var set = {};
(columnIds || []).forEach(function (id) { set[id] = true; });
for (var i = 0; i < sel.options.length; i++) {
var opt = sel.options[i];
if (opt.disabled) continue;
opt.selected = set[parseInt(opt.value, 10)] || false;
}
renderColumnCards(wrapId);
var wrap = document.getElementById(wrapId);
if (!wrap) return;
wrap._directIds = {};
(columnIds || []).forEach(function (id) {
var parsed = Number(id);
if (Number.isFinite(parsed) && parsed > 0) wrap._directIds[parsed] = true;
});
wrap._permissionsReady = true;
renderColumnPermissionWrap(wrapId);
}
function populateColumnParentSelects() {
['columnParentId', 'editColumnParentId'].forEach(function (selectId) {
var select = document.getElementById(selectId);
if (!select) return;
var current = select.value;
var typeSelect = document.getElementById(selectId === 'editColumnParentId' ? 'editColumnMenuType' : 'columnMenuType');
var menuType = typeSelect ? (typeSelect.value || 'app') : 'app';
var editingId = selectId === 'editColumnParentId' ? Number(document.getElementById('editColumnId').value || 0) : 0;
var blockedIds = editingId > 0 ? [editingId].concat(columnDescendantIds(editingId)) : [];
select.innerHTML = '<option value="">无(根菜单)</option>';
allColumnsList.filter(function (item) {
return !item._structureOnly && String(item.menu_type || 'app') === String(menuType) && blockedIds.indexOf(columnId(item)) < 0;
}).forEach(function (item) {
var option = document.createElement('option');
option.value = item.id;
option.textContent = (item.name || '') + ' (' + (item.column_key || '') + ')';
select.appendChild(option);
});
select.value = current;
});
}
function updateCreateFormByRole() {
var roleSel = document.getElementById('createRole');
@@ -531,14 +738,25 @@
editCreatorName.value = u.creator_username || '';
if (u.role !== 'super_admin') { editRole.value = u.role || 'normal'; }
document.getElementById('msgEdit').textContent = '';
if (allColumnsList.length) {
renderColumnPermissionWrap('editColumnPermissionWrap');
fetch('/api/admin/user/' + (u.id) + '/columns')
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success && res.column_ids) setColumnPermissionCheckboxes('editColumnPermissionWrap', res.column_ids);
});
}
var editPermissionWrap = document.getElementById('editColumnPermissionWrap');
editPermissionWrap._directIds = {};
editPermissionWrap._permissionsReady = false;
renderColumnPermissionWrap('editColumnPermissionWrap');
fetch('/api/admin/user/' + (u.id) + '/columns')
.then(function (r) { return r.json(); })
.then(function (res) {
if (String(document.getElementById('editUserId').value || '') !== String(u.id || '')) return;
if (!res.success || !Array.isArray(res.column_ids)) {
throw new Error(res.error || '权限加载失败');
}
setColumnPermissionCheckboxes('editColumnPermissionWrap', res.column_ids);
})
.catch(function (error) {
if (String(document.getElementById('editUserId').value || '') !== String(u.id || '')) return;
var msgEl = document.getElementById('msgEdit');
msgEl.textContent = error.message || '权限加载失败';
msgEl.className = 'msg err';
});
document.getElementById('editUserModal').classList.add('show');
};
});
@@ -575,6 +793,11 @@
msgEl.classList.add('err');
return;
}
if (!columnPermissionCatalogReady) {
msgEl.textContent = '菜单权限尚未加载,请稍后重试';
msgEl.classList.add('err');
return;
}
var body = { username: username, password: password, role: role };
if (role === 'normal' && currentUserRole === 'super_admin' && createdById) body.created_by_id = createdById;
body.column_ids = getSelectedColumnIds('createColumnPermissionWrap');
@@ -609,6 +832,12 @@
var msgEl = document.getElementById('msgEdit');
msgEl.textContent = '';
msgEl.className = 'msg';
var editPermissionWrap = document.getElementById('editColumnPermissionWrap');
if (!columnPermissionCatalogReady || !editPermissionWrap._permissionsReady) {
msgEl.textContent = '用户权限尚未加载,不能保存';
msgEl.classList.add('err');
return;
}
var body = {};
if (password) body.password = password;
if (currentUserRole === 'super_admin' && editRoleEl && editRoleEl.offsetParent !== null)
@@ -4483,114 +4712,6 @@
// ========== 栏目权限配置 ==========
function loadColumns() {
fetch('/api/admin/columns')
.then(function (r) { return r.json(); })
.then(function (res) {
var tbody = document.getElementById('columnListBody');
if (!res.success) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
allColumnsList = res.items || [];
if (allColumnsList.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="empty-tip">暂无菜单,请在上方新增</td></tr>';
} else {
tbody.innerHTML = allColumnsList.map(function (c) {
return '<tr><td>' + c.id + '</td><td>' + (c.name || '') + '</td><td>' + (c.column_key || '') + '</td><td>' + (c.created_at || '') + '</td><td>' +
'<button class="btn btn-sm" data-column-edit="' + c.id + '" data-name="' + (c.name || '').replace(/"/g, '&quot;') + '" data-key="' + (c.column_key || '').replace(/"/g, '&quot;') + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-column-delete="' + c.id + '" data-name="' + (c.name || '').replace(/"/g, '&quot;') + '">删除</button></td></tr>';
}).join('');
}
bindColumnActions();
})
.catch(function () {
document.getElementById('columnListBody').innerHTML = '<tr><td colspan="5" class="empty-tip">请求失败</td></tr>';
});
}
function bindColumnActions() {
document.querySelectorAll('[data-column-edit]').forEach(function (btn) {
btn.onclick = function () {
document.getElementById('editColumnId').value = btn.dataset.columnEdit || '';
document.getElementById('editColumnName').value = (btn.dataset.name || '').replace(/&quot;/g, '"');
document.getElementById('editColumnKey').value = (btn.dataset.key || '').replace(/&quot;/g, '"');
document.getElementById('msgEditColumn').textContent = '';
document.getElementById('editColumnModal').classList.add('show');
};
});
document.querySelectorAll('[data-column-delete]').forEach(function (btn) {
btn.onclick = function () {
if (!confirm('确定删除菜单“' + (btn.dataset.name || '').replace(/&quot;/g, '"') + '”吗?')) return;
fetch('/api/admin/column/' + btn.dataset.columnDelete, { method: 'DELETE' })
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) { loadColumns(); loadColumnsForPermission(); loadAdminMenus(getActiveAdminTabName()); }
else { alert(res.error || '删除失败'); }
});
};
});
}
document.getElementById('btnAddColumn').onclick = function () {
var name = (document.getElementById('columnName').value || '').trim();
var key = (document.getElementById('columnKey').value || '').trim();
var msgEl = document.getElementById('msgColumn');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!name) { msgEl.textContent = '请填写菜单名称'; msgEl.classList.add('err'); return; }
if (!key) {
msgEl.textContent = '请填写栏目标识'; msgEl.classList.add('err'); return;
}
fetch('/api/admin/column', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name, column_key: key })
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) {
msgEl.textContent = res.msg || '新增成功';
msgEl.classList.add('ok');
document.getElementById('columnName').value = '';
document.getElementById('columnKey').value = '';
loadColumns();
loadColumnsForPermission();
loadAdminMenus(getActiveAdminTabName());
} else {
msgEl.textContent = res.error || '新增失败';
msgEl.classList.add('err');
}
})
.catch(function () { msgEl.textContent = '请求失败'; msgEl.classList.add('err'); });
};
document.getElementById('btnSaveColumn').onclick = function () {
var cid = document.getElementById('editColumnId').value;
var name = (document.getElementById('editColumnName').value || '').trim();
var key = (document.getElementById('editColumnKey').value || '').trim();
var msgEl = document.getElementById('msgEditColumn');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!name) { msgEl.textContent = '请填写菜单名称'; msgEl.classList.add('err'); return; }
if (!key) {
msgEl.textContent = '请填写栏目标识'; msgEl.classList.add('err'); return;
}
fetch('/api/admin/column/' + cid, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name, column_key: key })
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) {
document.getElementById('editColumnModal').classList.remove('show');
loadColumns();
loadColumnsForPermission();
loadAdminMenus(getActiveAdminTabName());
} else {
msgEl.textContent = res.error || '保存失败';
msgEl.classList.add('err');
}
});
};
document.getElementById('btnCloseEditColumn').onclick = function () {
document.getElementById('editColumnModal').classList.remove('show');
};
@@ -4601,29 +4722,39 @@
.then(function (res) {
var tbody = document.getElementById('columnListBody');
if (!res.success) {
tbody.innerHTML = '<tr><td colspan="7" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
tbody.innerHTML = '<tr><td colspan="9" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
return;
}
allColumnsList = res.items || [];
if (allColumnsList.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">暂无菜单,请先在上方新增</td></tr>';
tbody.innerHTML = '<tr><td colspan="9" class="empty-tip">暂无菜单,请先在上方新增</td></tr>';
} else {
tbody.innerHTML = allColumnsList.map(function (c, index) {
tbody.innerHTML = allColumnsList.map(function (c) {
var siblings = columnSiblingItems(c);
var siblingIndex = siblings.findIndex(function (item) { return String(item.id) === String(c.id); });
var moveButtons =
'<button class="btn btn-sm btn-secondary" data-column-move-up="' + c.id + '"' + (index === 0 ? ' disabled' : '') + '>上移</button> ' +
'<button class="btn btn-sm btn-secondary" data-column-move-down="' + c.id + '"' + (index === allColumnsList.length - 1 ? ' disabled' : '') + '>下移</button> ';
return '<tr><td>' + c.id + '</td><td>' + (c.name || '') + '</td><td>' + (c.column_key || '') + '</td><td>' + ((c.menu_type || '') === 'admin' ? '后台(admin)' : '软件(app)') + '</td><td>' + (c.sort_order != null ? c.sort_order : 0) + '</td><td>' + (c.route_path || '') + '</td><td>' + (c.created_at || '') + '</td><td>' +
'<button class="btn btn-sm btn-secondary" data-column-move-up="' + c.id + '"' + (siblingIndex <= 0 ? ' disabled' : '') + '>上移</button> ' +
'<button class="btn btn-sm btn-secondary" data-column-move-down="' + c.id + '"' + (siblingIndex < 0 || siblingIndex === siblings.length - 1 ? ' disabled' : '') + '>下移</button> ';
var parent = allColumnsList.find(function (item) { return String(item.id) === String(c.parent_id || c.parentId || ''); });
return '<tr><td>' + c.id + '</td><td>' + (c.name || '') + '</td><td>' + (c.column_key || '') + '</td><td>' + ((c.menu_type || '') === 'admin' ? '后台(admin)' : '软件(app)') + '</td><td>' + (parent ? (parent.name || '') : '-') + '</td><td>' + (c.sort_order != null ? c.sort_order : 0) + '</td><td>' + (c.route_path || '') + '</td><td>' + (c.created_at || '') + '</td><td>' +
moveButtons +
'<button class="btn btn-sm" data-column-edit="' + c.id + '" data-name="' + (c.name || '').replace(/"/g, '&quot;') + '" data-key="' + (c.column_key || '').replace(/"/g, '&quot;') + '" data-route="' + (c.route_path || '').replace(/"/g, '&quot;') + '" data-menu-type="' + (c.menu_type || 'app').replace(/"/g, '&quot;') + '" data-sort-order="' + (c.sort_order != null ? String(c.sort_order) : '0').replace(/"/g, '&quot;') + '">编辑</button> ' +
'<button class="btn btn-sm" data-column-edit="' + c.id + '" data-name="' + (c.name || '').replace(/"/g, '&quot;') + '" data-key="' + (c.column_key || '').replace(/"/g, '&quot;') + '" data-route="' + (c.route_path || '').replace(/"/g, '&quot;') + '" data-menu-type="' + (c.menu_type || 'app').replace(/"/g, '&quot;') + '" data-sort-order="' + (c.sort_order != null ? String(c.sort_order) : '0').replace(/"/g, '&quot;') + '" data-parent-id="' + String(c.parent_id || c.parentId || '').replace(/"/g, '&quot;') + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-column-delete="' + c.id + '" data-name="' + (c.name || '').replace(/"/g, '&quot;') + '">删除</button></td></tr>';
}).join('');
}
bindColumnActions();
})
.catch(function () {
document.getElementById('columnListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败</td></tr>';
document.getElementById('columnListBody').innerHTML = '<tr><td colspan="9" class="empty-tip">请求失败</td></tr>';
});
}
function columnSiblingItems(item) {
var menuType = String(item && item.menu_type || 'app');
var parentId = columnParentId(item);
return allColumnsList.filter(function (candidate) {
return String(candidate.menu_type || 'app') === menuType && columnParentId(candidate) === parentId;
});
}
function bindColumnActions() {
document.querySelectorAll('[data-column-move-up]').forEach(function (btn) {
btn.onclick = function () {
@@ -4645,6 +4776,8 @@
document.getElementById('editColumnRoutePath').value = (btn.dataset.route || '').replace(/&quot;/g, '"');
document.getElementById('editColumnMenuType').value = ((btn.dataset.menuType || 'app').replace(/&quot;/g, '"') || 'app');
document.getElementById('editColumnSortOrder').value = ((btn.dataset.sortOrder || '0').replace(/&quot;/g, '"') || '0');
populateColumnParentSelects();
document.getElementById('editColumnParentId').value = btn.dataset.parentId || '';
document.getElementById('msgEditColumn').textContent = '';
document.getElementById('editColumnModal').classList.add('show');
};
@@ -4662,26 +4795,22 @@
});
}
function moveColumnItem(columnId, delta, triggerBtn) {
var currentIndex = -1;
for (var i = 0; i < allColumnsList.length; i++) {
if (String(allColumnsList[i].id) === String(columnId)) {
currentIndex = i;
break;
}
}
var currentItem = allColumnsList.find(function (item) { return String(item.id) === String(columnId); });
if (!currentItem) return;
var siblings = columnSiblingItems(currentItem);
var currentIndex = siblings.findIndex(function (item) { return String(item.id) === String(columnId); });
if (currentIndex < 0) return;
var targetIndex = currentIndex + delta;
if (targetIndex < 0 || targetIndex >= allColumnsList.length) return;
var currentItem = allColumnsList[currentIndex];
var targetItem = allColumnsList[targetIndex];
if (!currentItem || !targetItem) return;
if (targetIndex < 0 || targetIndex >= siblings.length) return;
var targetItem = siblings[targetIndex];
if (triggerBtn) triggerBtn.disabled = true;
fetch('/api/admin/column/reorder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
column_id: currentItem.id,
target_id: targetItem.id
target_id: targetItem.id,
menu_type: currentItem.menu_type || 'app'
})
}).then(function (r) { return r.json(); })
.then(function (res) {
@@ -4704,6 +4833,7 @@
var routePath = (document.getElementById('columnRoutePath').value || '').trim();
var sortOrderValue = (document.getElementById('columnSortOrder').value || '').trim();
var menuType = (document.getElementById('columnMenuType').value || 'admin').trim() || 'admin';
var parentIdValue = (document.getElementById('columnParentId').value || '').trim();
var sortOrder = sortOrderValue === '' ? null : parseInt(sortOrderValue, 10);
var msgEl = document.getElementById('msgColumn');
msgEl.textContent = '';
@@ -4721,7 +4851,7 @@
fetch('/api/admin/column', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name, column_key: key, route_path: routePath, menu_type: menuType, sort_order: sortOrder })
body: JSON.stringify({ name: name, column_key: key, route_path: routePath, menu_type: menuType, sort_order: sortOrder, parent_id: parentIdValue ? Number(parentIdValue) : null })
})
.then(function (r) { return r.json(); })
.then(function (res) {
@@ -4733,6 +4863,7 @@
document.getElementById('columnRoutePath').value = '';
document.getElementById('columnSortOrder').value = '';
document.getElementById('columnMenuType').value = 'admin';
document.getElementById('columnParentId').value = '';
loadColumns();
loadColumnsForPermission();
loadAdminMenus(getActiveAdminTabName());
@@ -4750,6 +4881,7 @@
var routePath = (document.getElementById('editColumnRoutePath').value || '').trim();
var sortOrderValue = (document.getElementById('editColumnSortOrder').value || '').trim();
var menuType = (document.getElementById('editColumnMenuType').value || 'admin').trim() || 'admin';
var parentIdValue = (document.getElementById('editColumnParentId').value || '').trim();
var sortOrder = sortOrderValue === '' ? null : parseInt(sortOrderValue, 10);
var msgEl = document.getElementById('msgEditColumn');
msgEl.textContent = '';
@@ -4767,7 +4899,7 @@
fetch('/api/admin/column/' + cid, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name, column_key: key, route_path: routePath, menu_type: menuType, sort_order: sortOrder })
body: JSON.stringify({ name: name, column_key: key, route_path: routePath, menu_type: menuType, sort_order: sortOrder, parent_id: parentIdValue ? Number(parentIdValue) : null })
})
.then(function (r) { return r.json(); })
.then(function (res) {
@@ -4782,6 +4914,8 @@
}
});
};
if (document.getElementById('columnMenuType')) document.getElementById('columnMenuType').onchange = populateColumnParentSelects;
if (document.getElementById('editColumnMenuType')) document.getElementById('editColumnMenuType').onchange = populateColumnParentSelects;
// ========== 分页 ==========
function renderPagination(elId, total, page, pageSize, onPage) {