Files
workspace/code/fms/分配权限抽屉布局草图.html
T
2026-09-21 17:33:45 +08:00

390 lines
17 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.
<!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>