mirror of
				https://github.com/continew-org/continew-admin-ui.git
				synced 2025-10-31 22:57:15 +08:00 
			
		
		
		
	Merge branch 'dev' of https://gitee.com/continew/continew-admin-ui into dev
This commit is contained in:
		
							
								
								
									
										2
									
								
								src/types/env.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/types/env.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -5,6 +5,8 @@ interface ImportMetaEnv { | ||||
|   readonly VITE_API_PREFIX: string | ||||
|   readonly VITE_API_BASE_URL: string | ||||
|   readonly VITE_BASE: string | ||||
|   readonly FILE_OPEN_PREVIEW: string | ||||
|   readonly FILE_VIEW_SERVER_URL: string | ||||
| } | ||||
|  | ||||
| interface ImportMeta { | ||||
|   | ||||
| @@ -22,7 +22,6 @@ export function downloadByUrl({ | ||||
|   url: string | ||||
|   target?: '_self' | '_blank' | ||||
|   fileName?: string | ||||
|   isSameHost: boolean | ||||
| }): Promise<boolean> { | ||||
|   // 是否同源 | ||||
|   const isSameHost = new URL(url).host === location.host | ||||
|   | ||||
| @@ -261,3 +261,21 @@ export const copyText = (text: any) => { | ||||
|   document.body.removeChild(textarea) | ||||
|   Message.success('复制成功') | ||||
| } | ||||
|  | ||||
| /** @desc 文件的转换base64 */ | ||||
| export const fileToBase64 = (file: File): Promise<string> => { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     const reader = new FileReader() | ||||
|     reader.onload = () => { | ||||
|       if (reader.result) { | ||||
|         resolve(reader.result.toString()) | ||||
|       } else { | ||||
|         reject(new Error('文件转base64失败')) | ||||
|       } | ||||
|     } | ||||
|     reader.onerror = (error) => { | ||||
|       reject(error) | ||||
|     } | ||||
|     reader.readAsDataURL(file) | ||||
|   }) | ||||
| } | ||||
|   | ||||
| @@ -106,11 +106,11 @@ import { | ||||
|   type SiteConfig, | ||||
|   listOption, | ||||
|   resetOptionValue, | ||||
|   updateOption, | ||||
|   uploadFile | ||||
|   updateOption | ||||
| } from '@/apis' | ||||
| import { useAppStore } from '@/stores' | ||||
| import { useForm } from '@/hooks' | ||||
| import { fileToBase64 } from '@/utils' | ||||
|  | ||||
| defineOptions({ name: 'BasicSetting' }) | ||||
|  | ||||
| @@ -212,15 +212,16 @@ const onResetValue = () => { | ||||
| // 上传 favicon | ||||
| const handleUploadFavicon = (options: RequestOption) => { | ||||
|   const controller = new AbortController() | ||||
|     ; (async function requestWrap() { | ||||
|     const { onProgress, onError, onSuccess, fileItem, name = 'file' } = options | ||||
|   ;(async function requestWrap() { | ||||
|     const { onProgress, onError, onSuccess, fileItem } = options | ||||
|     onProgress(20) | ||||
|     const formData = new FormData() | ||||
|     formData.append(name as string, fileItem.file as Blob) | ||||
|     uploadFile(formData) | ||||
|     if (!fileItem.file) { | ||||
|       return | ||||
|     } | ||||
|     fileToBase64(fileItem.file).then() | ||||
|       .then((res) => { | ||||
|         onSuccess(res) | ||||
|         form.SITE_FAVICON = res.data.url | ||||
|         onSuccess() | ||||
|         form.SITE_FAVICON = res | ||||
|         Message.success('上传成功') | ||||
|       }) | ||||
|       .catch((error) => { | ||||
| @@ -242,15 +243,16 @@ const handleChangeFavicon = (_: any, currentFile: any) => { | ||||
| // 上传 Logo | ||||
| const handleUploadLogo = (options: RequestOption) => { | ||||
|   const controller = new AbortController() | ||||
|     ; (async function requestWrap() { | ||||
|     const { onProgress, onError, onSuccess, fileItem, name = 'file' } = options | ||||
|   ;(async function requestWrap() { | ||||
|     const { onProgress, onError, onSuccess, fileItem } = options | ||||
|     onProgress(20) | ||||
|     const formData = new FormData() | ||||
|     formData.append(name as string, fileItem.file as Blob) | ||||
|     uploadFile(formData) | ||||
|     if (!fileItem.file) { | ||||
|       return | ||||
|     } | ||||
|     fileToBase64(fileItem.file).then() | ||||
|       .then((res) => { | ||||
|         onSuccess(res) | ||||
|         form.SITE_LOGO = res.data.url | ||||
|         onSuccess() | ||||
|         form.SITE_LOGO = res | ||||
|         Message.success('上传成功') | ||||
|       }) | ||||
|       .catch((error) => { | ||||
|   | ||||
							
								
								
									
										101
									
								
								src/views/system/file/main/FileMain/FilePreview.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								src/views/system/file/main/FileMain/FilePreview.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <a-modal | ||||
|         v-model:visible="visible" | ||||
|         :title="title" | ||||
|         :mask-closable="false" | ||||
|         :esc-to-close="false" | ||||
|         width="90%" | ||||
|         draggable | ||||
|     > | ||||
|       <div class="modal-content"> | ||||
|         <div class="modal-header"> | ||||
|           <!--          <a-button type="primary" @click="onPrintFile"> --> | ||||
|           <!--            <template #icon> --> | ||||
|           <!--              <icon-printer /> --> | ||||
|           <!--            </template> --> | ||||
|           <!--            <template #default> --> | ||||
|           <!--              打印 --> | ||||
|           <!--            </template> --> | ||||
|           <!--          </a-button> --> | ||||
|           <a-button type="primary" status="success" @click="onDownloadFile"> | ||||
|             <template #icon> | ||||
|               <icon-download /> | ||||
|             </template> | ||||
|             下载 | ||||
|           </a-button> | ||||
|         </div> | ||||
|         <div class="iframe-container"> | ||||
|           <iframe :src="previewUrl" /> | ||||
|         </div> | ||||
|       </div> | ||||
|     </a-modal> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { ref } from 'vue' | ||||
| import type { FileItem } from '@/apis' | ||||
| import { encodeByBase64 } from '@/utils/encrypt' | ||||
|  | ||||
| const emit = defineEmits(['download']) | ||||
|  | ||||
| const visible = ref(false) | ||||
| const title = ref('文件预览') | ||||
| const fileObject = ref<FileItem>() | ||||
| const isLoading = ref(false) | ||||
| const error = ref('') | ||||
| const previewUrl = ref('') | ||||
|  | ||||
| // 显示弹窗 | ||||
| function show(fileItem: FileItem) { | ||||
|   fileObject.value = fileItem | ||||
|   visible.value = true | ||||
|   title.value = `${fileItem.name}.${fileItem.extension}` | ||||
|   isLoading.value = true | ||||
|   error.value = '' | ||||
|  | ||||
|   previewUrl.value = `${import.meta.env.FILE_VIEW_SERVER_URL}/onlinePreview?url=${encodeURIComponent(encodeByBase64(fileItem.url))}` | ||||
| } | ||||
|  | ||||
| // 打印文件 | ||||
| // const onPrintFile = () => { | ||||
| // } | ||||
| // 下载文件 | ||||
| const onDownloadFile = () => { | ||||
|   emit('download', fileObject.value) | ||||
| } | ||||
| defineExpose({ | ||||
|   show | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <style scoped lang="scss"> | ||||
| .modal-content { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   height: 80vh; | ||||
| } | ||||
|  | ||||
| .modal-header { | ||||
|   display: flex; | ||||
|   justify-content: flex-end; | ||||
|   padding: 10px; | ||||
|   background: #f5f5f5; | ||||
|   border-bottom: 1px solid #e8e8e8; | ||||
| } | ||||
|  | ||||
| .iframe-container { | ||||
|   overflow: hidden; | ||||
|   flex: 1; | ||||
|   height: calc(80vh - 50px); | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
| } | ||||
|  | ||||
| iframe { | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
| } | ||||
| </style> | ||||
| @@ -17,7 +17,8 @@ | ||||
|         </a-dropdown> | ||||
|  | ||||
|         <a-input-group> | ||||
|           <a-input v-model="queryForm.name" placeholder="请输入文件名" allow-clear style="width: 200px" @change="search" /> | ||||
|           <a-input v-model="queryForm.name" placeholder="请输入文件名" allow-clear style="width: 200px" | ||||
|                    @change="search" /> | ||||
|           <a-button type="primary" @click="search"> | ||||
|             <template #icon> | ||||
|               <icon-search /> | ||||
| @@ -30,7 +31,7 @@ | ||||
|       <!-- 右侧区域 --> | ||||
|       <a-space wrap> | ||||
|         <a-button v-if="isBatchMode" :disabled="!selectedFileIds.length" type="primary" status="danger" | ||||
|           @click="handleMulDelete"> | ||||
|                   @click="handleMulDelete"> | ||||
|           <template #icon> | ||||
|             <icon-delete /> | ||||
|           </template> | ||||
| @@ -57,16 +58,17 @@ | ||||
|     <!-- 文件列表-宫格模式 --> | ||||
|     <a-spin id="fileMain" class="file-main__list" :loading="loading"> | ||||
|       <FileGrid v-show="fileList.length && mode === 'grid'" :data="fileList" :is-batch-mode="isBatchMode" | ||||
|         :selected-file-ids="selectedFileIds" @click="handleClickFile" @select="handleSelectFile" | ||||
|         @right-menu-click="handleRightMenuClick"></FileGrid> | ||||
|                 :selected-file-ids="selectedFileIds" @click="handleClickFile" @select="handleSelectFile" | ||||
|                 @right-menu-click="handleRightMenuClick"></FileGrid> | ||||
|  | ||||
|       <!-- 文件列表-列表模式 --> | ||||
|       <FileList v-show="fileList.length && mode === 'list'" :data="fileList" :is-batch-mode="isBatchMode" | ||||
|         :selected-file-ids="selectedFileIds" @click="handleClickFile" @select="handleSelectFile" | ||||
|         @right-menu-click="handleRightMenuClick"></FileList> | ||||
|                 :selected-file-ids="selectedFileIds" @click="handleClickFile" @select="handleSelectFile" | ||||
|                 @right-menu-click="handleRightMenuClick"></FileList> | ||||
|  | ||||
|       <a-empty v-if="!fileList.length" /> | ||||
|     </a-spin> | ||||
|     <FilePreview ref="filePreviewRef" @download="args => onDownload(args)" /> | ||||
|     <div class="pagination"> | ||||
|       <a-pagination v-bind="pagination" /> | ||||
|     </div> | ||||
| @@ -89,6 +91,7 @@ import { type FileItem, type FileQuery, deleteFile, listFile, uploadFile } from | ||||
| import { ImageTypes } from '@/constant/file' | ||||
| import 'viewerjs/dist/viewer.css' | ||||
| import { downloadByUrl } from '@/utils/downloadFile' | ||||
| import FilePreview from '@/views/system/file/main/FileMain/FilePreview.vue' | ||||
|  | ||||
| const FileList = defineAsyncComponent(() => import('./FileList.vue')) | ||||
| const route = useRoute() | ||||
| @@ -110,29 +113,43 @@ const { | ||||
|   pagination, | ||||
|   search | ||||
| } = useTable((page) => listFile({ ...queryForm, ...page }), { immediate: false, paginationOption }) | ||||
|  | ||||
| const filePreviewRef = ref() | ||||
| // 点击文件 | ||||
| const handleClickFile = (item: FileItem) => { | ||||
|   if (ImageTypes.includes(item.extension)) { | ||||
|     if (item.url) { | ||||
|       const imgList: string[] = fileList.value.filter((i) => ImageTypes.includes(i.extension)).map((a) => a.url || '') | ||||
|       const index = imgList.findIndex((i) => i === item.url) | ||||
|       if (imgList.length) { | ||||
|         viewerApi({ | ||||
|           options: { | ||||
|             initialViewIndex: index | ||||
|           }, | ||||
|           images: imgList | ||||
|         }) | ||||
|   if (JSON.parse(import.meta.env.FILE_OPEN_PREVIEW)) { | ||||
|     filePreviewRef.value.show(item) | ||||
|   } else { | ||||
|     if (ImageTypes.includes(item.extension)) { | ||||
|       if (item.url) { | ||||
|         const imgList: string[] = fileList.value.filter((i) => ImageTypes.includes(i.extension)).map((a) => a.url || '') | ||||
|         const index = imgList.findIndex((i) => i === item.url) | ||||
|         if (imgList.length) { | ||||
|           viewerApi({ | ||||
|             options: { | ||||
|               initialViewIndex: index | ||||
|             }, | ||||
|             images: imgList | ||||
|           }) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     if (item.extension === 'mp4') { | ||||
|       previewFileVideoModal(item) | ||||
|     } | ||||
|     if (item.extension === 'mp3') { | ||||
|       previewFileAudioModal(item) | ||||
|     } | ||||
|   } | ||||
|   if (item.extension === 'mp4') { | ||||
|     previewFileVideoModal(item) | ||||
|   } | ||||
|   if (item.extension === 'mp3') { | ||||
|     previewFileAudioModal(item) | ||||
|   } | ||||
| } | ||||
| // 下载文件 | ||||
| const onDownload = async (fileInfo: FileItem) => { | ||||
|   const res = await downloadByUrl({ | ||||
|     url: fileInfo.url, | ||||
|     target: '_self', | ||||
|     fileName: `${fileInfo.name}.${fileInfo.extension}` | ||||
|   }) | ||||
|   res ? Message.success('下载成功') : Message.error('下载失败') | ||||
|   search() | ||||
| } | ||||
|  | ||||
| // 右键菜单 | ||||
| @@ -154,13 +171,7 @@ const handleRightMenuClick = async (mode: string, fileInfo: FileItem) => { | ||||
|   } else if (mode === 'detail') { | ||||
|     openFileDetailModal(fileInfo) | ||||
|   } else if (mode === 'download') { | ||||
|     const res = await downloadByUrl({ | ||||
|       url: fileInfo.url, | ||||
|       target: '_self', | ||||
|       fileName: `${fileInfo.name}.${fileInfo.extension}` | ||||
|     }) | ||||
|     res ? Message.success('下载成功') : Message.error('下载失败') | ||||
|     search() | ||||
|     await onDownload(fileInfo) | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -186,7 +197,7 @@ const handleMulDelete = () => { | ||||
| // 上传 | ||||
| const handleUpload = (options: RequestOption) => { | ||||
|   const controller = new AbortController() | ||||
|     ; (async function requestWrap() { | ||||
|   ;(async function requestWrap() { | ||||
|     const { onProgress, onError, onSuccess, fileItem, name = 'file' } = options | ||||
|     onProgress(20) | ||||
|     const formData = new FormData() | ||||
|   | ||||
| @@ -1,67 +0,0 @@ | ||||
| <template> | ||||
|   <a-menu class="right-menu"> | ||||
|     <a-menu-item @click="onClick('add')"> | ||||
|       <template #icon><icon-plus-circle :size="16" :stroke-width="3" /></template> | ||||
|       <span>新增</span> | ||||
|     </a-menu-item> | ||||
|  | ||||
|     <a-menu-item v-permission="['system:dept:update']" @click="onClick('update')"> | ||||
|       <template #icon><icon-edit :size="16" :stroke-width="3" /></template> | ||||
|       <span>修改</span> | ||||
|     </a-menu-item> | ||||
|  | ||||
|     <a-menu-item v-permission="['system:dept:delete']" :title="data.isSystem ? '系统内置数据不能删除' : undefined" :disabled="data.isSystem" @click="onClick('delete')"> | ||||
|       <template #icon><icon-delete :size="16" :stroke-width="3" /></template> | ||||
|       <span>删除</span> | ||||
|     </a-menu-item> | ||||
|   </a-menu> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import type { DeptResp } from '@/apis' | ||||
|  | ||||
| interface Props { | ||||
|   data: DeptResp | ||||
| } | ||||
|  | ||||
| const props = withDefaults(defineProps<Props>(), {}) | ||||
|  | ||||
| const emit = defineEmits<{ | ||||
|   (e: 'on-menu-item-click', mode: string, data: DeptResp): void | ||||
| }>() | ||||
|  | ||||
| // 点击菜单项 | ||||
| const onClick = (mode: string) => { | ||||
|   emit('on-menu-item-click', mode, props.data) | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| :deep(.arco-menu-inner) { | ||||
|   padding: 4px; | ||||
|  | ||||
|   .arco-menu-item { | ||||
|     height: 34px; | ||||
|  | ||||
|     &:not(.arco-menu-selected) { | ||||
|       color: $color-text-1; | ||||
|     } | ||||
|  | ||||
|     &:last-child { | ||||
|       margin-bottom: 0; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| .right-menu { | ||||
|   width: 120px; | ||||
|   box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); | ||||
|   border-radius: 4px; | ||||
|   border: 1px solid var(--color-border-2); | ||||
|   box-sizing: border-box; | ||||
|  | ||||
|   .arrow-icon { | ||||
|     margin-right: 0; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
| @@ -9,51 +9,27 @@ | ||||
|       <div class="left-tree__tree"> | ||||
|         <a-tree | ||||
|           ref="treeRef" | ||||
|           :data="(treeData as unknown as TreeNodeData[])" | ||||
|           :field-names="{ key: 'id' }" | ||||
|           :data="deptList" | ||||
|           show-line | ||||
|           block-node | ||||
|           default-expand-all | ||||
|           :selected-keys="selectedKeys" | ||||
|           @select="select" | ||||
|         > | ||||
|           <template #title="node"> | ||||
|             <a-trigger | ||||
|               v-model:popup-visible="node.popupVisible" | ||||
|               trigger="contextMenu" | ||||
|               align-point | ||||
|               animation-name="slide-dynamic-origin" | ||||
|               auto-fit-transform-origin | ||||
|               position="bl" | ||||
|               scroll-to-close | ||||
|             > | ||||
|               <a-tooltip v-if="node.description" :content="node.description" background-color="rgb(var(--primary-6))" position="right"> | ||||
|                 <div @contextmenu="onContextmenu(node)">{{ node.name }}</div> | ||||
|               </a-tooltip> | ||||
|               <div v-else @contextmenu="onContextmenu(node)">{{ node.name }}</div> | ||||
|               <template #content> | ||||
|                 <RightMenu | ||||
|                   v-if="has.hasPermOr(['system:dept:update', 'system:dept:delete'])" | ||||
|                   :data="node" | ||||
|                   @on-menu-item-click="onMenuItemClick" | ||||
|                 /> | ||||
|               </template> | ||||
|             </a-trigger> | ||||
|           <template #switcher-icon="node, { isLeaf }"> | ||||
|             <IconCaretDown v-if="!isLeaf" /> | ||||
|             <IconIdcard v-else /> | ||||
|           </template> | ||||
|         </a-tree> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|   <DeptAddModal ref="DeptAddModalRef" @save-success="getTreeData" /> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="tsx"> | ||||
| import type { Message, Modal, TreeInstance, TreeNodeData } from '@arco-design/web-vue' | ||||
| import { mapTree } from 'xe-utils' | ||||
| import DeptAddModal from '../../dept/DeptAddModal.vue' | ||||
| import RightMenu from './RightMenu.vue' | ||||
| import { type DeptQuery, type DeptResp, deleteDept, listDept } from '@/apis' | ||||
| import has from '@/utils/has' | ||||
| import type { TreeInstance } from '@arco-design/web-vue' | ||||
| import { ref } from 'vue' | ||||
| import { useDept } from '@/hooks/app' | ||||
|  | ||||
| interface Props { | ||||
|   placeholder?: string | ||||
| @@ -71,92 +47,25 @@ const select = (keys: Array<any>) => { | ||||
|   emit('node-click', keys) | ||||
| } | ||||
|  | ||||
| const queryForm = reactive<DeptQuery>({ | ||||
|   sort: ['parentId,asc', 'sort,asc', 'createTime,desc'] | ||||
| }) | ||||
|  | ||||
| interface TreeItem extends DeptResp { | ||||
|   popupVisible: boolean | ||||
| } | ||||
| const treeRef = ref<TreeInstance>() | ||||
| const treeData = ref<TreeItem[]>([]) | ||||
| const loading = ref(false) | ||||
| // 查询树列表 | ||||
| const getTreeData = async (query: DeptQuery = { ...queryForm }) => { | ||||
|   try { | ||||
|     loading.value = true | ||||
|     const { data } = await listDept(query) | ||||
|     treeData.value = mapTree(data, (i) => ({ | ||||
|       ...i, | ||||
|       popupVisible: false, | ||||
|       switcherIcon: (node: any) => { | ||||
|         if (!node.isLeaf) { | ||||
|           return <icon-caret-down /> | ||||
|         } | ||||
|         return <icon-idcard /> | ||||
|       } | ||||
|     })) | ||||
|     await nextTick(() => { | ||||
| const { deptList, getDeptList } = useDept({ | ||||
|   onSuccess: () => { | ||||
|     nextTick(() => { | ||||
|       treeRef.value?.expandAll(true) | ||||
|       select([data[0].id]) | ||||
|       select([deptList.value[0]?.key]) | ||||
|     }) | ||||
|   } finally { | ||||
|     loading.value = false | ||||
|   } | ||||
| } | ||||
| }) | ||||
|  | ||||
| // 树查询 | ||||
| const inputValue = ref('') | ||||
| watch(inputValue, (val) => { | ||||
|   queryForm.description = val | ||||
|   getTreeData() | ||||
|   getDeptList(val) | ||||
| }) | ||||
|  | ||||
| // 保存当前右键的节点 | ||||
| const contextmenuNode = ref<TreeItem | null>(null) | ||||
| const onContextmenu = (node: TreeItem) => { | ||||
|   contextmenuNode.value = node | ||||
| } | ||||
|  | ||||
| // 关闭右键菜单弹框 | ||||
| const closeRightMenuPopup = () => { | ||||
|   if (contextmenuNode.value?.popupVisible) { | ||||
|     contextmenuNode.value.popupVisible = false | ||||
|   } | ||||
| } | ||||
|  | ||||
| const DeptAddModalRef = ref<InstanceType<typeof DeptAddModal>>() | ||||
| // 右键菜单项点击 | ||||
| const onMenuItemClick = (mode: string, node: DeptResp) => { | ||||
|   closeRightMenuPopup() | ||||
|   if (mode === 'add') { | ||||
|     DeptAddModalRef.value?.onAdd(node.id) | ||||
|   } else if (mode === 'update') { | ||||
|     DeptAddModalRef.value?.onUpdate(node.id) | ||||
|   } else if (mode === 'delete') { | ||||
|     Modal.warning({ | ||||
|       title: '提示', | ||||
|       content: `是否确定删除 [${node.name}]?`, | ||||
|       hideCancel: false, | ||||
|       okButtonProps: { status: 'danger' }, | ||||
|       onBeforeOk: async () => { | ||||
|         try { | ||||
|           const res = await deleteDept(node.id) | ||||
|           if (res.success) { | ||||
|             Message.success('删除成功') | ||||
|             getTreeData() | ||||
|           } | ||||
|           return res.success | ||||
|         } catch (error) { | ||||
|           return false | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|   } | ||||
| } | ||||
|  | ||||
| onMounted(() => { | ||||
|   getTreeData() | ||||
|   getDeptList() | ||||
| }) | ||||
| </script> | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 秋帆
					秋帆