index.vue 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. <script lang="ts" setup>
  2. import { ref, watch, onMounted, nextTick } from 'vue'
  3. import { ElTree } from 'element-plus'
  4. import * as DeptApi from '@/api/system/dept'
  5. // 1. 引入需要的收缩/展开图标
  6. import { Search, CaretLeft, CaretRight } from '@element-plus/icons-vue'
  7. const props = defineProps({
  8. deptId: {
  9. type: Number,
  10. required: true
  11. },
  12. modelValue: {
  13. type: String,
  14. default: undefined
  15. },
  16. contractName: {
  17. type: String,
  18. default: undefined
  19. },
  20. title: {
  21. type: String,
  22. default: '井'
  23. }
  24. })
  25. const emits = defineEmits(['update:modelValue', 'node-click', 'update:contractName'])
  26. // --- 展开/收缩状态 ---
  27. const isCollapsed = ref(false)
  28. // --- 原有业务逻辑保持不变 ---
  29. const wellName = ref('')
  30. interface Tree {
  31. label: string
  32. value: string
  33. children?: Tree[]
  34. type: '1' | '2'
  35. rawData: any
  36. }
  37. const deptList = ref<Tree[]>([])
  38. const treeRef = ref<InstanceType<typeof ElTree>>()
  39. const expandedKeys = ref<string[]>([])
  40. const loadTree = async () => {
  41. try {
  42. const res = await DeptApi.getTaskWellNames(props.deptId, wellName.value)
  43. // --- 数据处理开始 ---
  44. const parentMap = new Map<number, Tree>()
  45. const treeData: Tree[] = []
  46. // 第一步:先找出所有的 Type 1 (父级),建立映射
  47. res.forEach((item: any) => {
  48. if (item.type === '1') {
  49. const node: Tree = {
  50. label: item.projectName,
  51. value: item.projectName,
  52. type: '1',
  53. children: [],
  54. rawData: item
  55. }
  56. parentMap.set(item.projectId, node)
  57. treeData.push(node)
  58. }
  59. })
  60. res.forEach((item: any) => {
  61. if (item.type === '2') {
  62. const parent = parentMap.get(item.projectId)
  63. const childNode: Tree = {
  64. label: item.wellName,
  65. value: item.wellName,
  66. type: '2',
  67. rawData: item
  68. }
  69. if (parent) {
  70. parent.children?.push(childNode)
  71. }
  72. }
  73. })
  74. deptList.value = treeData
  75. if (!props.modelValue && treeData.length > 0 && treeData[0].children?.length) {
  76. const firstChild = treeData[0].children[0]
  77. emits('update:modelValue', firstChild.value)
  78. nextTick(() => {
  79. if (treeRef.value) {
  80. treeRef.value.setCurrentKey(firstChild.value)
  81. expandedKeys.value = [treeData[0].value]
  82. }
  83. })
  84. } else if (props.modelValue) {
  85. nextTick(() => {
  86. if (treeRef.value) {
  87. treeRef.value.setCurrentKey(props.modelValue)
  88. expandedKeys.value = treeData.map((node) => node.value)
  89. }
  90. })
  91. }
  92. } catch (error) {
  93. console.error('加载井名失败:', error)
  94. }
  95. }
  96. const handleNodeClick = (data: Tree) => {
  97. if (data.type === '1') {
  98. emits('update:contractName', data.value)
  99. emits('update:modelValue', '')
  100. } else if (data.type === '2') {
  101. emits('update:modelValue', data.value)
  102. emits('update:contractName', '')
  103. }
  104. emits('node-click', data)
  105. }
  106. /** 筛选节点逻辑 */
  107. const filterNode = (value: string, data: Tree) => {
  108. if (!value) return true
  109. return data.label.includes(value)
  110. }
  111. /** 监听输入框进行过滤 */
  112. watch(wellName, (val) => {
  113. treeRef.value?.filter(val)
  114. })
  115. watch(
  116. () => props.deptId,
  117. (newVal, oldVal) => {
  118. if (newVal !== oldVal) {
  119. loadTree()
  120. }
  121. }
  122. )
  123. watch(
  124. () => props.modelValue,
  125. (newVal) => {
  126. if (newVal && treeRef.value) {
  127. treeRef.value.setCurrentKey(newVal)
  128. if (!expandedKeys.value.includes(newVal)) {
  129. expandedKeys.value.push(newVal)
  130. }
  131. }
  132. },
  133. { immediate: true }
  134. )
  135. onMounted(() => {
  136. loadTree()
  137. })
  138. </script>
  139. <template>
  140. <div
  141. class="dept-aside-container relative bg-white dark:bg-[#1d1e1f] shadow rounded-lg transition-all duration-300 ease-in-out overflow-visible"
  142. :class="[isCollapsed ? 'is-collapsed' : 'p-4']"
  143. >
  144. <div class="inner-content flex flex-col gap-4 h-full w-full">
  145. <h1 class="text-lg font-medium truncate shrink-0">{{ props.title }}</h1>
  146. <div class="shrink-0">
  147. <el-input
  148. v-model="wellName"
  149. size="default"
  150. placeholder="请输入井名"
  151. clearable
  152. :prefix-icon="Search"
  153. />
  154. </div>
  155. <div class="flex-1 relative overflow-hidden">
  156. <el-auto-resizer class="absolute">
  157. <template #default="{ height }">
  158. <el-scrollbar :style="{ height: `${height}px` }">
  159. <el-tree
  160. ref="treeRef"
  161. :data="deptList"
  162. :expand-on-click-node="false"
  163. :filter-node-method="filterNode"
  164. node-key="value"
  165. highlight-current
  166. :default-expanded-keys="expandedKeys"
  167. @node-click="handleNodeClick"
  168. />
  169. </el-scrollbar>
  170. </template>
  171. </el-auto-resizer>
  172. </div>
  173. </div>
  174. <div class="collapse-handle" @click="isCollapsed = !isCollapsed">
  175. <el-icon size="12">
  176. <CaretLeft v-if="!isCollapsed" />
  177. <CaretRight v-else />
  178. </el-icon>
  179. </div>
  180. </div>
  181. </template>
  182. <style scoped>
  183. /* 容器基础样式及动画 */
  184. .dept-aside-container {
  185. width: 14vw;
  186. height: calc(
  187. 100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
  188. );
  189. min-width: 200px;
  190. box-sizing: border-box;
  191. flex-shrink: 0;
  192. transition:
  193. width 0.3s ease-in-out,
  194. min-width 0.3s ease-in-out,
  195. padding 0.3s ease-in-out,
  196. margin 0.3s ease-in-out;
  197. }
  198. /* 内部内容区域防挤压及淡入淡出 */
  199. .inner-content {
  200. min-width: 168px;
  201. opacity: 1;
  202. transition: opacity 0.2s ease-in-out;
  203. }
  204. /* 折叠状态下的容器形态 */
  205. .dept-aside-container.is-collapsed {
  206. width: 0;
  207. min-width: 0;
  208. padding: 0;
  209. margin-right: -16px; /* 根据实际父级 grid gap 调整,若无间距可设为 0 */
  210. pointer-events: none;
  211. box-shadow: none;
  212. }
  213. /* 折叠状态下隐藏内部内容 */
  214. .dept-aside-container.is-collapsed .inner-content {
  215. opacity: 0;
  216. visibility: hidden;
  217. transition:
  218. opacity 0.1s ease-in-out,
  219. visibility 0s 0.1s;
  220. }
  221. /* 交互把手样式 */
  222. .collapse-handle {
  223. position: absolute;
  224. top: 50%;
  225. right: -14px;
  226. z-index: 200;
  227. display: flex;
  228. width: 14px;
  229. height: 60px;
  230. color: var(--el-text-color-secondary);
  231. pointer-events: auto;
  232. cursor: pointer;
  233. background-color: var(--el-bg-color);
  234. border: 1px solid var(--el-border-color-light);
  235. border-left: none;
  236. border-radius: 0 12px 12px 0;
  237. transform: translateY(-50%);
  238. box-shadow: 2px 0 6px rgb(0 0 0 / 5%);
  239. transition: right 0.3s ease-in-out;
  240. align-items: center;
  241. justify-content: center;
  242. }
  243. /* 折叠时把手向左位移贴边 */
  244. .is-collapsed .collapse-handle {
  245. right: -8px;
  246. border-left: 1px solid var(--el-border-color-light);
  247. }
  248. .collapse-handle:hover {
  249. color: var(--el-color-primary);
  250. background-color: var(--el-fill-color-light);
  251. }
  252. .truncate {
  253. overflow: hidden;
  254. text-overflow: ellipsis;
  255. white-space: nowrap;
  256. }
  257. </style>