334 lines
8.3 KiB
SCSS
334 lines
8.3 KiB
SCSS
/* Tree 组件样式:缩进、悬停/聚焦、选中、禁用、复选半选、拖拽指示线、过滤高亮、loading。
|
||
尺寸/间距/圆角/字号/阴影统一引用 --fms-* token,不写死数值。 */
|
||
|
||
.tree {
|
||
color: var(--fms-text);
|
||
font-size: var(--fms-font-size);
|
||
line-height: 1.5;
|
||
font-weight: 500;
|
||
outline: none;
|
||
}
|
||
|
||
/* 单节点行:padding-left 为「固定左留白 + 层级缩进」;flex 布局承载 switcher/checkbox/title/grip */
|
||
.tree-node {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
min-height: var(--fms-tree-node-height);
|
||
padding-left: calc(var(--fms-tree-gutter-x) + var(--fms-tree-indent) * var(--level));
|
||
border-radius: var(--fms-control-radius);
|
||
cursor: default;
|
||
user-select: none;
|
||
transition:
|
||
background-color 0.15s ease,
|
||
color 0.15s ease,
|
||
border-color 0.15s ease,
|
||
box-shadow 0.15s ease;
|
||
}
|
||
|
||
/* hover:仅内容区域背景变化(箭头/缩进区不变),整行保持 pointer 提示可点 */
|
||
.tree-node:hover:not(.is-disabled) {
|
||
cursor: pointer;
|
||
}
|
||
.tree-node:hover:not(.is-disabled) .tree-node-content {
|
||
background: var(--fms-secondary-hover);
|
||
}
|
||
|
||
/* 键盘/鼠标聚焦:focus-visible 描边,避免鼠标点击也出现描边 */
|
||
.tree-node:focus-visible {
|
||
outline: 2px solid var(--fms-primary);
|
||
outline-offset: -2px;
|
||
}
|
||
|
||
/* 选中:仅内容区域主色淡底(整行 hover 背景、箭头/缩进区不受影响)。
|
||
比例取 5%:深色主题(如 #0f172b)在白底上约 #f3f3f4,接近 hover 浅色 #f1f5f9;
|
||
比例过高(10%)时深色主题会算出 #e7e7e9 等偏深灰,显得脏。
|
||
注意 hover 规则 specificity 更高,需连同 hover 一起覆盖,避免悬停时选中态被盖掉 */
|
||
.tree-node.is-selected .tree-node-content,
|
||
.tree-node.is-selected:hover:not(.is-disabled) .tree-node-content {
|
||
background: color-mix(in srgb, var(--fms-primary) 5%, transparent);
|
||
}
|
||
|
||
.tree-node.is-disabled,
|
||
.tree-node.is-disabled .tree-node-content {
|
||
color: var(--fms-disabled-text);
|
||
cursor: not-allowed;
|
||
}
|
||
.tree-node.is-checkbox-disabled .tree-node-content {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
/* 拖拽源:半透明提示 */
|
||
.tree-node.is-dragging {
|
||
opacity: 0.4;
|
||
}
|
||
|
||
/* ---------- 展开/收起图标 ---------- */
|
||
.tree-node-switcher {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex: none;
|
||
width: var(--fms-tree-switcher-size);
|
||
height: var(--fms-tree-switcher-size);
|
||
margin-right: 2px;
|
||
color: inherit;
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
transition:
|
||
background-color 0.15s ease,
|
||
color 0.15s ease;
|
||
}
|
||
.tree-node-switcher:hover:not(.is-placeholder) {
|
||
background: color-mix(in srgb, var(--fms-primary) 10%, transparent);
|
||
}
|
||
.tree-node-switcher.is-placeholder {
|
||
cursor: default;
|
||
}
|
||
/* 展开/收起箭头:黑色(跟随主题文字色),hover 仅显示淡底提示、箭头不变色 */
|
||
.tree-switcher-icon {
|
||
width: var(--fms-tree-icon-size);
|
||
height: var(--fms-tree-icon-size);
|
||
color: var(--fms-text);
|
||
transition: transform 0.2s cubic-bezier(0.2, 0.8, 0.4, 1);
|
||
}
|
||
.tree-switcher-icon.is-open {
|
||
transform: rotate(90deg);
|
||
}
|
||
.tree-spinner {
|
||
width: var(--fms-tree-icon-size);
|
||
height: var(--fms-tree-icon-size);
|
||
color: var(--fms-primary);
|
||
animation: tree-spin 0.8s linear infinite;
|
||
}
|
||
@keyframes tree-spin {
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
/* ---------- 复选框 ---------- */
|
||
.tree-node-checkbox {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
flex: none;
|
||
margin-right: 6px;
|
||
}
|
||
|
||
/* ---------- 标题 ---------- */
|
||
.tree-node-content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
/* 父容器 .tree-node 仅有 min-height(无显式 height),height:100% 无法解析,
|
||
故用 flex 交叉轴拉伸占满整行实际高度(选中背景/圆角才能覆盖整行) */
|
||
align-self: stretch;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 var(--fms-tree-title-padding-x);
|
||
border-radius: var(--fms-control-radius);
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.tree-node.is-block .tree-node-content {
|
||
flex: 1 1 auto;
|
||
}
|
||
|
||
/* 节点图标:位于标题前,继承文字默认色,与标题对齐 */
|
||
.tree-node-icon {
|
||
display: inline-flex;
|
||
flex: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: var(--fms-tree-icon-size);
|
||
height: var(--fms-tree-icon-size);
|
||
margin-right: 8px;
|
||
color: inherit;
|
||
|
||
svg {
|
||
width: var(--fms-tree-icon-size);
|
||
height: var(--fms-tree-icon-size);
|
||
}
|
||
}
|
||
.tree-highlight {
|
||
background: color-mix(in srgb, var(--fms-primary) 22%, transparent);
|
||
color: var(--fms-primary);
|
||
font-weight: 600;
|
||
border-radius: 2px;
|
||
padding: 0 1px;
|
||
}
|
||
|
||
/* ---------- 拖拽手柄(在最左侧) ---------- */
|
||
.tree-node-grip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
flex: none;
|
||
width: var(--fms-tree-grip-size);
|
||
height: var(--fms-tree-grip-size);
|
||
margin-right: 4px;
|
||
color: #b0b7c3;
|
||
cursor: grab;
|
||
/* 从手柄按下时不选中行文本(拖拽由 mousedown 启动,不能 preventDefault) */
|
||
user-select: none;
|
||
}
|
||
.tree-node-grip:active {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
/* ---------- 拖拽落点指示:圆点 + 连接线 ---------- */
|
||
.tree-node.drop-before::before,
|
||
.tree-node.drop-after::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: calc(var(--fms-tree-gutter-x) + var(--fms-tree-indent) * var(--level));
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: var(--fms-primary);
|
||
box-shadow: 0 0 0 2px var(--fms-card);
|
||
z-index: 1;
|
||
}
|
||
.tree-node.drop-before::before {
|
||
top: -4px;
|
||
}
|
||
.tree-node.drop-after::before {
|
||
bottom: -4px;
|
||
}
|
||
.tree-node.drop-before::after,
|
||
.tree-node.drop-after::after {
|
||
content: '';
|
||
position: absolute;
|
||
left: calc(var(--fms-tree-gutter-x) + var(--fms-tree-indent) * var(--level) + 8px);
|
||
right: 0;
|
||
height: var(--fms-tree-drop-line-size);
|
||
background: var(--fms-primary);
|
||
border-radius: var(--fms-tree-drop-line-size);
|
||
}
|
||
.tree-node.drop-before::after {
|
||
top: -1px;
|
||
}
|
||
.tree-node.drop-after::after {
|
||
bottom: -1px;
|
||
}
|
||
.tree-node.drop-inside {
|
||
background: color-mix(in srgb, var(--fms-primary) 12%, transparent);
|
||
outline: 2px solid color-mix(in srgb, var(--fms-primary) 55%, transparent);
|
||
outline-offset: -2px;
|
||
}
|
||
|
||
/* ---------- 展开/收起动画(grid-template-rows 高度过渡) ---------- */
|
||
.tree-node-children {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr);
|
||
grid-template-rows: 1fr;
|
||
overflow: hidden;
|
||
}
|
||
.tree-collapse-enter-active,
|
||
.tree-collapse-leave-active {
|
||
transition:
|
||
grid-template-rows 0.22s cubic-bezier(0.2, 0.8, 0.4, 1),
|
||
opacity 0.18s ease;
|
||
}
|
||
.tree-collapse-enter-from,
|
||
.tree-collapse-leave-to {
|
||
grid-template-rows: 0fr;
|
||
opacity: 0;
|
||
}
|
||
.tree-node-children-inner {
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* ---------- 空态 ---------- */
|
||
.tree-empty {
|
||
padding: 12px 8px;
|
||
color: var(--fms-text-secondary);
|
||
text-align: center;
|
||
}
|
||
|
||
.tree-disabled .tree-node {
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* 尊重系统「减少动态效果」偏好 */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.tree-node,
|
||
.tree-switcher-icon,
|
||
.tree-node-grip,
|
||
.tree-spinner,
|
||
.tree-node-children {
|
||
transition: none;
|
||
animation: none;
|
||
}
|
||
.tree-collapse-enter-active,
|
||
.tree-collapse-leave-active {
|
||
transition: none;
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
* 右键菜单(fixed 定位,挂载到 body)
|
||
* ============================================================ */
|
||
.tree-context-menu {
|
||
position: fixed;
|
||
z-index: 3000;
|
||
min-width: 140px;
|
||
padding: 4px;
|
||
background: var(--fms-card);
|
||
border: 1px solid var(--fms-border);
|
||
border-radius: var(--fms-control-radius);
|
||
box-shadow: var(--fms-shadow);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.tree-context-menu__item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
width: 100%;
|
||
padding: 5px 10px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--fms-text);
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
|
||
&:hover:not(:disabled) {
|
||
background: var(--fms-secondary-hover);
|
||
}
|
||
|
||
&.is-danger {
|
||
color: #f53f3f;
|
||
}
|
||
|
||
&.is-disabled {
|
||
color: var(--fms-disabled-text);
|
||
cursor: not-allowed;
|
||
}
|
||
}
|
||
|
||
.tree-context-menu__icon {
|
||
flex: none;
|
||
}
|
||
|
||
.tree-context-menu__divider {
|
||
height: 1px;
|
||
margin: 4px 8px;
|
||
background: var(--fms-border);
|
||
}
|
||
|
||
/* 淡入淡出动画 */
|
||
.tree-context-fade-enter-active {
|
||
transition: opacity 0.12s ease;
|
||
}
|
||
|
||
.tree-context-fade-leave-active {
|
||
transition: opacity 0.08s ease;
|
||
}
|
||
|
||
.tree-context-fade-enter-from,
|
||
.tree-context-fade-leave-to {
|
||
opacity: 0;
|
||
}
|