Преглед изворни кода

Merge branch 'qhse_file' of shuzhihua/pms-iot-vue into master

yanghao пре 1 недеља
родитељ
комит
7c36bf84e1

+ 2 - 1
src/components/DeptTreeSelect/index.vue

@@ -76,7 +76,8 @@ const loadTree = async () => {
   try {
     const { depts, currentId } = await loadDeptData()
     const targetKey = props.modelValue ?? (props.initSelect ? currentId : null)
-    if (props.initSelect && props.modelValue) {
+    // Only initialize the parent value when no external selection was provided.
+    if (props.initSelect && props.modelValue === undefined && currentId !== undefined) {
       emits('update:modelValue', currentId)
     }
     if (targetKey && !expandedKeys.value.includes(targetKey)) {

+ 3 - 0
src/views/pms/fiveDailyReportSummary/index.vue

@@ -204,10 +204,13 @@ function handleDeptCellClick(row) {
   }
 
   if (rowType === '2' && row?.projectDeptId) {
+    targetQuery.deptId = String(row.projectDeptId)
     targetQuery.projectDeptId = String(row.projectDeptId)
   } else if (row?.teamId) {
+    targetQuery.deptId = String(row.teamId)
     targetQuery.teamId = String(row.teamId)
   } else if (row?.projectDeptId) {
+    targetQuery.deptId = String(row.projectDeptId)
     targetQuery.projectDeptId = String(row.projectDeptId)
   } else {
     return

+ 51 - 38
src/views/pms/iotrhdailyreport/rh-form.vue

@@ -66,6 +66,7 @@ interface FormOriginal {
   otherNptReason: string
 
   productionStatus: string
+  malfunction: string
   remark: string
   relocationDays: number
   capacity: number
@@ -91,6 +92,7 @@ const FORM_KEYS: (keyof FormOriginal)[] = [
   'dailyInjectWaterTime',
   'constructionStatus',
   'productionStatus',
+  'malfunction',
   'remark',
   'relocationDays',
   'capacity',
@@ -244,6 +246,15 @@ const validateOtherReason = (_rule: any, value: any, callback: any) => {
   }
 }
 
+const validateMalfunction = (_rule: any, value: any, callback: any) => {
+  const accidentTime = Number(form.value.accidentTime || 0)
+  if (accidentTime > 0 && !String(value || '').trim()) {
+    callback(new Error('工程质量大于0时,故障情况必填'))
+  } else {
+    callback()
+  }
+}
+
 // 动态构建校验规则
 const rules = reactive<FormRules>({
   dailyGasInjection: [{ required: true, message: '请输入当日注气量', trigger: ['blur', 'change'] }],
@@ -259,6 +270,7 @@ const rules = reactive<FormRules>({
     { validator: validateTotalTime(), trigger: ['blur', 'change'] }
   ],
   dailyInjectWaterTime: [{ validator: validateTotalTime(), trigger: ['blur', 'change'] }],
+  malfunction: [{ validator: validateMalfunction, trigger: ['blur', 'change'] }],
   otherNptReason: [{ validator: validateOtherReason, trigger: ['blur', 'change'] }]
 })
 
@@ -289,6 +301,17 @@ watch(
   { deep: true }
 )
 
+watch(
+  () => form.value.accidentTime,
+  () => {
+    if (!isMainFieldDisabled.value) {
+      nextTick(() => {
+        formRef.value?.validateField('malfunction')
+      })
+    }
+  }
+)
+
 // --- 提交 ---
 const submitForm = async () => {
   if (!formRef.value) return
@@ -336,8 +359,7 @@ const handleAudit = async (auditStatus: 20 | 30) => {
     :model-value="visible"
     @update:model-value="emits('update:visible', $event)"
     header-class="mb-0!"
-    size="50%"
-  >
+    size="50%">
     <template #header>
       <span class="text-xl font-bold text-[var(--el-text-color-primary)]">
         {{ type === 'edit' ? '编辑日报' : '审批日报' }}
@@ -352,13 +374,11 @@ const handleAudit = async (auditStatus: 20 | 30) => {
       :model="form"
       v-loading="loading"
       require-asterisk-position="right"
-      :disabled="formType === 'readonly' && type !== 'approval'"
-    >
+      :disabled="formType === 'readonly' && type !== 'approval'">
       <!-- 顶部提示区 -->
       <div class="flex flex-col gap-3 text-sm mb-4">
         <div
-          class="rounded-md border border-blue-100 bg-blue-50/80 p-3 dark:border-blue-900/30 dark:bg-blue-900/10"
-        >
+          class="rounded-md border border-blue-100 bg-blue-50/80 p-3 dark:border-blue-900/30 dark:bg-blue-900/10">
           <div class="flex flex-col gap-2.5">
             <div class="flex items-center justify-between">
               <div class="text-gray-600 dark:text-gray-400">
@@ -366,8 +386,7 @@ const handleAudit = async (auditStatus: 20 | 30) => {
                 当日注气量 / 产能
               </div>
               <span
-                class="inline-flex items-center rounded border border-red-200 bg-red-100 px-2 py-0.5 text-xs font-medium text-red-600 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400"
-              >
+                class="inline-flex items-center rounded border border-red-200 bg-red-100 px-2 py-0.5 text-xs font-medium text-red-600 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400">
                 >120% 红色预警
               </span>
             </div>
@@ -380,12 +399,10 @@ const handleAudit = async (auditStatus: 20 | 30) => {
         </div>
         <div
           v-if="isEdit && form.opinion"
-          class="flex gap-3 rounded-md border border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-800 dark:bg-yellow-900/10"
-        >
+          class="flex gap-3 rounded-md border border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-800 dark:bg-yellow-900/10">
           <Icon
             icon="ep:warning-filled"
-            class="mt-0.5 shrink-0 text-base text-yellow-600 dark:text-yellow-500"
-          />
+            class="mt-0.5 shrink-0 text-base text-yellow-600 dark:text-yellow-500" />
           <div class="flex flex-col">
             <h4 class="mb-1 font-bold text-yellow-800 dark:text-yellow-500">审核意见</h4>
             <p class="leading-relaxed text-gray-600 dark:text-gray-400">{{ form.opinion }}</p>
@@ -415,22 +432,19 @@ const handleAudit = async (auditStatus: 20 | 30) => {
           <el-input
             :model-value="transitTime.value"
             disabled
-            :class="{ 'warning-input': transitTime.original > 1.2 }"
-          />
+            :class="{ 'warning-input': transitTime.original > 1.2 }" />
         </el-form-item>
         <el-form-item label="施工状态" prop="constructionStatus">
           <el-select
             v-model="form.constructionStatus"
             placeholder="请选择施工状态"
             :disabled="isMainFieldDisabled"
-            clearable
-          >
+            clearable>
             <el-option
               v-for="(dict, index) in getStrDictOptions(DICT_TYPE.PMS_PROJECT_TASK_SCHEDULE)"
               :key="index"
               :label="dict.label"
-              :value="dict.value"
-            />
+              :value="dict.value" />
           </el-select>
         </el-form-item>
         <el-form-item label="当日注气量(方)" prop="dailyGasInjection">
@@ -440,8 +454,7 @@ const handleAudit = async (auditStatus: 20 | 30) => {
             v-model="form.dailyGasInjection"
             :controls="false"
             align="left"
-            :disabled="isMainFieldDisabled"
-          />
+            :disabled="isMainFieldDisabled" />
         </el-form-item>
         <el-form-item label="当日注水量(方)" prop="dailyWaterInjection">
           <el-input-number
@@ -450,8 +463,7 @@ const handleAudit = async (auditStatus: 20 | 30) => {
             v-model="form.dailyWaterInjection"
             :controls="false"
             align="left"
-            :disabled="isMainFieldDisabled"
-          />
+            :disabled="isMainFieldDisabled" />
         </el-form-item>
         <el-form-item class="col-span-2" label="施工区域" prop="location">
           <el-input v-model="form.location" disabled />
@@ -462,8 +474,16 @@ const handleAudit = async (auditStatus: 20 | 30) => {
             type="textarea"
             autosize
             maxlength="1000"
-            :disabled="isMainFieldDisabled"
-          />
+            :disabled="isMainFieldDisabled" />
+        </el-form-item>
+
+        <el-form-item class="col-span-2" label="故障情况" prop="malfunction">
+          <el-input
+            v-model="form.malfunction"
+            type="textarea"
+            autosize
+            maxlength="1000"
+            :disabled="isMainFieldDisabled" />
         </el-form-item>
         <el-form-item class="col-span-2" label="备注" prop="remark">
           <el-input
@@ -471,8 +491,7 @@ const handleAudit = async (auditStatus: 20 | 30) => {
             type="textarea"
             autosize
             maxlength="1000"
-            :disabled="isMainFieldDisabled"
-          />
+            :disabled="isMainFieldDisabled" />
         </el-form-item>
 
         <!-- 时间信息 -->
@@ -492,8 +511,7 @@ const handleAudit = async (auditStatus: 20 | 30) => {
             :controls="false"
             align="left"
             placeholder="注气+非生产=24H"
-            :disabled="isMainFieldDisabled"
-          />
+            :disabled="isMainFieldDisabled" />
         </el-form-item>
         <el-form-item label="当日注水时间(H)" prop="dailyInjectWaterTime">
           <el-input-number
@@ -504,8 +522,7 @@ const handleAudit = async (auditStatus: 20 | 30) => {
             :controls="false"
             align="left"
             placeholder="注气=0时,注水+非生产=24H"
-            :disabled="isMainFieldDisabled"
-          />
+            :disabled="isMainFieldDisabled" />
         </el-form-item>
 
         <!-- 循环渲染11个具体非生产时间 -->
@@ -513,8 +530,7 @@ const handleAudit = async (auditStatus: 20 | 30) => {
           v-for="field in NON_PROD_FIELDS"
           :key="field.key"
           :label="field.label + '(H)'"
-          :prop="field.key"
-        >
+          :prop="field.key">
           <el-input-number
             class="!w-full"
             :min="0"
@@ -522,8 +538,7 @@ const handleAudit = async (auditStatus: 20 | 30) => {
             v-model="form[field.key as keyof FormOriginal]"
             :controls="false"
             align="left"
-            :disabled="isMainFieldDisabled"
-          />
+            :disabled="isMainFieldDisabled" />
         </el-form-item>
 
         <!-- 其他非生产原因 -->
@@ -531,8 +546,7 @@ const handleAudit = async (auditStatus: 20 | 30) => {
           <el-input
             v-model="form.otherNptReason"
             placeholder="请输入其他非生产原因"
-            :disabled="isMainFieldDisabled"
-          />
+            :disabled="isMainFieldDisabled" />
         </el-form-item>
 
         <!-- 审批意见 -->
@@ -544,8 +558,7 @@ const handleAudit = async (auditStatus: 20 | 30) => {
               maxlength="1000"
               type="textarea"
               :autosize="{ minRows: 3 }"
-              :disabled="formType === 'readonly'"
-            />
+              :disabled="formType === 'readonly'" />
           </el-form-item>
         </div>
       </div>

+ 33 - 0
src/views/pms/iotrydailyreport/ry-form.vue

@@ -99,6 +99,7 @@ interface FormOriginal {
   auditStatus: number
 
   nextPlan: string
+  malfunction: string
   location: string
 }
 
@@ -148,6 +149,7 @@ const FORM_KEYS: (keyof FormOriginal)[] = [
   'otherNptReason',
   'auditStatus',
   'nextPlan',
+  'malfunction',
   'location'
 ]
 
@@ -321,6 +323,15 @@ const validateOtherReason = (_rule: any, value: any, callback: any) => {
   }
 }
 
+const validateMalfunction = (_rule: any, value: any, callback: any) => {
+  const accidentTime = Number(form.value.accidentTime || 0)
+  if (accidentTime > 0 && !String(value || '').trim()) {
+    callback(new Error('工程质量大于0时,故障情况必填'))
+  } else {
+    callback()
+  }
+}
+
 const validateLastCurrentDepth = (_rule: any, value: any, callback: any) => {
   if (value && value < (form.value.lastCurrentDepth ?? 0)) {
     callback(new Error(`当前深度需大于等于上一次填报深度 ${form.value.lastCurrentDepth}`))
@@ -352,6 +363,7 @@ const rules = reactive<FormRules>({
     { validator: validateTotalTime(), trigger: ['blur', 'change'] }
   ],
   otherProductionTime: [{ validator: validateTotalTime(), trigger: ['blur', 'change'] }],
+  malfunction: [{ validator: validateMalfunction, trigger: ['blur', 'change'] }],
   otherNptReason: [{ validator: validateOtherReason, trigger: ['blur', 'change'] }]
 })
 
@@ -383,6 +395,17 @@ watch(
 )
 
 // --- 提交 ---
+watch(
+  () => form.value.accidentTime,
+  () => {
+    if (!isMainFieldDisabled.value) {
+      nextTick(() => {
+        formRef.value?.validateField('malfunction')
+      })
+    }
+  }
+)
+
 const submitForm = async () => {
   if (!formRef.value) return
 
@@ -884,6 +907,16 @@ const inputCurrentDepth = useDebounceFn(function inputCurrentDepth() {
         </el-form-item>
 
         <!-- 时间信息 -->
+        <el-form-item class="col-span-2" label="故障情况" prop="malfunction">
+          <el-input
+            v-model="form.malfunction"
+            type="textarea"
+            autosize
+            maxlength="1000"
+            placeholder="请输入故障情况"
+            :disabled="isMainFieldDisabled" />
+        </el-form-item>
+
         <div class="col-span-2 flex items-center gap-2 mt-4">
           <div class="bg-[var(--el-color-primary)] w-1 h-5 rounded-full"></div>
           <div class="text-lg font-medium text-[var(--el-text-color-primary)]"

+ 33 - 0
src/views/pms/iotrydailyreport/ry-xj-form.vue

@@ -100,6 +100,7 @@ interface FormOriginal {
   opinion: string
 
   auditStatus: number
+  malfunction: string
   location: string
   daySupervisors: string
   nightSupervisors: string
@@ -149,6 +150,7 @@ const FORM_KEYS: (keyof FormOriginal)[] = [
   'otherNptTime',
   'otherNptReason',
   'auditStatus',
+  'malfunction',
   'location',
   'daySupervisors',
   'nightSupervisors'
@@ -329,6 +331,15 @@ const validateOtherReason = (_rule: any, value: any, callback: any) => {
 }
 
 // 动态构建校验规则
+const validateMalfunction = (_rule: any, value: any, callback: any) => {
+  const accidentTime = Number(form.value.accidentTime || 0)
+  if (accidentTime > 0 && !String(value || '').trim()) {
+    callback(new Error('工程质量大于0时,故障情况必填'))
+  } else {
+    callback()
+  }
+}
+
 const rules = reactive<FormRules>({
   repairStatus: [{ required: true, message: '请选择施工状态', trigger: ['change', 'blur'] }],
   daySupervisors: [{ required: true, message: '请输入白班跟班干部', trigger: ['change', 'blur'] }],
@@ -355,6 +366,7 @@ const rules = reactive<FormRules>({
     { validator: validateTotalTime(), trigger: ['blur', 'change'] }
   ],
   otherProductionTime: [{ validator: validateTotalTime(), trigger: ['blur', 'change'] }],
+  malfunction: [{ validator: validateMalfunction, trigger: ['blur', 'change'] }],
   otherNptReason: [{ validator: validateOtherReason, trigger: ['blur', 'change'] }]
 })
 
@@ -382,6 +394,17 @@ watch(
 )
 
 // --- 提交 ---
+watch(
+  () => form.value.accidentTime,
+  () => {
+    if (!isMainFieldDisabled.value) {
+      nextTick(() => {
+        formRef.value?.validateField('malfunction')
+      })
+    }
+  }
+)
+
 const submitForm = async () => {
   if (!formRef.value) return
 
@@ -869,6 +892,16 @@ const allocatedTimeDisabled = computed(() => {
         </el-form-item>
 
         <!-- 时间信息 -->
+        <el-form-item class="col-span-2" label="故障情况" prop="malfunction">
+          <el-input
+            v-model="form.malfunction"
+            type="textarea"
+            autosize
+            maxlength="1000"
+            placeholder="请输入故障情况"
+            :disabled="isMainFieldDisabled" />
+        </el-form-item>
+
         <div class="col-span-2 flex items-center gap-2 mt-4">
           <div class="bg-[var(--el-color-primary)] w-1 h-5 rounded-full"></div>
           <div class="text-lg font-medium text-[var(--el-text-color-primary)]"

+ 252 - 0
src/views/system/tree/PmsTree.vue

@@ -4,6 +4,7 @@ import { CaretLeft, CaretRight, Search } from '@element-plus/icons-vue'
 import type { IotTreeVO } from '@/api/system/tree'
 import { IotTreeApi } from '@/api/system/tree'
 import { defaultProps, handleTree } from '@/utils/tree'
+import { CommonStatusEnum } from '@/utils/constants'
 
 defineOptions({ name: 'IotTree' })
 
@@ -28,11 +29,41 @@ const emits = defineEmits<{
   (event: 'success', value?: number): void
 }>()
 
+const { t } = useI18n()
+const message = useMessage()
 const isCollapsed = ref(false)
 const treeName = ref('')
 const treeList = ref<TreeNode[]>([])
 const treeRef = ref<InstanceType<typeof ElTree>>()
 const expandedKeys = ref<Array<number | string>>([])
+const formLoading = ref(false)
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const formRef = ref()
+const menuVisible = ref(false)
+const menuX = ref(0)
+const menuY = ref(0)
+const contextMenuRef = ref<HTMLElement | null>(null)
+const nodeInfo = ref<Partial<TreeNode>>({})
+const formType = ref<'create' | 'update'>('create')
+const selectedNode = ref<TreeNode | null>(null)
+
+const createDefaultFormData = () => ({
+  id: undefined as number | undefined,
+  originId: undefined as number | undefined,
+  parentId: undefined as number | undefined,
+  name: '',
+  type: 'folder',
+  sort: 0,
+  status: CommonStatusEnum.ENABLE,
+  remark: ''
+})
+
+const formData = ref(createDefaultFormData())
+const formRules = reactive({
+  name: [{ required: true, message: '请输入目录名称', trigger: 'blur' }],
+  sort: [{ required: true, message: '请输入显示排序', trigger: 'blur' }]
+})
 
 const getNodeIcon = (type: string) => {
   if (type === 'device') return 'fa:tasks'
@@ -64,6 +95,24 @@ const findNodePath = (
   return null
 }
 
+const findNodeById = (
+  nodes: TreeNode[],
+  targetId: number | string
+): TreeNode | null => {
+  for (const node of nodes) {
+    if (node.id === targetId) {
+      return node
+    }
+
+    if (node.children?.length) {
+      const result = findNodeById(node.children, targetId)
+      if (result) return result
+    }
+  }
+
+  return null
+}
+
 const locateNode = (targetId: number | string) => {
   if (!targetId || !treeList.value.length) return
 
@@ -81,6 +130,32 @@ const locateNode = (targetId: number | string) => {
   })
 }
 
+const resetForm = () => {
+  formData.value = createDefaultFormData()
+  formRef.value?.resetFields()
+}
+
+const handleRightClick = (event: MouseEvent, data: TreeNode) => {
+  nodeInfo.value = { ...data }
+  selectedNode.value = data
+  event.preventDefault()
+
+  const nodeContent = (event.target as HTMLElement)?.closest(
+    '.el-tree-node__content'
+  ) as HTMLElement | null
+
+  if (nodeContent) {
+    const rect = nodeContent.getBoundingClientRect()
+    menuX.value = rect.right + 8
+    menuY.value = rect.top
+  } else {
+    menuX.value = event.clientX
+    menuY.value = event.clientY
+  }
+
+  menuVisible.value = true
+}
+
 const getTreeInfo = async () => {
   const res = await IotTreeApi.getSimpleTreeList()
   treeList.value = handleTree(res) as TreeNode[]
@@ -101,6 +176,89 @@ const handleNodeClick = (row: TreeNode) => {
   emits('node-click', row)
 }
 
+const handleMenuClick = async (action: 'add' | 'edit' | 'delete') => {
+  switch (action) {
+    case 'add':
+      resetForm()
+      formType.value = 'create'
+      dialogTitle.value = '新增目录'
+      formData.value.parentId = selectedNode.value?.id
+      formData.value.originId = selectedNode.value?.id
+      formData.value.type = selectedNode.value?.type || 'folder'
+      dialogVisible.value = true
+      break
+    case 'edit':
+      if (!selectedNode.value) return
+      resetForm()
+      formType.value = 'update'
+      dialogTitle.value = '编辑目录'
+      formData.value = {
+        ...createDefaultFormData(),
+        ...selectedNode.value
+      }
+      dialogVisible.value = true
+      break
+    case 'delete':
+      if (!selectedNode.value?.id) return
+      const parentId = selectedNode.value.parentId
+      await message.delConfirm()
+      await IotTreeApi.deleteIotTree(selectedNode.value.id)
+      message.success(t('common.delSuccess'))
+      await getTreeInfo()
+      if (parentId) {
+        locateNode(parentId)
+        const parentNode = findNodeById(treeList.value, parentId)
+        if (parentNode) {
+          emits('node-click', parentNode)
+        }
+      }
+      break
+  }
+
+  menuVisible.value = false
+}
+
+const submitForm = async () => {
+  await formRef.value?.validate()
+  formLoading.value = true
+  try {
+    const targetParentId =
+      formType.value === 'create' ? selectedNode.value?.id : formData.value.parentId
+    const payload = {
+      ...formData.value,
+      parentId: formType.value === 'create' ? selectedNode.value?.id : formData.value.parentId,
+      originId:
+        formType.value === 'create'
+          ? selectedNode.value?.originId ?? selectedNode.value?.id ?? 0
+          : formData.value.originId,
+      type: formData.value.type || selectedNode.value?.type || 'folder'
+    } as IotTreeVO
+
+    if (formType.value === 'create') {
+      await IotTreeApi.createIotTree(payload)
+      message.success(t('common.createSuccess'))
+    } else {
+      await IotTreeApi.updateIotTree(payload)
+      message.success(t('common.updateSuccess'))
+    }
+
+    dialogVisible.value = false
+    await getTreeInfo()
+    if (targetParentId) {
+      expandedKeys.value = [...new Set([...expandedKeys.value, targetParentId])]
+      locateNode(targetParentId)
+    }
+  } finally {
+    formLoading.value = false
+  }
+}
+
+const handleClickOutside = (event: MouseEvent) => {
+  if (menuVisible.value && contextMenuRef.value && !contextMenuRef.value.contains(event.target as Node)) {
+    menuVisible.value = false
+  }
+}
+
 watch(treeName, (val) => {
   treeRef.value?.filter(val)
 })
@@ -114,7 +272,18 @@ watch(
   }
 )
 
+watch(menuVisible, (visible) => {
+  if (visible) {
+    document.addEventListener('click', handleClickOutside)
+  } else {
+    document.removeEventListener('click', handleClickOutside)
+  }
+})
+
 onMounted(getTreeInfo)
+onBeforeUnmount(() => {
+  document.removeEventListener('click', handleClickOutside)
+})
 </script>
 
 <template>
@@ -144,6 +313,7 @@ onMounted(getTreeInfo)
                 :props="defaultProps"
                 highlight-current
                 node-key="id"
+                @node-contextmenu="handleRightClick"
                 @node-click="handleNodeClick">
                 <template #default="{ node }">
                   <div class="tree-node">
@@ -154,12 +324,59 @@ onMounted(getTreeInfo)
                   </div>
                 </template>
               </el-tree>
+
+              <div
+                v-show="menuVisible"
+                ref="contextMenuRef"
+                class="custom-menu"
+                :style="{ left: menuX + 'px', top: menuY + 'px' }">
+                <ul>
+                  <li @click="handleMenuClick('add')" class="flex items-center gap-1">
+                    <Icon icon="ep:plus" /> 新建目录
+                  </li>
+                  <li @click="handleMenuClick('edit')" class="flex items-center gap-1">
+                    <Icon icon="ep:edit" /> 编辑目录
+                  </li>
+                  <li @click="handleMenuClick('delete')" class="flex items-center gap-1">
+                    <Icon icon="ep:delete" /> 删除目录
+                  </li>
+                </ul>
+              </div>
             </el-scrollbar>
           </template>
         </el-auto-resizer>
       </div>
     </div>
 
+    <Dialog v-model="dialogVisible" :title="dialogTitle" style="width: 40em">
+      <el-form
+        ref="formRef"
+        v-loading="formLoading"
+        :model="formData"
+        :rules="formRules"
+        label-width="96px">
+        <div class="flex flex-col gap-4">
+          <el-form-item label="目录名称" prop="name">
+            <el-input v-model="formData.name" placeholder="请输入目录名称" />
+          </el-form-item>
+          <el-form-item label="显示排序" prop="sort">
+            <el-input-number v-model="formData.sort" :min="0" controls-position="right" />
+          </el-form-item>
+          <el-form-item label="备注" prop="remark">
+            <el-input
+              v-model="formData.remark"
+              type="textarea"
+              :rows="3"
+              placeholder="请输入备注" />
+          </el-form-item>
+        </div>
+      </el-form>
+      <template #footer>
+        <el-button type="primary" @click="submitForm">确定</el-button>
+        <el-button @click="dialogVisible = false">取消</el-button>
+      </template>
+    </Dialog>
+
     <div class="collapse-handle" @click="isCollapsed = !isCollapsed">
       <el-icon size="12">
         <CaretLeft v-if="!isCollapsed" />
@@ -233,6 +450,41 @@ onMounted(getTreeInfo)
   white-space: nowrap;
 }
 
+.custom-menu {
+  position: fixed;
+  min-width: 144px;
+  padding: 6px;
+  background: #fff;
+  border: 1px solid rgb(15 23 42 / 8%);
+  border-radius: 10px;
+  box-shadow: 0 14px 30px rgb(15 23 42 / 16%);
+  z-index: 3000;
+}
+
+.custom-menu ul {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+}
+
+.custom-menu li {
+  padding: 8px 12px;
+  margin: 0;
+  font-size: 13px;
+  line-height: 1.2;
+  color: #334155;
+  cursor: pointer;
+  border-radius: 8px;
+  transition:
+    background-color 0.18s ease,
+    color 0.18s ease;
+}
+
+.custom-menu li:hover {
+  color: #1677ff;
+  background: #eff6ff;
+}
+
 .collapse-handle {
   position: absolute;
   top: 50%;