407 lines
19 KiB
HTML
407 lines
19 KiB
HTML
<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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
}
|
||
|
||
// 离开页面前提示未保存
|
||
window.addEventListener('beforeunload', (e) => {
|
||
if (_hasChanges) {
|
||
e.preventDefault();
|
||
e.returnValue = '';
|
||
}
|
||
});
|
||
|
||
init();
|
||
</script>
|