Files
workspace/code/fms/fms-vue/src/components/ui/tree/index.scss
T
2026-09-04 23:24:45 +08:00

334 lines
8.3 KiB
SCSS
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.
/* 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;
}