Files
SMANAGE/TempLate/GERP/pages/system/role-permissions.html
T

407 lines
19 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script src="/assets/gerp/CompanyCommon.js?v=20260627c"></script>
<style>
.rp-page { padding: 24px; height: 100%; display: flex; flex-direction: column; box-sizing: border-box; }
.rp-header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; flex-shrink: 0; flex-wrap: wrap; }
.rp-header h1 { margin: 0; flex: 1; min-width: 200px; }
.rp-toolbar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 12px; flex-shrink: 0; padding: 12px; background: var(--CardBackground, #fff); border: 1px solid var(--BorderColor, #e2e8f0); border-radius: 8px; }
.rp-toolbar label { font-size: 13px; color: var(--TextSecondary, #64748b); margin-right: 4px; font-weight: 600; }
.rp-toolbar select { padding: 6px 10px; border: 1px solid var(--BorderColor, #e2e8f0); border-radius: 6px; font-size: 13px; min-width: 200px; background: var(--CardBackground, #fff); color: var(--TextPrimary, #1e293b); }
.rp-toolbar .stat { margin-left: auto; font-size: 13px; color: var(--TextSecondary, #64748b); }
.rp-toolbar .stat strong { color: var(--PrimaryColor, #2563eb); }
.rp-actions { display: flex; gap: 8px; }
.rp-tree-wrap { flex: 1; min-height: 0; overflow: auto; padding: 12px; background: var(--CardBackground, #fff); border: 1px solid var(--BorderColor, #e2e8f0); border-radius: 8px; }
.rp-group { margin-bottom: 16px; border: 1px solid var(--BorderColor, #e2e8f0); border-radius: 6px; overflow: hidden; }
.rp-group-header { display: flex; align-items: center; gap: 8px; padding: 10px 12px; background: var(--BgSecondary, #f8fafc); cursor: pointer; user-select: none; font-weight: 600; font-size: 14px; color: var(--TextPrimary, #1e293b); }
.rp-group-header:hover { background: var(--BgHover, #f1f5f9); }
.rp-group-header .toggle { transition: transform 0.2s; }
.rp-group.collapsed .toggle { transform: rotate(-90deg); }
.rp-group.collapsed .rp-group-body { display: none; }
.rp-group-header .count { margin-left: auto; font-size: 12px; color: var(--TextSecondary, #64748b); font-weight: normal; }
.rp-group-body { padding: 4px 0; }
.rp-perm-row { display: flex; align-items: center; gap: 10px; padding: 8px 16px 8px 36px; cursor: pointer; font-size: 13px; }
.rp-perm-row:hover { background: var(--BgHover, #f1f5f9); }
.rp-perm-row input[type="checkbox"] { cursor: pointer; }
.rp-perm-row .name { font-weight: 500; color: var(--TextPrimary, #1e293b); }
.rp-perm-row .code { font-family: 'Consolas', 'Monaco', monospace; font-size: 12px; color: var(--TextSecondary, #64748b); padding: 1px 6px; background: var(--BgSecondary, #f8fafc); border-radius: 3px; }
.rp-perm-row .desc { color: var(--TextSecondary, #64748b); font-size: 12px; margin-left: auto; }
.rp-empty { text-align: center; padding: 60px 20px; color: var(--TextSecondary, #64748b); }
.rp-empty i { font-size: 48px; margin-bottom: 12px; display: block; color: var(--TextTertiary, #94a3b8); }
.rp-footer { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; flex-shrink: 0; padding: 12px; background: var(--CardBackground, #fff); border: 1px solid var(--BorderColor, #e2e8f0); border-radius: 8px; }
</style>
<div class="GERPPage">
<div class="rp-page">
<div class="rp-header">
<a href="javascript:void(0)" class="cp-back-btn" onclick="goBack()" style="cursor:pointer;color:var(--PrimaryColor);display:flex;align-items:center;gap:4px;font-size:14px;">
<i class="fas fa-arrow-left"></i> 返回
</a>
<h1 class="PageTitle">
<i class="fas fa-shield-alt icon-primary icon-left-lg"></i>
<span>角色权限分配</span>
</h1>
<span id="admin-badge" style="display:none;padding:2px 10px;border-radius:4px;background:#1e40af;color:#fff;font-size:12px;font-weight:600;"><i class="fas fa-shield-alt"></i> 特权模式</span>
</div>
<div class="rp-toolbar">
<label>角色:</label>
<select id="roleSelect" onchange="onRoleChange()"></select>
<label>公司:</label>
<select id="companySelect" onchange="onCompanyChange()"></select>
<div class="stat" id="permStat">共 <strong>0</strong> 项权限,已勾选 <strong>0</strong> 项</div>
<div class="rp-actions">
<button class="GERPBtn Secondary" onclick="expandAll()"><i class="fas fa-expand"></i> 全部展开</button>
<button class="GERPBtn Secondary" onclick="collapseAll()"><i class="fas fa-compress"></i> 全部折叠</button>
<button class="GERPBtn Secondary" onclick="checkAll()"><i class="fas fa-check-double"></i> 全选</button>
<button class="GERPBtn Secondary" onclick="uncheckAll()"><i class="fas fa-times"></i> 清空</button>
</div>
</div>
<div class="rp-tree-wrap" id="treeWrap">
<div class="rp-empty" id="emptyState">
<i class="fas fa-shield-alt"></i>
<div>请先选择角色和公司</div>
</div>
<div id="errorState" style="display:none;color:var(--DangerColor);padding:40px;text-align:center;"></div>
<div id="loadingState" style="display:none;text-align:center;padding:40px;color:var(--TextSecondary);">
<i class="fas fa-spinner fa-spin"></i> 加载中...
</div>
<div id="treeBody"></div>
</div>
<div class="rp-footer">
<button class="GERPBtn Primary" id="saveBtn" onclick="savePermissions()" disabled style="display:flex;align-items:center;gap:6px;">
<i class="fas fa-save"></i> 保存权限
</button>
</div>
<div id="deniedState" style="display:none;text-align:center;padding:60px 20px;color:var(--TextSecondary,#64748b);">
<i class="fas fa-lock" style="font-size:48px;margin-bottom:12px;display:block;color:var(--DangerColor,#ef4444);"></i>
<div style="font-size:16px;font-weight:600;color:var(--TextPrimary,#1e293b);margin-bottom:6px;">无权访问</div>
<div>该页面仅管理员可查看,请切换至管理员角色或联系管理员。</div>
</div>
</div>
</div>
<script>
const _urlParams = new URLSearchParams(window.location.search);
const _initRoleUUID = _urlParams.get('RoleUUID') || '';
const _initCompanyUUID = _urlParams.get('CompanyUUID') || '';
let _isAdminMode = _urlParams.get('admin_verify') === '1';
let _permissionTree = [];
let _roles = [];
let _companies = [];
let _originalCheckedSet = new Set();
let _hasChanges = false;
GERP.SetTitle('角色权限分配');
function goBack() {
if (window.history.length > 1) {
window.history.back();
} else {
GERP.Navigate('/pages/system/roles.html', '角色管理');
}
}
async function checkAdmin() {
try {
const resp = await fetch('/API/FormAPI/CheckAdmin', { credentials: 'include' });
const result = await resp.json();
return result.code === 0 && result.data && result.data.is_admin;
} catch (e) { return false; }
}
function apiQs(extra = {}) {
const params = {};
if (_isAdminMode) params.admin_verify = '1';
Object.assign(params, extra);
const qs = new URLSearchParams(params).toString();
return qs ? `?${qs}` : '';
}
async function init() {
// 入口校验:仅管理员可访问角色权限分配页
const isAdmin = await checkAdmin();
if (!isAdmin) {
document.querySelector('.rp-toolbar').style.display = 'none';
document.querySelector('.rp-tree-wrap').style.display = 'none';
document.querySelector('.rp-footer').style.display = 'none';
document.getElementById('deniedState').style.display = 'block';
return;
}
_isAdminMode = true;
document.getElementById('admin-badge').style.display = 'inline-block';
await Promise.all([loadRoles(), loadCompanies()]);
// 自动选中初始角色/公司
if (_initRoleUUID) {
const roleSel = document.getElementById('roleSelect');
for (const opt of roleSel.options) {
if (opt.value === _initRoleUUID) { opt.selected = true; break; }
}
}
if (_initCompanyUUID) {
const compSel = document.getElementById('companySelect');
for (const opt of compSel.options) {
if (opt.value === _initCompanyUUID) { opt.selected = true; break; }
}
}
if (_initRoleUUID && _initCompanyUUID) {
loadTree();
}
}
async function loadRoles() {
try {
const resp = await fetch(`/API/FormAPI/Get/Role${apiQs({ PageSize: '500' })}`, { credentials: 'include' });
const result = await resp.json();
if (result.code !== 0) return;
_roles = Array.isArray(result.data) ? result.data : [];
const sel = document.getElementById('roleSelect');
sel.innerHTML = '<option value="">请选择角色</option>' + _roles.map(r =>
`<option value="${r.RoleUUID}">${escapeHtml(r.RoleName || '')}</option>`
).join('');
} catch (e) { console.error(e); }
}
async function loadCompanies() {
try {
const resp = await fetch(`/API/FormAPI/Get/Company${apiQs({ PageSize: '500' })}`, { credentials: 'include' });
const result = await resp.json();
if (result.code !== 0) return;
_companies = Array.isArray(result.data) ? result.data : [];
const sel = document.getElementById('companySelect');
sel.innerHTML = '<option value="">请选择公司</option>' + _companies.map(c =>
`<option value="${c.CompanyUUID}">${escapeHtml(c.CompanyName || '')}</option>`
).join('');
} catch (e) { console.error(e); }
}
function onRoleChange() { if (getSelectedCompany()) loadTree(); }
function onCompanyChange() { if (getSelectedRole()) loadTree(); }
function getSelectedRole() { return document.getElementById('roleSelect').value; }
function getSelectedCompany() { return document.getElementById('companySelect').value; }
async function loadTree() {
const roleUUID = getSelectedRole();
const companyUUID = getSelectedCompany();
const treeBody = document.getElementById('treeBody');
const emptyEl = document.getElementById('emptyState');
const errorEl = document.getElementById('errorState');
const loadingEl = document.getElementById('loadingState');
const saveBtn = document.getElementById('saveBtn');
treeBody.innerHTML = '';
emptyEl.style.display = 'none';
errorEl.style.display = 'none';
loadingEl.style.display = 'none';
saveBtn.disabled = true;
_hasChanges = false;
if (!roleUUID || !companyUUID) {
emptyEl.style.display = 'block';
return;
}
loadingEl.style.display = 'block';
try {
const url = `/API/Role/PermissionsTree${apiQs({ RoleUUID: roleUUID, CompanyUUID: companyUUID })}`;
const resp = await fetch(url, { credentials: 'include' });
const result = await resp.json();
loadingEl.style.display = 'none';
if (result.code === 403) {
errorEl.textContent = result.msg || '无权操作';
errorEl.style.display = 'block';
return;
}
if (result.code !== 0) {
errorEl.textContent = result.msg || '加载失败';
errorEl.style.display = 'block';
return;
}
_permissionTree = (result.data && result.data.Groups) || [];
_originalCheckedSet = new Set(
_permissionTree.flatMap(g => g.Permissions.filter(p => p.Checked).map(p => p.PermUUID))
);
renderTree();
updateStat();
saveBtn.disabled = false;
} catch (err) {
loadingEl.style.display = 'none';
errorEl.textContent = '网络错误: ' + err.message;
errorEl.style.display = 'block';
}
}
function renderTree() {
const treeBody = document.getElementById('treeBody');
if (_permissionTree.length === 0) {
treeBody.innerHTML = '<div class="rp-empty"><i class="fas fa-inbox"></i><div>暂无权限定义</div></div>';
return;
}
treeBody.innerHTML = _permissionTree.map((group, gi) => {
const checkedCount = group.Permissions.filter(p => p.Checked).length;
const allChecked = checkedCount === group.Permissions.length && group.Permissions.length > 0;
return `
<div class="rp-group" id="group-${gi}">
<div class="rp-group-header" onclick="toggleGroup(${gi})">
<i class="fas fa-chevron-down toggle"></i>
<input type="checkbox" ${allChecked ? 'checked' : ''} onclick="event.stopPropagation();toggleGroupCheck(${gi}, this.checked)" />
<span>${escapeHtml(group.Category)}</span>
<span class="count">${checkedCount} / ${group.Permissions.length}</span>
</div>
<div class="rp-group-body">
${group.Permissions.map((p, pi) => `
<div class="rp-perm-row" onclick="togglePerm(${gi}, ${pi})">
<input type="checkbox" data-group="${gi}" data-perm="${pi}" ${p.Checked ? 'checked' : ''} onclick="event.stopPropagation();onPermCheck(${gi}, ${pi}, this.checked)" />
<span class="name">${escapeHtml(p.PermName)}</span>
<span class="code">${escapeHtml(p.PermCode)}</span>
${p.PermDesc ? `<span class="desc">${escapeHtml(p.PermDesc)}</span>` : ''}
</div>
`).join('')}
</div>
</div>
`;
}).join('');
}
function toggleGroup(idx) {
const el = document.getElementById(`group-${idx}`);
if (el) el.classList.toggle('collapsed');
}
function expandAll() {
document.querySelectorAll('.rp-group').forEach(el => el.classList.remove('collapsed'));
}
function collapseAll() {
document.querySelectorAll('.rp-group').forEach(el => el.classList.add('collapsed'));
}
function toggleGroupCheck(groupIdx, checked) {
const group = _permissionTree[groupIdx];
if (!group) return;
group.Permissions.forEach(p => { p.Checked = checked; });
renderTree();
updateStat();
markChanged();
}
function togglePerm(groupIdx, permIdx) {
const perm = _permissionTree[groupIdx] && _permissionTree[groupIdx].Permissions[permIdx];
if (!perm) return;
perm.Checked = !perm.Checked;
renderTree();
updateStat();
markChanged();
}
function onPermCheck(groupIdx, permIdx, checked) {
const perm = _permissionTree[groupIdx] && _permissionTree[groupIdx].Permissions[permIdx];
if (!perm) return;
perm.Checked = checked;
// 仅更新当前组头部勾选状态,避免全量重渲染丢焦点
updateGroupHeader(groupIdx);
updateStat();
markChanged();
}
function updateGroupHeader(groupIdx) {
const group = _permissionTree[groupIdx];
if (!group) return;
const checkedCount = group.Permissions.filter(p => p.Checked).length;
const allChecked = checkedCount === group.Permissions.length && group.Permissions.length > 0;
const groupEl = document.getElementById(`group-${groupIdx}`);
if (!groupEl) return;
const headerCheckbox = groupEl.querySelector('.rp-group-header input[type="checkbox"]');
if (headerCheckbox) headerCheckbox.checked = allChecked;
const countEl = groupEl.querySelector('.count');
if (countEl) countEl.textContent = `${checkedCount} / ${group.Permissions.length}`;
}
function updateStat() {
const total = _permissionTree.reduce((s, g) => s + g.Permissions.length, 0);
const checked = _permissionTree.reduce((s, g) => s + g.Permissions.filter(p => p.Checked).length, 0);
document.getElementById('permStat').innerHTML = `共 <strong>${total}</strong> 项权限,已勾选 <strong>${checked}</strong> 项`;
}
function checkAll() {
_permissionTree.forEach(g => g.Permissions.forEach(p => p.Checked = true));
renderTree();
updateStat();
markChanged();
}
function uncheckAll() {
_permissionTree.forEach(g => g.Permissions.forEach(p => p.Checked = false));
renderTree();
updateStat();
markChanged();
}
function markChanged() {
const currentCheckedSet = new Set(
_permissionTree.flatMap(g => g.Permissions.filter(p => p.Checked).map(p => p.PermUUID))
);
_hasChanges = !(currentCheckedSet.size === _originalCheckedSet.size &&
[...currentCheckedSet].every(x => _originalCheckedSet.has(x)));
}
async function savePermissions() {
const roleUUID = getSelectedRole();
const companyUUID = getSelectedCompany();
if (!roleUUID || !companyUUID) {
if (window.GERPLayui) GERPLayui.Toast('请先选择角色和公司', 'warning');
return;
}
const permUUIDs = _permissionTree.flatMap(g => g.Permissions.filter(p => p.Checked).map(p => p.PermUUID));
const btn = document.getElementById('saveBtn');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 保存中...';
try {
const resp = await fetch(`/API/Role/SavePermissions${apiQs()}`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ RoleUUID: roleUUID, CompanyUUID: companyUUID, PermUUIDs: permUUIDs })
});
const result = await resp.json();
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-save"></i> 保存权限';
if (result.code === 0) {
const d = result.data || {};
if (window.GERPLayui) GERPLayui.Toast(`保存成功(新增 ${d.Added || 0},移除 ${d.Removed || 0},保留 ${d.Kept || 0})`, 'success');
_originalCheckedSet = new Set(permUUIDs);
_hasChanges = false;
} else {
if (window.GERPLayui) GERPLayui.Toast(result.msg || '保存失败', 'error');
}
} catch (err) {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-save"></i> 保存权限';
if (window.GERPLayui) GERPLayui.Toast('网络错误: ' + err.message, 'error');
}
}
function escapeHtml(str) {
if (str === null || str === undefined) return '';
return String(str).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
// 离开页面前提示未保存
window.addEventListener('beforeunload', (e) => {
if (_hasChanges) {
e.preventDefault();
e.returnValue = '';
}
});
init();
</script>