|
|
@@ -0,0 +1,663 @@
|
|
|
+<template>
|
|
|
+ <!-- 公共树形组件:基于 Element UI el-tree 封装 -->
|
|
|
+ <!-- common_tree_wrapper:用作非 scoped 样式的全局命名空间,防止样式泄漏到其他组件 -->
|
|
|
+ <div class="common_tree_wrapper common_tree">
|
|
|
+ <!--
|
|
|
+ el-tree 核心配置说明:
|
|
|
+ - ref: 用于父组件通过 this.$refs 调用 el-tree 原生方法(如 setCheckedKeys、getCheckedNodes 等)
|
|
|
+ - data: 树形数据源(由父组件通过 props 传入)
|
|
|
+ - props: 配置 el-tree 如何读取数据结构中的字段(label/children)
|
|
|
+ - node-key: 每个树节点的唯一标识字段,用于选中/高亮定位
|
|
|
+ - show-checkbox: 开启多选复选框
|
|
|
+ - highlight-current: 高亮当前点击选中的节点
|
|
|
+ - default-expand-all: 默认展开所有节点
|
|
|
+ -->
|
|
|
+ <el-tree
|
|
|
+ ref="commonTree"
|
|
|
+ class="tree_container"
|
|
|
+ :data="treeData"
|
|
|
+ :props="treeProps"
|
|
|
+ :node-key="nodeKey"
|
|
|
+ :show-checkbox="showCheckbox"
|
|
|
+ :highlight-current="highlightCurrent"
|
|
|
+ :default-expand-all="defaultExpandAll"
|
|
|
+ :default-expanded-keys="defaultExpandedKeys"
|
|
|
+ :current-node-key="currentNodeKey"
|
|
|
+ :expand-on-click-node="expandOnClickNode"
|
|
|
+ @check="handleNodeCheck"
|
|
|
+ >
|
|
|
+ <!--
|
|
|
+ 自定义树节点内容插槽(slot-scope)
|
|
|
+ - node: 当前节点的完整数据对象(label、children、data 等)
|
|
|
+ - data: 当前节点绑定的用户数据(即 treeData 中的每一项)
|
|
|
+ -->
|
|
|
+ <span class="custom_tree_node" slot-scope="{ node, data }">
|
|
|
+ <!-- 节点名称区域 -->
|
|
|
+ <span class="node_label" :title="data[treeProps.label]">
|
|
|
+ <!-- 名称文本单独包裹一层,确保省略号只作用于真正承载文字的元素 -->
|
|
|
+ <span class="node_label_text">{{ data[treeProps.label] }}</span>
|
|
|
+ <!-- 右侧计数(可选,数据存在 count 字段时显示) -->
|
|
|
+ <span
|
|
|
+ class="node_count"
|
|
|
+ v-if="data.count !== undefined && data.count !== null"
|
|
|
+ >
|
|
|
+ ({{ data.count }})
|
|
|
+ </span>
|
|
|
+ </span>
|
|
|
+
|
|
|
+ <!--
|
|
|
+ AI 讲解按钮(Element UI 文字按钮 type="text")
|
|
|
+ - 默认隐藏(opacity: 0)
|
|
|
+ - 通过父级 hover 显示
|
|
|
+ - @click.stop 阻止冒泡,避免触发树节点点击
|
|
|
+ -->
|
|
|
+ <el-button
|
|
|
+ class="ai_explain_btn"
|
|
|
+ type="text"
|
|
|
+ size="mini"
|
|
|
+ icon=""
|
|
|
+ @click.stop="HandleAiExplain(node, data)"
|
|
|
+ >
|
|
|
+ <img
|
|
|
+ src="@/assets/knowledgeGraph/aiIcon.png"
|
|
|
+ alt="AI讲解"
|
|
|
+ style="width: 16px; height: 16px"
|
|
|
+ />
|
|
|
+ <span>AI讲解</span>
|
|
|
+ </el-button>
|
|
|
+ </span>
|
|
|
+ </el-tree>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+/**
|
|
|
+ * CommonTree 公共树形组件
|
|
|
+ *
|
|
|
+ * 功能说明:
|
|
|
+ * 1. 基于 Element UI el-tree 二次封装,支持数据传入渲染
|
|
|
+ * 2. 支持多选复选框
|
|
|
+ * 3. 节点勾选回传(仅发送勾选节点的 key 数组,如 knowledgeId 列表)
|
|
|
+ * 4. 鼠标悬停节点行显示 "AI讲解" 按钮,点击打印并回传当前节点
|
|
|
+ *
|
|
|
+ * 父组件使用示例:
|
|
|
+ * <CommonTree
|
|
|
+ * :tree-data="knowledgeTreeData"
|
|
|
+ * :tree-props="{ label: 'knowledgeName', children: 'children' }"
|
|
|
+ * node-key="knowledgeId"
|
|
|
+ * show-checkbox
|
|
|
+ * @nodeCheck="handleTreeCheck"
|
|
|
+ * @ai-explain="handleAiExplain"
|
|
|
+ * />
|
|
|
+ *
|
|
|
+ * 父组件 handleTreeCheck 接收参数:
|
|
|
+ * - checkedKeys: Array — 所有勾选节点的 key 数组(勾选父节点时包含其全部子节点 key)
|
|
|
+ */
|
|
|
+export default {
|
|
|
+ name: "CommonTree",
|
|
|
+
|
|
|
+ // ==================== Props 配置(由父组件传入) ====================
|
|
|
+ props: {
|
|
|
+ /**
|
|
|
+ * 树形数据源
|
|
|
+ * 数据结构示例:
|
|
|
+ * [
|
|
|
+ * { id: 1, label: '节点1', count: 10, children: [
|
|
|
+ * { id: 11, label: '子节点1', count: 5 }
|
|
|
+ * ]}
|
|
|
+ * ]
|
|
|
+ */
|
|
|
+ treeData: {
|
|
|
+ type: Array,
|
|
|
+ default: () => [],
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * el-tree 的 props 配置,用于映射数据结构的字段
|
|
|
+ * 默认:{ label: 'label', children: 'children' }
|
|
|
+ * 如使用其他字段名,可传:{ label: 'knowledgeName', children: 'childList' }
|
|
|
+ */
|
|
|
+ treeProps: {
|
|
|
+ type: Object,
|
|
|
+ default: () => ({
|
|
|
+ label: "label",
|
|
|
+ children: "children",
|
|
|
+ }),
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 节点唯一标识字段名
|
|
|
+ * 用于选中、高亮、展开等操作的定位
|
|
|
+ */
|
|
|
+ nodeKey: {
|
|
|
+ type: String,
|
|
|
+ default: "id",
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 是否显示多选复选框 */
|
|
|
+ showCheckbox: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 是否高亮当前点击的节点 */
|
|
|
+ highlightCurrent: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true,
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 是否默认展开所有节点 */
|
|
|
+ defaultExpandAll: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true,
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 默认展开的节点 key 数组
|
|
|
+ * 与 defaultExpandAll 互斥使用
|
|
|
+ */
|
|
|
+ defaultExpandedKeys: {
|
|
|
+ type: Array,
|
|
|
+ default: () => [],
|
|
|
+ },
|
|
|
+
|
|
|
+ /** 当前高亮选中的节点 key */
|
|
|
+ currentNodeKey: {
|
|
|
+ type: [String, Number],
|
|
|
+ default: "",
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 点击节点时是否展开/收起
|
|
|
+ * false:只有点击小箭头才展开;点击节点文字只触发选中
|
|
|
+ * true:点击节点文字也会触发展开/收起
|
|
|
+ */
|
|
|
+ expandOnClickNode: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ inject: ['openAiRobot'],//注入Ai机器人组件调用方法
|
|
|
+ // ==================== 数据状态 ====================
|
|
|
+ data() {
|
|
|
+ return {};
|
|
|
+ },
|
|
|
+ computed:{
|
|
|
+
|
|
|
+ subjectName()
|
|
|
+ {
|
|
|
+ return this.$store.state.target.subjectName;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ },
|
|
|
+ // ==================== 生命周期 ====================
|
|
|
+ mounted() {
|
|
|
+ if (Array.isArray(this.treeData) && this.treeData.length > 0) {
|
|
|
+ clearTimeout(this._defaultSelectTimer);
|
|
|
+ this._defaultSelectTimer = setTimeout(() => {
|
|
|
+ this.applyDefaultFirstNode(this.treeData);
|
|
|
+ }, 100);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ beforeDestroy() {
|
|
|
+ clearTimeout(this._defaultSelectTimer);
|
|
|
+ },
|
|
|
+
|
|
|
+ // ==================== Watch ====================
|
|
|
+ watch: {
|
|
|
+ treeData: {
|
|
|
+ handler(newVal) {
|
|
|
+ if (!Array.isArray(newVal) || newVal.length === 0) return;
|
|
|
+ // 用 setTimeout 确保 el-tree 内部节点已完全渲染(比 $nextTick 更可靠)
|
|
|
+ clearTimeout(this._defaultSelectTimer);
|
|
|
+ this._defaultSelectTimer = setTimeout(() => {
|
|
|
+ this.applyDefaultFirstNode(newVal);
|
|
|
+ }, 100);
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+
|
|
|
+ // ==================== 方法 ====================
|
|
|
+ methods: {
|
|
|
+ /**
|
|
|
+ * 树节点勾选事件(多选,showCheckbox=true 时生效)
|
|
|
+ * 将所有勾选节点的 key(即 nodeKey 字段值,如 knowledgeId)数组发给父组件
|
|
|
+ * 勾选父节点时,el-tree 默认级联勾选其全部子节点,故数组中包含父节点及全部子节点 key
|
|
|
+ * @param {Object} data - 当前勾选/取消的节点数据
|
|
|
+ * @param {Object} checkedInfo - 勾选状态详情 { checkedNodes, checkedKeys, halfCheckedKeys, halfCheckedNodes }
|
|
|
+ */
|
|
|
+ handleNodeCheck(data, checkedInfo) {
|
|
|
+ console.log("当前节点数据:", data);
|
|
|
+ console.log("勾选状态详情:", checkedInfo);
|
|
|
+ this.$emit("nodeCheck", checkedInfo.checkedKeys);
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * AI 讲解按钮点击事件
|
|
|
+ * 1. 打印当前节点数据(方便调试)
|
|
|
+ * 2. 通过 $emit 回传给父组件处理
|
|
|
+ *
|
|
|
+ * @param {Object} node - el-tree 节点对象
|
|
|
+ * @param {Object} data - 当前节点绑定的用户数据
|
|
|
+ */
|
|
|
+ HandleAiExplain(node, data)
|
|
|
+ {
|
|
|
+ console.log("当前节点数据:", data);
|
|
|
+ let content = "科目:"+this.subjectName+",知识点:"+data.knowledgeName;
|
|
|
+ this.openAiRobot(content);
|
|
|
+
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 对外暴露方法:获取所有勾选的节点(仅多选模式 showCheckbox=true 有效)
|
|
|
+ * @param {Boolean} leafOnly - 是否只返回叶子节点
|
|
|
+ * @returns {Array} 选中的节点对象数组
|
|
|
+ */
|
|
|
+ getCheckedNodes(leafOnly = false) {
|
|
|
+ if (!this.$refs.commonTree) return [];
|
|
|
+ return this.$refs.commonTree.getCheckedNodes(leafOnly);
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 对外暴露方法:获取所有勾选节点的 key 数组
|
|
|
+ * @param {Boolean} leafOnly
|
|
|
+ * @returns {Array} 选中的节点 key 数组
|
|
|
+ */
|
|
|
+ getCheckedKeys(leafOnly = false) {
|
|
|
+ if (!this.$refs.commonTree) return [];
|
|
|
+ return this.$refs.commonTree.getCheckedKeys(leafOnly);
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 对外暴露方法:通过 keys 设置哪些节点被选中
|
|
|
+ * @param {Array} keys - 要选中的节点 key 数组
|
|
|
+ * @param {Boolean} leafOnly - 是否只设置叶子节点
|
|
|
+ */
|
|
|
+ setCheckedKeys(keys, leafOnly = false) {
|
|
|
+ if (!this.$refs.commonTree) return;
|
|
|
+ this.$refs.commonTree.setCheckedKeys(keys, leafOnly);
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 对外暴露方法:清空所有选中节点
|
|
|
+ */
|
|
|
+ clearChecked() {
|
|
|
+ if (!this.$refs.commonTree) return;
|
|
|
+ this.$refs.commonTree.setCheckedKeys([]);
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 对外暴露方法:设置当前高亮的节点
|
|
|
+ * @param {String|Number} key - 节点 key
|
|
|
+ */
|
|
|
+ setCurrentKey(key) {
|
|
|
+ if (!this.$refs.commonTree) return;
|
|
|
+ this.$refs.commonTree.setCurrentKey(key);
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 默认勾选并展开第一个一级节点
|
|
|
+ * 直接通过 el-tree 内部 store 获取节点实例,避免 node-key 与数据字段不匹配导致的 key 查找失败
|
|
|
+ * @param {Array} data - 树形数据
|
|
|
+ */
|
|
|
+ applyDefaultFirstNode(data) {
|
|
|
+ if (!data || data.length === 0) return;
|
|
|
+ const treeRef = this.$refs.commonTree;
|
|
|
+ if (!treeRef || !treeRef.store || !treeRef.store.root) return;
|
|
|
+
|
|
|
+ // 1. 获取第一个顶层节点实例(绕过 key 匹配)
|
|
|
+ const rootNode = treeRef.store.root;
|
|
|
+ const childNodes = rootNode.childNodes;
|
|
|
+ if (!childNodes || childNodes.length === 0) return;
|
|
|
+
|
|
|
+ const firstNodeInstance = childNodes[0];
|
|
|
+ if (!firstNodeInstance) return;
|
|
|
+
|
|
|
+ // 2. 默认勾选第一个节点及其全部子节点(deep=true 级联勾选所有子节点)
|
|
|
+ if (this.showCheckbox) {
|
|
|
+ firstNodeInstance.setChecked(true, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 默认高亮选中第一个节点
|
|
|
+ treeRef.setCurrentKey(firstNodeInstance.key);
|
|
|
+
|
|
|
+ // 4. 默认展开第一个节点及其全部子节点(所有层级)
|
|
|
+ const expandAllChildren = (node) => {
|
|
|
+ if (!node) return;
|
|
|
+ node.expanded = true;
|
|
|
+ const subs = node.childNodes;
|
|
|
+ if (Array.isArray(subs) && subs.length > 0) {
|
|
|
+ subs.forEach((s) => expandAllChildren(s));
|
|
|
+ }
|
|
|
+ };
|
|
|
+ expandAllChildren(firstNodeInstance);
|
|
|
+
|
|
|
+ // 5. 触发 nodeCheck 事件,将所有勾选节点的 key 数组发给父组件
|
|
|
+ // 仅发送一个数组参数(包含第一个节点及其所有子节点的 key)
|
|
|
+ if (this.showCheckbox) {
|
|
|
+ const checkedKeys = treeRef.getCheckedKeys();
|
|
|
+ console.log("当前节点数据:", checkedKeys);
|
|
|
+ this.$emit("nodeCheck", checkedKeys);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<!--
|
|
|
+ ==========================================================================
|
|
|
+ 样式分两块:
|
|
|
+ 1. 下面这个不带 scoped 的 style 块:专门覆盖 Element UI 原生类名(连接线、padding、间距、选中态)
|
|
|
+ - scoped + :deep 在 el-tree 深层级嵌套下经常不生效,改用非 scoped 搭配组件唯一根类
|
|
|
+ .common_tree_wrapper 做命名空间隔离,不会污染全局其他 el-tree
|
|
|
+ 2. 后面带 scoped 的 style 块:放自定义节点内容(custom_tree_node / node_label / ai_explain_btn 等)
|
|
|
+ ==========================================================================
|
|
|
+-->
|
|
|
+<style lang="scss">
|
|
|
+/* ============== 可配置常量 ============== */
|
|
|
+$line_color: #dcdfe6; // 连接线颜色(虚线)
|
|
|
+$line_style: dashed; // 连接线样式
|
|
|
+$line_width: 1px; // 连接线宽度
|
|
|
+$indent_size: 35px; // 每层缩进
|
|
|
+$node_height: 36px; // 单行节点高度
|
|
|
+$node_padding_l: 15px; // 节点左侧 padding (用户要求:15px)
|
|
|
+$node_gap: 10px; // 节点上下间距 (用户要求:10px)
|
|
|
+$label_color: #333333; // 节点文字 (用户要求)
|
|
|
+$label_active: #2e64fa; // 选中节点文字 (用户要求)
|
|
|
+$label_fs: 14px; // 文字字号 (用户要求)
|
|
|
+$label_fw: 400; // 文字字重 (用户要求)
|
|
|
+$btn_color: #4f3eff; // AI讲解按钮颜色 (用户要求)
|
|
|
+
|
|
|
+/* =========================================================
|
|
|
+ 命名空间:.common_tree_wrapper(只影响本组件)
|
|
|
+ ========================================================= */
|
|
|
+.common_tree_wrapper {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ box-sizing: border-box;
|
|
|
+ padding: 8px 0;
|
|
|
+
|
|
|
+ /* ===================== 基础 ===================== */
|
|
|
+ .tree_container,
|
|
|
+ .el-tree {
|
|
|
+ width: 100%;
|
|
|
+ min-width: max-content; /* 整棵树按最宽节点内容撑开,所有节点行宽度一致 */
|
|
|
+ background: transparent;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 允许每个节点内容宽度超过容器,从而触发父容器横向滚动条 */
|
|
|
+ .el-tree-node__content {
|
|
|
+ padding-left: 0 !important;
|
|
|
+ padding-right: 0px;
|
|
|
+ height: $node_height;
|
|
|
+ line-height: $node_height;
|
|
|
+ background: transparent;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin-top: 0;
|
|
|
+ margin-bottom: 0;
|
|
|
+ font-weight: $label_fw;
|
|
|
+ font-size: $label_fs;
|
|
|
+ color: $label_color;
|
|
|
+ width: 100%; /* 所有节点行跟随树宽度,保持一致 */
|
|
|
+ overflow: visible; /* 取消默认内容裁剪 */
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background-color: #f0f7ff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 第一级也统一 15px */
|
|
|
+ .el-tree > .el-tree-node > .el-tree-node__content {
|
|
|
+ padding-left: $node_padding_l !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ===================== 自定义展开收起图标样式 ===================== */
|
|
|
+ .el-tree-node__expand-icon {
|
|
|
+ width: 16px;
|
|
|
+ height: 16px;
|
|
|
+ display: flex !important;
|
|
|
+ align-items: center !important;
|
|
|
+ justify-content: center !important;
|
|
|
+ cursor: pointer;
|
|
|
+ margin-right: 8px;
|
|
|
+ padding: 0 !important;
|
|
|
+ border: none !important;
|
|
|
+ /* 去掉Element UI默认的transform旋转过渡动画,直接切换图标 */
|
|
|
+ transform: none !important;
|
|
|
+ transition: none !important;
|
|
|
+ }
|
|
|
+ /* 去掉展开状态的transform旋转 */
|
|
|
+ .el-tree-node__expand-icon.expanded {
|
|
|
+ transform: none !important;
|
|
|
+ -webkit-transform: none !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 隐藏Element UI默认的箭头伪元素 */
|
|
|
+ .el-tree-node__expand-icon::before {
|
|
|
+ display: none !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 收起状态:显示加号 icon_jia */
|
|
|
+ .el-tree-node__expand-icon:not(.expanded):not(.is-leaf)::after {
|
|
|
+ content: "\e64e";
|
|
|
+ font-family: "iconfont" !important;
|
|
|
+ font-size: 16px;
|
|
|
+ color: #e0e3e9;
|
|
|
+ display: inline-block;
|
|
|
+ width: 16px;
|
|
|
+ height: 16px;
|
|
|
+ line-height: 16px;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 展开状态:显示减号 icon_jian */
|
|
|
+ .el-tree-node__expand-icon.expanded:not(.is-leaf)::after {
|
|
|
+ content: "\e64f";
|
|
|
+ font-family: "iconfont" !important;
|
|
|
+ font-size: 16px;
|
|
|
+ color: #2e64fa;
|
|
|
+ display: inline-block;
|
|
|
+ width: 16px;
|
|
|
+ height: 16px;
|
|
|
+ line-height: 16px;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 叶子节点的占位符,保持对齐一致 */
|
|
|
+ .el-tree-node__expand-icon.is-leaf {
|
|
|
+ width: 0 !important;
|
|
|
+ height: 0 !important;
|
|
|
+ display: inline-block !important;
|
|
|
+ cursor: default;
|
|
|
+ visibility: hidden;
|
|
|
+ margin-right: 8px;
|
|
|
+ padding: 0 !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ===================== 虚线连接线(核心) ===================== */
|
|
|
+
|
|
|
+ /* 1. 竖虚线:画在 children 容器的左侧,贯穿所有子节点 */
|
|
|
+ .el-tree-node .el-tree-node__children {
|
|
|
+ overflow: visible;
|
|
|
+ position: relative;
|
|
|
+ margin-left: 0;
|
|
|
+ padding-left: 20px; // 层级缩进
|
|
|
+
|
|
|
+ &::before {
|
|
|
+ content: "";
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ bottom: $node_gap + $node_height / 2; // 末端停在最后一个节点"半高"处,和横线接上
|
|
|
+ left: 18px; // 竖线 x 坐标
|
|
|
+ width: $line_width;
|
|
|
+ border-left: $line_width $line_style $line_color;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 2. 横虚线:画在每个节点行的左侧,从竖线方向延伸到文字左边 */
|
|
|
+ .el-tree-node {
|
|
|
+ position: relative;
|
|
|
+
|
|
|
+ > .el-tree-node__content::before {
|
|
|
+ content: "";
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: -($indent_size - 18px); // 起点对齐竖线
|
|
|
+ width: $indent_size - 20px; // 横线长度
|
|
|
+ height: 0;
|
|
|
+ border-top: $line_width $line_style $line_color;
|
|
|
+ pointer-events: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 3. 第一级节点不画横线 */
|
|
|
+ .el-tree > .el-tree-node > .el-tree-node__content::before {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 4. 最后一个子节点:竖线在横线之下的部分用白色盖掉,形成"L"拐角 */
|
|
|
+ .el-tree-node > .el-tree-node__children > .el-tree-node:last-child {
|
|
|
+ > .el-tree-node__content::after {
|
|
|
+ content: "";
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ bottom: -$node_gap;
|
|
|
+ left: 18px - $indent_size; // 与竖线同 x
|
|
|
+ width: $line_width + 2px;
|
|
|
+ background: #fff; // 白色遮挡(面板必须是白色背景)
|
|
|
+ z-index: 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ===================== 选中 & 复选框 ===================== */
|
|
|
+ .el-tree-node.is-current > .el-tree-node__content {
|
|
|
+ background-color: #e6f0ff;
|
|
|
+ color: $label_active; // 让文字内容(含原生 el-tree label 模式)也变色
|
|
|
+
|
|
|
+ /* 额外命中自定义 slot 中的 .node_label,防止 scoped 里写的 #333 覆盖掉选中色 */
|
|
|
+ .node_label {
|
|
|
+ color: $label_active !important;
|
|
|
+ font-weight: $label_fw;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-checkbox {
|
|
|
+ margin-right: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ===================== AI 讲解按钮(Element UI 文字按钮) =====================
|
|
|
+ 字体 400 / 14 / #4F3EFF;opacity 0 默认隐藏,hover 节点行显示
|
|
|
+ */
|
|
|
+
|
|
|
+ .ai_explain_btn.el-button--text {
|
|
|
+ display: inline-flex !important;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 4px;
|
|
|
+ padding: 4px 6px !important;
|
|
|
+ height: auto !important;
|
|
|
+ min-height: 24px;
|
|
|
+ margin-left: 8px;
|
|
|
+ opacity: 0;
|
|
|
+ transition: opacity 0.2s ease;
|
|
|
+ white-space: nowrap;
|
|
|
+ flex-shrink: 0;
|
|
|
+ position: relative;
|
|
|
+ z-index: 3;
|
|
|
+ font-weight: 400; // 用户要求
|
|
|
+ font-size: 14px; // 用户要求
|
|
|
+ color: $btn_color !important; // 用户要求:#4F3EFF
|
|
|
+
|
|
|
+ // 确保内部img和span都居中对齐
|
|
|
+ img {
|
|
|
+ display: inline-block;
|
|
|
+ vertical-align: middle;
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin-right: 4px;
|
|
|
+ }
|
|
|
+ span {
|
|
|
+ display: inline-block;
|
|
|
+ vertical-align: middle;
|
|
|
+ line-height: 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .ai_explain_btn.el-button--text:hover {
|
|
|
+ color: darken($btn_color, 10%) !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ai_explain_btn .ai_icon {
|
|
|
+ display: inline-block;
|
|
|
+ width: 16px;
|
|
|
+ height: 16px;
|
|
|
+ line-height: 16px;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 10px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #fff;
|
|
|
+ background: $btn_color;
|
|
|
+ border-radius: 3px;
|
|
|
+ font-style: normal;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .ai_explain_btn.el-button--text:hover .ai_icon {
|
|
|
+ background: darken($btn_color, 10%);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* hover 节点行时,让 AI 讲解按钮淡入 */
|
|
|
+ .el-tree-node__content:hover .ai_explain_btn.el-button--text {
|
|
|
+ opacity: 1 !important;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|
|
|
+
|
|
|
+<!-- ===============================================================
|
|
|
+ 以下 scoped 样式块:只处理"自定义节点内容"(我们自己模板里写的)
|
|
|
+ 类名是自定义的,能被 scoped 正常命中,无需 :deep
|
|
|
+ =============================================================== -->
|
|
|
+<style lang="scss" scoped>
|
|
|
+.custom_tree_node {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ flex: 1;
|
|
|
+ width: 100%; /* 占满整个节点行宽度(跟随树整体宽度) */
|
|
|
+ padding-right: 8px;
|
|
|
+ height: 36px;
|
|
|
+ line-height: 36px;
|
|
|
+ white-space: nowrap;
|
|
|
+ position: relative;
|
|
|
+ z-index: 2;
|
|
|
+
|
|
|
+ /* ===== 节点文字(用户要求:400 / 14px / #333333) ===== */
|
|
|
+ .node_label {
|
|
|
+ flex: 1;
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ font-weight: 400;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #333333;
|
|
|
+ white-space: nowrap;
|
|
|
+
|
|
|
+ /* 名称文本:完整显示,不省略 */
|
|
|
+ .node_label_text {
|
|
|
+ display: inline-block;
|
|
|
+ white-space: nowrap;
|
|
|
+ line-height: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 计数(60):灰色小字,不收缩 */
|
|
|
+ .node_count {
|
|
|
+ color: #909399;
|
|
|
+ margin-left: 4px;
|
|
|
+ font-size: 13px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|