390 lines
17 KiB
HTML
390 lines
17 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>分配权限 · 布局草图</title>
|
||
<style>
|
||
/* 值取自 fms-vue/src/theme/tokens.css(浅色),保证草图与系统观感一致 */
|
||
:root{
|
||
--fms-background:#eef1f6; --fms-card:#fff; --fms-text:#1f2329;
|
||
--fms-secondary:#e9edf2; --fms-secondary-hover:#f1f5f9;
|
||
--fms-text-secondary:#6f7784; --fms-border:#e3e8ee; --fms-border-soft:#ebedf2;
|
||
--fms-primary:#0f172b; --fms-primary-text:#fff; --fms-primary-soft:rgba(15,23,43,.12);
|
||
--fms-table-header-bg:#f2f4f8;
|
||
--fms-font-size:13px; --fms-line-height:1.4;
|
||
--fms-control-height:26px; --fms-control-padding-x:8px;
|
||
--fms-control-radius:6px; --fms-control-size:16px;
|
||
--fms-popover-shadow:0 3px 8px rgba(0,0,0,.12); --fms-mask-bg:rgba(0,0,0,.45);
|
||
--fms-duration-fast:100ms; --fms-duration-base:150ms;
|
||
--fms-ease-standard:cubic-bezier(.4,0,.2,1);
|
||
--fms-tree-node-height:32px; --fms-tree-indent:18px;
|
||
}
|
||
*{box-sizing:border-box}
|
||
body{
|
||
margin:0;padding:28px 24px 40px;background:var(--fms-background);color:var(--fms-text);
|
||
font-family:"Noto Sans SC","Microsoft YaHei",system-ui,sans-serif;
|
||
font-size:var(--fms-font-size);line-height:var(--fms-line-height);
|
||
}
|
||
/* ---------- 仅演示用的控件条(不属于设计本身) ---------- */
|
||
.demo-bar{
|
||
display:flex;align-items:center;gap:8px;margin:0 auto 14px;max-width:920px;
|
||
padding:8px 12px;border:1px dashed #b9c2cf;border-radius:6px;background:#fff;
|
||
color:var(--fms-text-secondary);font-size:12px;flex-wrap:wrap;
|
||
}
|
||
.demo-bar strong{color:var(--fms-text);font-weight:600}
|
||
.demo-bar em{font-style:normal;color:#8b93a1;margin-right:2px}
|
||
.demo-btn{
|
||
height:24px;padding:0 10px;border:1px solid var(--fms-border);border-radius:4px;
|
||
background:#fff;color:var(--fms-text);font:inherit;font-size:12px;cursor:pointer;
|
||
}
|
||
.demo-btn:hover{background:var(--fms-secondary-hover)}
|
||
.demo-btn.is-on{border-color:var(--fms-primary);background:var(--fms-primary);color:#fff}
|
||
|
||
/* ---------- 抽屉外壳:按真实 ui/Drawer 复刻 ---------- */
|
||
.drawer{
|
||
display:flex;flex-direction:column;max-width:920px;height:660px;margin:0 auto;
|
||
background:var(--fms-card);box-shadow:var(--fms-popover-shadow);border-radius:2px;overflow:hidden;
|
||
}
|
||
.drawer-header{
|
||
display:flex;align-items:center;gap:12px;flex:none;flex:0 0 auto;
|
||
padding:8px 20px;border-bottom:1px solid var(--fms-border);
|
||
}
|
||
.drawer-title{
|
||
margin:0;flex:1;min-width:0;font-size:14px;font-weight:600;
|
||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
||
}
|
||
.close{
|
||
display:inline-flex;align-items:center;justify-content:center;flex:none;
|
||
width:24px;height:24px;border:0;border-radius:4px;
|
||
background:transparent;color:var(--fms-text-secondary);font-size:14px;cursor:pointer;
|
||
}
|
||
.close:hover{background:var(--fms-secondary-hover);color:var(--fms-text)}
|
||
.drawer-body{display:flex;flex-direction:column;flex:1;min-height:0}
|
||
.drawer-footer{
|
||
display:flex;justify-content:flex-end;gap:8px;flex:none;padding:8px 20px;border-top:1px solid var(--fms-border);
|
||
}
|
||
.btn{
|
||
height:var(--fms-control-height);padding:0 var(--fms-control-padding-x);border-radius:var(--fms-control-radius);
|
||
border:1px solid var(--fms-border);background:#fff;color:var(--fms-text);font:inherit;cursor:pointer;
|
||
}
|
||
.btn:hover{background:var(--fms-secondary-hover)}
|
||
.btn-primary{border-color:var(--fms-primary);background:var(--fms-primary);color:var(--fms-primary-text)}
|
||
.btn-primary:hover{opacity:.9}
|
||
|
||
/* ---------- tabs 落在内容区,不在 header ---------- */
|
||
.tabs-band{display:flex;gap:20px;flex:none;padding:0 20px;border-bottom:1px solid var(--fms-border)}
|
||
.ptab{padding:9px 0 8px;color:var(--fms-text-secondary);cursor:pointer;white-space:nowrap}
|
||
.ptab.is-active{color:var(--fms-text);font-weight:600;box-shadow:inset 0 -2px 0 var(--fms-primary)}
|
||
|
||
/* ---------- 面板:左菜单树 + 右权限表 ---------- */
|
||
.panel{display:flex;flex:1;min-height:0;padding:12px 20px}
|
||
|
||
/* 左栏:菜单树 */
|
||
.tree{
|
||
display:flex;flex-direction:column;flex:none;width:220px;height:100%;
|
||
padding-right:12px;border-right:1px solid var(--fms-border);
|
||
}
|
||
.tree__search{padding:0 0 8px}
|
||
.tree__search input{
|
||
width:100%;height:var(--fms-control-height);padding:0 8px;border:1px solid var(--fms-border);
|
||
border-radius:var(--fms-control-radius);font:inherit;color:var(--fms-text);
|
||
}
|
||
.tree__scroll{flex:1;min-height:0;overflow-y:auto}
|
||
.tnode{
|
||
display:flex;align-items:center;gap:6px;height:var(--fms-tree-node-height);padding:0 8px;
|
||
border-radius:var(--fms-control-radius);color:var(--fms-text-secondary);cursor:pointer;
|
||
}
|
||
.tnode:hover{background:var(--fms-secondary-hover);color:var(--fms-text)}
|
||
.tnode.is-selected{background:var(--fms-primary-soft);color:var(--fms-text);font-weight:600}
|
||
.tnode__caret{width:12px;flex:none;text-align:center;font-size:10px;color:var(--fms-text-secondary)}
|
||
.tnode--l1{padding-left:26px}
|
||
.tnode--l2{padding-left:44px}
|
||
|
||
/* 右栏:页头(菜单权限)+ 模块/动作表 */
|
||
.detail{display:flex;flex-direction:column;flex:1;min-width:0;min-height:0;padding-left:16px}
|
||
|
||
/* 页头:菜单入口,唯一的一个页面级开关 */
|
||
.page-band{
|
||
display:flex;align-items:center;gap:6px;flex:none;min-height:34px;
|
||
padding:0 8px 8px 0;border-bottom:1px solid var(--fms-border);
|
||
}
|
||
.page-band__name{margin:0;font-size:14px;font-weight:600;white-space:nowrap}
|
||
.page-band__meta{color:var(--fms-text-secondary);font-size:12px}
|
||
.page-band__actions{margin-left:auto;display:flex;gap:2px}
|
||
.link-btn{
|
||
height:24px;padding:0 8px;border:0;border-radius:4px;background:none;
|
||
color:var(--fms-text-secondary);font:inherit;font-size:12px;cursor:pointer;
|
||
}
|
||
.link-btn:hover{background:var(--fms-secondary-hover);color:var(--fms-text)}
|
||
|
||
/* 表头:只有「模块 / 动作」两列,层级靠列而非靠嵌套框 */
|
||
.grid{display:flex;flex-direction:column;flex:1;min-height:0;padding-top:8px}
|
||
.grid__head{
|
||
display:flex;align-items:center;gap:12px;flex:none;padding:0 8px 6px;
|
||
color:var(--fms-text-secondary);font-size:12px;border-bottom:1px solid var(--fms-border-soft);
|
||
}
|
||
.col-module{width:160px;flex:none}
|
||
.col-actions{flex:1;min-width:0}
|
||
.col-count{width:52px;flex:none;text-align:right}
|
||
.grid__body{flex:1;min-height:0;overflow-y:auto}
|
||
.grid__body.is-locked{opacity:.45;pointer-events:none}
|
||
|
||
.row{
|
||
display:flex;align-items:flex-start;gap:12px;padding:7px 8px;
|
||
border-bottom:1px solid var(--fms-border-soft);
|
||
transition:background-color var(--fms-duration-fast) var(--fms-ease-standard);
|
||
}
|
||
.row:hover{background:var(--fms-secondary-hover)}
|
||
.row__module{display:flex;align-items:center;width:160px;flex:none}
|
||
.row__actions{display:flex;flex-wrap:wrap;flex:1;min-width:0;margin:-2px 0}
|
||
.row__count{
|
||
width:52px;flex:none;text-align:right;line-height:26px;
|
||
color:var(--fms-text-secondary);font-size:12px;font-variant-numeric:tabular-nums;
|
||
}
|
||
.grid__empty{padding:40px 8px;text-align:center;color:var(--fms-text-secondary)}
|
||
|
||
/* ---------- 勾选单元:[框] + 文字,整块可点 ---------- */
|
||
.check{
|
||
display:inline-flex;align-items:center;gap:6px;min-height:26px;padding:2px 6px;
|
||
border:0;border-radius:var(--fms-control-radius);background:none;
|
||
color:var(--fms-text-secondary);font:inherit;text-align:left;cursor:pointer;
|
||
transition:color var(--fms-duration-fast) var(--fms-ease-standard),
|
||
background-color var(--fms-duration-fast) var(--fms-ease-standard);
|
||
}
|
||
.check:hover{background:var(--fms-secondary);color:var(--fms-text)}
|
||
.check.is-checked{color:var(--fms-text);font-weight:500}
|
||
.box{
|
||
display:inline-flex;align-items:center;justify-content:center;flex:none;
|
||
width:var(--fms-control-size);height:var(--fms-control-size);
|
||
border:1px solid var(--fms-border);border-radius:4px;
|
||
background:var(--fms-card);color:var(--fms-primary-text);
|
||
}
|
||
.check:hover .box{border-color:var(--fms-primary)}
|
||
.check.is-checked .box{border-color:var(--fms-primary);background:var(--fms-primary)}
|
||
.box svg{display:none}
|
||
.check.is-checked .box svg{display:block}
|
||
.box i{display:none;width:8px;height:2px;background:var(--fms-primary)}
|
||
.check.is-partial .box{border-color:var(--fms-primary)}
|
||
.check.is-partial .box i{display:block}
|
||
.check--use{color:var(--fms-text);font-weight:500}
|
||
.check--use .label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
|
||
/* ---------- 动作两种呈现,仅供对比 ---------- */
|
||
/* A. 标签式:选中=浅底 chip,未选=正文灰字,动作多时靠换行消化 */
|
||
.tag{
|
||
display:inline-flex;align-items:center;height:24px;margin:2px 4px 2px 0;padding:0 10px;
|
||
border:1px solid transparent;border-radius:12px;background:transparent;
|
||
color:var(--fms-text-secondary);font:inherit;cursor:pointer;white-space:nowrap;
|
||
transition:background-color var(--fms-duration-fast) var(--fms-ease-standard),
|
||
color var(--fms-duration-fast) var(--fms-ease-standard);
|
||
}
|
||
.tag:hover{background:var(--fms-secondary);color:var(--fms-text)}
|
||
.tag.is-on{
|
||
background:var(--fms-primary-soft);color:var(--fms-text);font-weight:500;
|
||
box-shadow:inset 0 0 0 1px rgba(15,23,43,.16);
|
||
}
|
||
/* B. 复选框式 */
|
||
.action-check{margin:2px 4px 2px 0}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="demo-bar">
|
||
<strong>演示控件</strong>
|
||
<em>动作呈现</em>
|
||
<button class="demo-btn" data-style="tag">标签</button>
|
||
<button class="demo-btn" data-style="check">复选框</button>
|
||
<em style="margin-left:8px">动作数量</em>
|
||
<button class="demo-btn" data-scale="normal">常规</button>
|
||
<button class="demo-btn" data-scale="huge">超多</button>
|
||
<span style="flex:1 1 100%;color:#8b93a1">
|
||
点模块左侧方框 = 该模块动作全给/全撤 | 点动作 = 单项开关 | 关掉页头「可访问」= 收回本页全部模块授权(灰色锁定)
|
||
</span>
|
||
</div>
|
||
|
||
<div class="drawer">
|
||
<header class="drawer-header">
|
||
<h1 class="drawer-title">分配权限 · 张三(001)</h1>
|
||
<button class="close" title="关闭">✕</button>
|
||
</header>
|
||
|
||
<div class="drawer-body">
|
||
<nav class="tabs-band">
|
||
<span class="ptab is-active">功能权限</span>
|
||
<span class="ptab">字段权限</span>
|
||
<span class="ptab">数据范围</span>
|
||
</nav>
|
||
|
||
<div class="panel">
|
||
<aside class="tree">
|
||
<div class="tree__search"><input placeholder="搜索菜单" value=""></div>
|
||
<div class="tree__scroll">
|
||
<div class="tnode is-selected"><span class="tnode__caret">▾</span><span>海运工作台</span></div>
|
||
<div class="tnode tnode--l1"><span class="tnode__caret"></span><span>海运操作</span></div>
|
||
<div class="tnode tnode--l1"><span class="tnode__caret"></span><span>基础资料</span></div>
|
||
<div class="tnode"><span class="tnode__caret">▸</span><span>空运工作台</span></div>
|
||
<div class="tnode"><span class="tnode__caret">▸</span><span>财务管理</span></div>
|
||
<div class="tnode"><span class="tnode__caret">▸</span><span>系统管理</span></div>
|
||
</div>
|
||
</aside>
|
||
|
||
<main class="detail">
|
||
<div class="page-band">
|
||
<h3 class="page-band__name">海运工作台</h3>
|
||
<button class="check check--use" id="entry-check" type="button"
|
||
title="菜单入口。关闭会收回该页面下的全部模块授权">
|
||
<span class="box"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg><i></i></span>
|
||
<span class="label">可访问</span>
|
||
</button>
|
||
<span class="page-band__meta" id="entry-meta"></span>
|
||
<div class="page-band__actions">
|
||
<button class="link-btn" id="grant-all">全授权</button>
|
||
<button class="link-btn" id="clear-all">全禁用</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid">
|
||
<div class="grid__head">
|
||
<span class="col-module">模块</span>
|
||
<span class="col-actions">动作</span>
|
||
<span class="col-count">已选</span>
|
||
</div>
|
||
<div class="grid__body" id="rows"></div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
</div>
|
||
|
||
<footer class="drawer-footer">
|
||
<button class="btn">取消</button>
|
||
<button class="btn btn-primary">保存</button>
|
||
</footer>
|
||
</div>
|
||
|
||
<script>
|
||
const CHECK = '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>'
|
||
|
||
/* 模块与动作:动作数量可切到「超多」验证换行表现 */
|
||
const BASE_MODULES = [
|
||
{ name:'海运主单', actions:['新增','修改','删除','审核','反审核','作废','结算','放单','改单','分摊','批量导入','导出','打印','复制'],
|
||
checked:['新增','修改','审核','导出'] },
|
||
{ name:'装箱信息', actions:['新增','修改','删除','导出'], checked:['新增','导出'] },
|
||
{ name:'费用', actions:['新增','修改','删除','审核','导出'], checked:[] },
|
||
{ name:'船名船次', actions:['新增','修改','删除'], checked:[] },
|
||
{ name:'港口', actions:['新增','修改'], checked:[] },
|
||
]
|
||
const EXTRA_ACTIONS = ['订舱','改配','舱单发送','AMS 发送','ISF 发送','电放','换单','滞期费','押箱','核销','对账','统计']
|
||
|
||
let modules = clone(BASE_MODULES)
|
||
let entry = true
|
||
let actionStyle = 'tag'
|
||
let scale = 'normal'
|
||
|
||
function clone(list) {
|
||
return list.map((m) => ({ name: m.name, actions: [...m.actions], checked: [...m.checked] }))
|
||
}
|
||
|
||
function actionHtml(m, i, a) {
|
||
const on = m.checked.includes(a)
|
||
if (actionStyle === 'check') {
|
||
return `<button class="check action-check${on ? ' is-checked' : ''}" type="button" data-act="${a}" data-mod="${i}">
|
||
<span class="box">${CHECK}<i></i></span><span class="label">${a}</span>
|
||
</button>`
|
||
}
|
||
return `<button class="tag${on ? ' is-on' : ''}" type="button" data-act="${a}" data-mod="${i}">${a}</button>`
|
||
}
|
||
|
||
function render() {
|
||
const rows = document.getElementById('rows')
|
||
const body = modules.map((m, i) => {
|
||
const total = m.actions.length
|
||
const count = m.checked.length
|
||
const all = count === total && total > 0
|
||
const partial = count > 0 && !all
|
||
const state = all ? ' is-checked' : partial ? ' is-partial' : ''
|
||
const actions = m.actions.map((a) => actionHtml(m, i, a)).join('')
|
||
return `<div class="row" data-mod="${i}">
|
||
<div class="row__module">
|
||
<button class="check check--use${state}" type="button" data-toggle="${i}">
|
||
<span class="box">${CHECK}<i></i></span><span class="label">${m.name}</span>
|
||
</button>
|
||
</div>
|
||
<div class="row__actions">${actions}</div>
|
||
<div class="row__count">${count}/${total}</div>
|
||
</div>`
|
||
}).join('')
|
||
|
||
rows.innerHTML = body || '<div class="grid__empty">该页面下没有可授权的模块</div>'
|
||
rows.classList.toggle('is-locked', !entry)
|
||
|
||
const entryCheck = document.getElementById('entry-check')
|
||
entryCheck.classList.toggle('is-checked', entry)
|
||
const grantedModules = modules.filter((m) => m.checked.length > 0).length
|
||
document.getElementById('entry-meta').textContent =
|
||
`本页 ${modules.length} 个模块,已授权 ${grantedModules} 个`
|
||
}
|
||
|
||
function syncEntry() {
|
||
if (modules.some((m) => m.checked.length > 0)) entry = true
|
||
render()
|
||
}
|
||
|
||
document.getElementById('rows').addEventListener('click', (e) => {
|
||
const toggle = e.target.closest('[data-toggle]')
|
||
if (toggle) {
|
||
const m = modules[+toggle.dataset.toggle]
|
||
m.checked = m.checked.length === m.actions.length ? [] : [...m.actions]
|
||
return syncEntry()
|
||
}
|
||
const act = e.target.closest('[data-act]')
|
||
if (act) {
|
||
const m = modules[+act.dataset.mod], a = act.dataset.act
|
||
const at = m.checked.indexOf(a)
|
||
at < 0 ? m.checked.push(a) : m.checked.splice(at, 1)
|
||
return syncEntry()
|
||
}
|
||
})
|
||
|
||
document.getElementById('entry-check').addEventListener('click', () => {
|
||
entry = !entry
|
||
if (!entry) modules.forEach((m) => { m.checked = [] })
|
||
render()
|
||
})
|
||
|
||
document.getElementById('grant-all').addEventListener('click', () => {
|
||
entry = true
|
||
modules.forEach((m) => { m.checked = [...m.actions] })
|
||
render()
|
||
})
|
||
|
||
document.getElementById('clear-all').addEventListener('click', () => {
|
||
modules.forEach((m) => { m.checked = [] })
|
||
render()
|
||
})
|
||
|
||
document.querySelectorAll('.demo-btn').forEach((btn) => {
|
||
btn.addEventListener('click', () => {
|
||
if (btn.dataset.style) {
|
||
actionStyle = btn.dataset.style
|
||
document.querySelectorAll('[data-style]').forEach((b) => b.classList.toggle('is-on', b === btn))
|
||
}
|
||
if (btn.dataset.scale) {
|
||
scale = btn.dataset.scale
|
||
const target = scale === 'huge'
|
||
modules[0].actions = target
|
||
? [...BASE_MODULES[0].actions, ...EXTRA_ACTIONS]
|
||
: [...BASE_MODULES[0].actions]
|
||
modules[0].checked = modules[0].checked.filter((a) => modules[0].actions.includes(a))
|
||
document.querySelectorAll('[data-scale]').forEach((b) => b.classList.toggle('is-on', b === btn))
|
||
}
|
||
render()
|
||
})
|
||
})
|
||
document.querySelector('.demo-btn[data-style="tag"]').classList.add('is-on')
|
||
document.querySelector('.demo-btn[data-scale="normal"]').classList.add('is-on')
|
||
|
||
render()
|
||
</script>
|
||
</body>
|
||
</html>
|