/* 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; }