| | |
| | | :data="list" |
| | | @node-click="handleNodeClick" |
| | | :expand-on-click-node="false" |
| | | @node-expand="handleNodeExpand" |
| | | @node-collapse="handleNodeCollapse" |
| | | :key="treeKey" |
| | | :default-expanded-keys="expandedKeys" |
| | | :filter-node-method="filterNode" |
| | | :props="{ children: 'children', label: 'label' }" |
| | | highlight-current |
| | | node-key="id" |
| | | style=" |
| | | height: calc(100vh - 190px); |
| | | overflow-y: scroll; |
| | | scrollbar-width: none; |
| | | " |
| | | class="product-tree-scroll" |
| | | style="height: calc(100vh - 190px); overflow-y: auto" |
| | | > |
| | | <template #default="{ node, data }"> |
| | | <div class="custom-tree-node"> |
| | |
| | | <component :is="data.children && data.children.length > 0 |
| | | ? node.expanded ? 'FolderOpened' : 'Folder' : 'Tickets'" /> |
| | | </el-icon> |
| | | {{ data.label }} |
| | | <span class="tree-node-label">{{ data.label }}</span> |
| | | </span> |
| | | <div> |
| | | <el-button |
| | |
| | | > |
| | | 编辑 |
| | | </el-button> |
| | | <el-button type="primary" link @click="openProDia('add', data)" :disabled="node.level >= 3"> |
| | | <el-button type="primary" link @click="openProDia('add', data)"> |
| | | 添加产品 |
| | | </el-button> |
| | | <el-button |
| | |
| | | <el-button type="primary" @click="openModelDia('add')"> |
| | | 新增规格型号 |
| | | </el-button> |
| | | <ImportExcel @uploadSuccess="getModelList" /> |
| | | <ImportExcel :product-id="currentId" @uploadSuccess="getModelList" /> |
| | | <el-button |
| | | type="danger" |
| | | @click="handleDelete" |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref } from "vue"; |
| | | import { nextTick, ref } from "vue"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import { |
| | | addOrEditProduct, |
| | |
| | | const { proxy } = getCurrentInstance(); |
| | | const tree = ref(null); |
| | | const containerRef = ref(null); |
| | | const treeKey = ref(0); |
| | | const expandedKeySet = new Set(); |
| | | const EXPANDED_STORAGE_KEY = "basicData_product_tree_expanded_keys_v2"; |
| | | |
| | | const loadExpandedKeys = () => { |
| | | if (typeof window === "undefined") { |
| | | return []; |
| | | } |
| | | try { |
| | | const saved = localStorage.getItem(EXPANDED_STORAGE_KEY); |
| | | return saved ? JSON.parse(saved) : []; |
| | | } catch (error) { |
| | | console.error(error); |
| | | return []; |
| | | } |
| | | }; |
| | | |
| | | const saveExpandedKeys = () => { |
| | | if (typeof window === "undefined") { |
| | | return; |
| | | } |
| | | localStorage.setItem( |
| | | EXPANDED_STORAGE_KEY, |
| | | JSON.stringify(Array.from(expandedKeySet)) |
| | | ); |
| | | }; |
| | | |
| | | loadExpandedKeys().forEach((key) => expandedKeySet.add(key)); |
| | | |
| | | const syncExpandedKeysFromTree = () => { |
| | | const keys = []; |
| | | const walk = (nodes) => { |
| | | (nodes || []).forEach((item) => { |
| | | if (item.expanded && item.data?.id !== undefined) { |
| | | keys.push(item.data.id); |
| | | } |
| | | if (item.childNodes && item.childNodes.length) { |
| | | walk(item.childNodes); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | walk(tree.value?.root?.childNodes); |
| | | expandedKeySet.clear(); |
| | | keys.forEach((key) => expandedKeySet.add(key)); |
| | | expandedKeys.value = keys; |
| | | saveExpandedKeys(); |
| | | }; |
| | | |
| | | const normalizeExpandedKeys = (treeData) => { |
| | | const parentMap = new Map(); |
| | | const walk = (nodes, parentId = null) => { |
| | | (nodes || []).forEach((item) => { |
| | | parentMap.set(item.id, parentId); |
| | | if (item.children && item.children.length) { |
| | | walk(item.children, item.id); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | walk(treeData); |
| | | |
| | | const normalizedKeys = Array.from(expandedKeySet).filter((key) => { |
| | | if (!parentMap.has(key)) { |
| | | return false; |
| | | } |
| | | let currentId = key; |
| | | while (parentMap.has(currentId)) { |
| | | const parentId = parentMap.get(currentId); |
| | | if (!parentId) { |
| | | return true; |
| | | } |
| | | if (!expandedKeySet.has(parentId)) { |
| | | return false; |
| | | } |
| | | currentId = parentId; |
| | | } |
| | | return true; |
| | | }); |
| | | |
| | | if (normalizedKeys.length !== expandedKeySet.size) { |
| | | expandedKeySet.clear(); |
| | | normalizedKeys.forEach((key) => expandedKeySet.add(key)); |
| | | saveExpandedKeys(); |
| | | } |
| | | }; |
| | | |
| | | const productDia = ref(false); |
| | | const modelDia = ref(false); |
| | |
| | | productName: "", |
| | | }, |
| | | rules: { |
| | | productName: [{ required: true, message: "请输入", trigger: "blur" }], |
| | | productName: [ |
| | | { required: true, message: "请输入", trigger: "blur" }, |
| | | { max: 20, message: "产品名称不能超过20个字符", trigger: "blur" }, |
| | | ], |
| | | }, |
| | | modelForm: { |
| | | model: "", |
| | |
| | | treeLoad.value = true; |
| | | productTreeList() |
| | | .then((res) => { |
| | | list.value = res; |
| | | list.value.forEach((a) => { |
| | | expandedKeys.value.push(a.label); |
| | | list.value = res || []; |
| | | normalizeExpandedKeys(list.value); |
| | | expandedKeys.value = Array.from(expandedKeySet); |
| | | treeKey.value += 1; |
| | | nextTick(() => { |
| | | tree.value?.setDefaultExpandedKeys?.(expandedKeys.value); |
| | | }); |
| | | treeLoad.value = false; |
| | | }) |
| | | .catch((err) => { |
| | | console.error(err); |
| | | }) |
| | | .finally(() => { |
| | | treeLoad.value = false; |
| | | }); |
| | | }; |
| | | const handleNodeExpand = (data) => { |
| | | nextTick(syncExpandedKeysFromTree); |
| | | }; |
| | | const handleNodeCollapse = (data, node) => { |
| | | node?.eachNode?.((item) => { |
| | | item.collapse(); |
| | | }); |
| | | nextTick(syncExpandedKeysFromTree); |
| | | }; |
| | | // 过滤产品树 |
| | | const searchFilter = () => { |
| | |
| | | display: flex; |
| | | } |
| | | .left { |
| | | width: 380px; |
| | | width: 450px; |
| | | min-width: 450px; |
| | | padding: 16px; |
| | | background: #ffffff; |
| | | } |
| | | .right { |
| | | width: calc(100% - 380px); |
| | | flex: 1; |
| | | min-width: 0; |
| | | padding: 16px; |
| | | margin-left: 20px; |
| | | background: #ffffff; |
| | | } |
| | | .custom-tree-node { |
| | | flex: 1; |
| | | min-width: 0; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | |
| | | padding-right: 8px; |
| | | } |
| | | .tree-node-content { |
| | | flex: 1; |
| | | min-width: 0; |
| | | display: flex; |
| | | align-items: center; /* 垂直居中 */ |
| | | align-items: center; |
| | | height: 100%; |
| | | overflow: hidden; |
| | | } |
| | | .tree-node-content .orange-icon { |
| | | flex-shrink: 0; |
| | | } |
| | | .tree-node-label { |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | .orange-icon { |
| | | color: orange; |
| | | font-size: 18px; |
| | | margin-right: 8px; /* 图标与文字之间加点间距 */ |
| | | } |
| | | .product-tree-scroll { |
| | | scrollbar-width: thin; |
| | | scrollbar-color: #c0c4cc #f5f7fa; |
| | | } |
| | | .product-tree-scroll::-webkit-scrollbar { |
| | | width: 8px; |
| | | } |
| | | .product-tree-scroll::-webkit-scrollbar-track { |
| | | background: #f5f7fa; |
| | | border-radius: 4px; |
| | | } |
| | | .product-tree-scroll::-webkit-scrollbar-thumb { |
| | | background: #c0c4cc; |
| | | border-radius: 4px; |
| | | } |
| | | .product-tree-scroll::-webkit-scrollbar-thumb:hover { |
| | | background: #909399; |
| | | } |
| | | </style> |