|
|
@@ -3,15 +3,15 @@
|
|
|
<el-card shadow="never">
|
|
|
<template #header>
|
|
|
<div class="flex justify-between items-center">
|
|
|
- <span class="text-lg font-bold">文档管理</span>
|
|
|
- <el-button type="primary" @click="handleBack">返回项目列表</el-button>
|
|
|
+ <span class="text-lg font-bold">{{ t('document.document.header.title') }}</span>
|
|
|
+ <el-button type="primary" @click="handleBack">{{ t('document.document.header.backToList') }}</el-button>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<div class="content-wrapper">
|
|
|
<div class="tree-container">
|
|
|
<div class="tree-header">
|
|
|
- <el-button v-hasPermi="['document:folder:add']" type="primary" icon="Plus" size="small" @click="handleAddFolder">新建文件夹</el-button>
|
|
|
+ <el-button v-hasPermi="['document:folder:add']" type="primary" icon="Plus" size="small" @click="handleAddFolder">{{ t('document.document.button.newFolder') }}</el-button>
|
|
|
</div>
|
|
|
<el-scrollbar class="tree-scrollbar">
|
|
|
<el-tree
|
|
|
@@ -32,26 +32,67 @@
|
|
|
</el-icon>
|
|
|
<span class="node-label">{{ node.label }}</span>
|
|
|
<span class="node-actions">
|
|
|
- <el-dropdown trigger="click" @command="handleCommand($event, data)">
|
|
|
- <span class="el-dropdown-link">
|
|
|
- <el-icon><MoreFilled /></el-icon>
|
|
|
- </span>
|
|
|
- <template #dropdown>
|
|
|
- <el-dropdown-menu>
|
|
|
- <el-dropdown-item command="add" v-hasPermi="['document:folder:add']">添加子节点</el-dropdown-item>
|
|
|
- <el-dropdown-item command="edit" v-hasPermi="['document:folder:edit']">编辑</el-dropdown-item>
|
|
|
- <el-dropdown-item command="delete" v-hasPermi="['document:folder:remove']">删除</el-dropdown-item>
|
|
|
- </el-dropdown-menu>
|
|
|
- </template>
|
|
|
- </el-dropdown>
|
|
|
+ <span class="menu-trigger" @click="toggleMenu($event, data)">
|
|
|
+ <el-icon><MoreFilled /></el-icon>
|
|
|
+ </span>
|
|
|
</span>
|
|
|
</span>
|
|
|
</template>
|
|
|
</el-tree>
|
|
|
</el-scrollbar>
|
|
|
</div>
|
|
|
+
|
|
|
+ <!-- 一级菜单 -->
|
|
|
+ <ul class="primary-menu" v-if="activeMenu !== null" :style="primaryMenuStyle">
|
|
|
+ <li
|
|
|
+ class="menu-item has-submenu"
|
|
|
+ v-hasPermi="['document:folder:add']"
|
|
|
+ @click.stop="toggleSubmenu($event)"
|
|
|
+ >
|
|
|
+ <span>{{ t('document.document.menu.add') }}</span>
|
|
|
+ <el-icon class="arrow-icon"><ArrowRight /></el-icon>
|
|
|
+ </li>
|
|
|
+ <li
|
|
|
+ class="menu-item"
|
|
|
+ v-hasPermi="['document:folder:edit']"
|
|
|
+ @click="handleMenuItemClick('edit', currentMenuData)"
|
|
|
+ >
|
|
|
+ <span>{{ t('document.document.menu.edit') }}</span>
|
|
|
+ </li>
|
|
|
+ <li
|
|
|
+ class="menu-item"
|
|
|
+ v-hasPermi="['document:folder:remove']"
|
|
|
+ @click="handleMenuItemClick('delete', currentMenuData)"
|
|
|
+ >
|
|
|
+ <span>{{ t('document.document.menu.delete') }}</span>
|
|
|
+ </li>
|
|
|
+ </ul>
|
|
|
+
|
|
|
+ <!-- 二级菜单 -->
|
|
|
+ <ul
|
|
|
+ class="secondary-menu"
|
|
|
+ v-if="showSecondaryMenu"
|
|
|
+ :style="secondaryMenuStyle"
|
|
|
+ >
|
|
|
+ <!-- 国家或中心:显示中心和文件夹 -->
|
|
|
+ <template v-if="currentMenuData && (currentMenuData.type === 1 || currentMenuData.type === 2)">
|
|
|
+ <li class="menu-item" @click="handleMenuItemClick('add:2', currentMenuData)">
|
|
|
+ <span>{{ t('document.document.menu.center') }}</span>
|
|
|
+ </li>
|
|
|
+ <li class="menu-item" @click="handleMenuItemClick('add:0', currentMenuData)">
|
|
|
+ <span>{{ t('document.document.menu.folder') }}</span>
|
|
|
+ </li>
|
|
|
+ </template>
|
|
|
+ <!-- 文件夹:只显示文件夹 -->
|
|
|
+ <template v-else-if="currentMenuData && currentMenuData.type === 0">
|
|
|
+ <li class="menu-item" @click="handleMenuItemClick('add:0', currentMenuData)">
|
|
|
+ <span>{{ t('document.document.menu.folder') }}</span>
|
|
|
+ </li>
|
|
|
+ </template>
|
|
|
+ </ul>
|
|
|
+
|
|
|
<div class="content-container">
|
|
|
- <el-empty description="文档内容展示区域">
|
|
|
+ <el-empty :description="t('document.document.empty.description')">
|
|
|
</el-empty>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -59,31 +100,32 @@
|
|
|
|
|
|
<!-- 添加文件夹对话框 -->
|
|
|
<el-dialog v-model="dialog.visible" :title="dialog.title" width="600px" append-to-body>
|
|
|
- <el-form ref="folderFormRef" :model="form" :rules="rules" label-width="100px">
|
|
|
- <el-form-item label="名称" prop="name">
|
|
|
- <el-input v-model="form.name" placeholder="请输入名称" clearable />
|
|
|
+ <el-form ref="folderFormRef" :model="form" :rules="rules" label-width="140px">
|
|
|
+ <el-form-item :label="t('document.document.form.name')" prop="name">
|
|
|
+ <el-input v-model="form.name" :placeholder="t('document.document.form.namePlaceholder')" clearable />
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="类型" prop="type">
|
|
|
- <el-select v-model="form.type" placeholder="请选择类型" style="width: 100%;">
|
|
|
- <el-option
|
|
|
- v-for="typeOption in getAvailableTypes()"
|
|
|
- :key="typeOption.value"
|
|
|
- :label="typeOption.label"
|
|
|
- :value="typeOption.value"
|
|
|
- />
|
|
|
- </el-select>
|
|
|
+ <el-form-item :label="t('document.document.form.restrictionLevel')" prop="restrictionLevel">
|
|
|
+ <el-radio-group v-model="isRestricted" @change="handleRestrictionChange">
|
|
|
+ <el-radio :label="false">{{ t('document.document.form.noRestriction') }}</el-radio>
|
|
|
+ <el-radio :label="true">{{ t('document.document.form.restricted') }}</el-radio>
|
|
|
+ </el-radio-group>
|
|
|
+ <el-input-number
|
|
|
+ v-if="isRestricted"
|
|
|
+ v-model="restrictionLevelValue"
|
|
|
+ :min="0"
|
|
|
+ :max="10000"
|
|
|
+ style="width: 100%; margin-top: 10px;"
|
|
|
+ :placeholder="t('document.document.form.restrictionLevelPlaceholder')"
|
|
|
+ />
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="限制层级" prop="restrictionLevel">
|
|
|
- <el-input-number v-model="form.restrictionLevel" :min="-1" style="width: 100%;" placeholder="请输入限制层级" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="备注" prop="note">
|
|
|
- <el-input v-model="form.note" type="textarea" :rows="4" placeholder="请输入备注" />
|
|
|
+ <el-form-item :label="t('document.document.form.note')" prop="note">
|
|
|
+ <el-input v-model="form.note" type="textarea" :rows="4" :placeholder="t('document.document.form.notePlaceholder')" />
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
<template #footer>
|
|
|
<div class="dialog-footer">
|
|
|
- <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
|
|
- <el-button @click="cancel">取 消</el-button>
|
|
|
+ <el-button :loading="buttonLoading" type="primary" @click="submitForm">{{ t('document.document.button.submit') }}</el-button>
|
|
|
+ <el-button @click="cancel">{{ t('document.document.button.cancel') }}</el-button>
|
|
|
</div>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
@@ -91,10 +133,11 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, reactive, onMounted, getCurrentInstance } from 'vue';
|
|
|
+import { ref, reactive, onMounted, onUnmounted, nextTick, getCurrentInstance, watch } from 'vue';
|
|
|
+import { useI18n } from 'vue-i18n';
|
|
|
import { listFolder, addFolder, delFolder, getFolder, updateFolder } from '@/api/document/folder';
|
|
|
import { FolderListVO, FolderForm } from '@/api/document/folder/types';
|
|
|
-import { Folder, Document, Edit, Delete, Plus, MoreFilled, Location, OfficeBuilding } from '@element-plus/icons-vue';
|
|
|
+import { Folder, Document, Edit, Delete, Plus, MoreFilled, Location, OfficeBuilding, ArrowRight } from '@element-plus/icons-vue';
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
|
import type { FormInstance } from 'element-plus';
|
|
|
import type { ComponentInternalInstance } from 'vue';
|
|
|
@@ -110,6 +153,7 @@ const emit = defineEmits<{
|
|
|
}>();
|
|
|
|
|
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
|
+const { t } = useI18n();
|
|
|
|
|
|
// 数据定义
|
|
|
const loading = ref(false);
|
|
|
@@ -127,6 +171,10 @@ const dialog = reactive({
|
|
|
// 当前操作的节点
|
|
|
const currentNode = ref<FolderListVO | null>(null);
|
|
|
|
|
|
+// 限制层级相关状态
|
|
|
+const isRestricted = ref(false); // 是否限制
|
|
|
+const restrictionLevelValue = ref(0); // 限制层级值
|
|
|
+
|
|
|
// 表单初始数据
|
|
|
const initFormData: FolderForm = {
|
|
|
id: undefined,
|
|
|
@@ -145,10 +193,10 @@ const form = ref<FolderForm>({ ...initFormData });
|
|
|
// 表单验证规则
|
|
|
const rules = {
|
|
|
name: [
|
|
|
- { required: true, message: '请输入名称', trigger: 'blur' }
|
|
|
+ { required: true, message: t('document.document.rule.nameRequired'), trigger: 'blur' }
|
|
|
],
|
|
|
type: [
|
|
|
- { required: true, message: '请选择类型', trigger: 'change' }
|
|
|
+ { required: true, message: t('document.document.rule.typeRequired'), trigger: 'change' }
|
|
|
]
|
|
|
};
|
|
|
|
|
|
@@ -161,7 +209,7 @@ const treeProps = {
|
|
|
// 获取文件夹列表
|
|
|
const getList = async () => {
|
|
|
if (!props.projectId) {
|
|
|
- ElMessage.warning('项目ID不存在');
|
|
|
+ ElMessage.warning(t('document.document.message.projectIdNotExist'));
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -170,7 +218,7 @@ const getList = async () => {
|
|
|
const res = await listFolder({ projectId: props.projectId } as any);
|
|
|
treeData.value = res.data || [];
|
|
|
} catch (error) {
|
|
|
- ElMessage.error('获取文件夹列表失败');
|
|
|
+ ElMessage.error(t('document.document.message.getFolderListFailed'));
|
|
|
console.error(error);
|
|
|
} finally {
|
|
|
loading.value = false;
|
|
|
@@ -185,9 +233,29 @@ const handleBack = () => {
|
|
|
// 表单重置
|
|
|
const reset = () => {
|
|
|
form.value = { ...initFormData };
|
|
|
+ isRestricted.value = false;
|
|
|
+ restrictionLevelValue.value = 0;
|
|
|
folderFormRef.value?.resetFields();
|
|
|
};
|
|
|
|
|
|
+// 处理限制状态变化
|
|
|
+const handleRestrictionChange = (value: boolean) => {
|
|
|
+ if (value) {
|
|
|
+ // 选择限制,使用restrictionLevelValue的值
|
|
|
+ form.value.restrictionLevel = restrictionLevelValue.value;
|
|
|
+ } else {
|
|
|
+ // 选择不限制,设置为-1
|
|
|
+ form.value.restrictionLevel = -1;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 监听restrictionLevelValue变化,同步更新form.restrictionLevel
|
|
|
+watch(restrictionLevelValue, (newValue) => {
|
|
|
+ if (isRestricted.value) {
|
|
|
+ form.value.restrictionLevel = newValue;
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
// 取消按钮
|
|
|
const cancel = () => {
|
|
|
reset();
|
|
|
@@ -201,7 +269,7 @@ const handleAddFolder = () => {
|
|
|
form.value.projectId = props.projectId;
|
|
|
form.value.parentId = undefined;
|
|
|
dialog.visible = true;
|
|
|
- dialog.title = '新增文件夹';
|
|
|
+ dialog.title = t('document.document.dialog.addFolder');
|
|
|
dialog.isEdit = false;
|
|
|
};
|
|
|
|
|
|
@@ -212,7 +280,7 @@ const handleAddChild = (data: FolderListVO) => {
|
|
|
form.value.projectId = props.projectId;
|
|
|
form.value.parentId = data.id;
|
|
|
dialog.visible = true;
|
|
|
- dialog.title = '新增子节点';
|
|
|
+ dialog.title = t('document.document.dialog.addChild');
|
|
|
dialog.isEdit = false;
|
|
|
};
|
|
|
|
|
|
@@ -221,9 +289,9 @@ const getAvailableTypes = () => {
|
|
|
if (!currentNode.value) {
|
|
|
// 顶级节点,可以选择所有类型
|
|
|
return [
|
|
|
- { label: '文件夹', value: 0 },
|
|
|
- { label: '国家', value: 1 },
|
|
|
- { label: '中心', value: 2 }
|
|
|
+ { label: t('document.document.type.folder'), value: 0 },
|
|
|
+ { label: t('document.document.type.country'), value: 1 },
|
|
|
+ { label: t('document.document.type.center'), value: 2 }
|
|
|
];
|
|
|
}
|
|
|
|
|
|
@@ -232,21 +300,22 @@ const getAvailableTypes = () => {
|
|
|
if (parentType === 1 || parentType === 2) {
|
|
|
// 父节点是国家或中心,子节点只能是中心或文件夹
|
|
|
return [
|
|
|
- { label: '文件夹', value: 0 },
|
|
|
- { label: '中心', value: 2 }
|
|
|
+ { label: t('document.document.type.folder'), value: 0 },
|
|
|
+ { label: t('document.document.type.center'), value: 2 }
|
|
|
];
|
|
|
} else {
|
|
|
// 父节点是文件夹,子节点只能是文件夹
|
|
|
return [
|
|
|
- { label: '文件夹', value: 0 }
|
|
|
+ { label: t('document.document.type.folder'), value: 0 }
|
|
|
];
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 下拉菜单命令处理
|
|
|
const handleCommand = (command: string, data: FolderListVO) => {
|
|
|
- if (command === 'add') {
|
|
|
- handleAddChild(data);
|
|
|
+ if (command.startsWith('add:')) {
|
|
|
+ const type = parseInt(command.split(':')[1]);
|
|
|
+ handleAddChildWithType(data, type);
|
|
|
} else if (command === 'edit') {
|
|
|
handleEdit(data);
|
|
|
} else if (command === 'delete') {
|
|
|
@@ -254,6 +323,19 @@ const handleCommand = (command: string, data: FolderListVO) => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+// 新增子节点(指定类型)
|
|
|
+const handleAddChildWithType = (data: FolderListVO, type: number) => {
|
|
|
+ reset();
|
|
|
+ currentNode.value = data;
|
|
|
+ form.value.projectId = props.projectId;
|
|
|
+ form.value.parentId = data.id;
|
|
|
+ form.value.type = type;
|
|
|
+ dialog.visible = true;
|
|
|
+ const typeLabel = type === 0 ? t('document.document.type.folder') : type === 1 ? t('document.document.type.country') : t('document.document.type.center');
|
|
|
+ dialog.title = type === 0 ? t('document.document.dialog.addFolder') : type === 1 ? t('document.document.dialog.addCountry') : t('document.document.dialog.addCenter');
|
|
|
+ dialog.isEdit = false;
|
|
|
+};
|
|
|
+
|
|
|
// 提交表单
|
|
|
const submitForm = () => {
|
|
|
folderFormRef.value?.validate(async (valid: boolean) => {
|
|
|
@@ -261,18 +343,16 @@ const submitForm = () => {
|
|
|
// 如果是编辑,显示确认对话框
|
|
|
if (dialog.isEdit) {
|
|
|
try {
|
|
|
- const typeLabel = form.value.type === 0 ? '文件夹' : form.value.type === 1 ? '国家' : '中心';
|
|
|
const confirmMessage = `
|
|
|
<div style="text-align: left;">
|
|
|
- <p><strong>名称:</strong>${form.value.name}</p>
|
|
|
- <p><strong>类型:</strong>${typeLabel}</p>
|
|
|
- <p><strong>限制层级:</strong>${form.value.restrictionLevel}</p>
|
|
|
- <p><strong>备注:</strong>${form.value.note || '无'}</p>
|
|
|
+ <p><strong>${t('document.document.confirm.nameLabel')}</strong>${form.value.name}</p>
|
|
|
+ <p><strong>${t('document.document.confirm.restrictionLevelLabel')}</strong>${form.value.restrictionLevel}</p>
|
|
|
+ <p><strong>${t('document.document.confirm.noteLabel')}</strong>${form.value.note || t('document.document.confirm.noNote')}</p>
|
|
|
</div>
|
|
|
`;
|
|
|
- await ElMessageBox.confirm(confirmMessage, '确认修改信息', {
|
|
|
- confirmButtonText: '确认',
|
|
|
- cancelButtonText: '取消',
|
|
|
+ await ElMessageBox.confirm(confirmMessage, t('document.document.dialog.confirmEdit'), {
|
|
|
+ confirmButtonText: t('document.document.message.confirmButton'),
|
|
|
+ cancelButtonText: t('document.document.message.cancelButton'),
|
|
|
type: 'warning',
|
|
|
dangerouslyUseHTMLString: true
|
|
|
});
|
|
|
@@ -285,15 +365,15 @@ const submitForm = () => {
|
|
|
try {
|
|
|
if (dialog.isEdit) {
|
|
|
await updateFolder(form.value);
|
|
|
- proxy?.$modal.msgSuccess('修改成功');
|
|
|
+ proxy?.$modal.msgSuccess(t('document.document.message.editSuccess'));
|
|
|
} else {
|
|
|
await addFolder(form.value);
|
|
|
- proxy?.$modal.msgSuccess('新增成功');
|
|
|
+ proxy?.$modal.msgSuccess(t('document.document.message.addSuccess'));
|
|
|
}
|
|
|
dialog.visible = false;
|
|
|
await getList();
|
|
|
} catch (error) {
|
|
|
- console.error(dialog.isEdit ? '修改失败' : '新增失败', error);
|
|
|
+ console.error(dialog.isEdit ? t('document.document.message.editFailed') : t('document.document.message.addFailed'), error);
|
|
|
} finally {
|
|
|
buttonLoading.value = false;
|
|
|
}
|
|
|
@@ -308,12 +388,22 @@ const handleEdit = async (data: FolderListVO) => {
|
|
|
try {
|
|
|
const res = await getFolder(data.id);
|
|
|
Object.assign(form.value, res.data);
|
|
|
+
|
|
|
+ // 设置限制层级状态
|
|
|
+ if (form.value.restrictionLevel === -1) {
|
|
|
+ isRestricted.value = false;
|
|
|
+ restrictionLevelValue.value = 0;
|
|
|
+ } else {
|
|
|
+ isRestricted.value = true;
|
|
|
+ restrictionLevelValue.value = form.value.restrictionLevel;
|
|
|
+ }
|
|
|
+
|
|
|
currentNode.value = null; // 编辑时不限制类型
|
|
|
dialog.visible = true;
|
|
|
- dialog.title = '修改文件夹';
|
|
|
+ dialog.title = t('document.document.dialog.editFolder');
|
|
|
dialog.isEdit = true;
|
|
|
} catch (error) {
|
|
|
- ElMessage.error('获取文件夹信息失败');
|
|
|
+ ElMessage.error(t('document.document.message.getFolderInfoFailed'));
|
|
|
console.error(error);
|
|
|
} finally {
|
|
|
loading.value = false;
|
|
|
@@ -324,34 +414,162 @@ const handleEdit = async (data: FolderListVO) => {
|
|
|
const handleDelete = async (data: FolderListVO) => {
|
|
|
// 检查是否有子节点
|
|
|
if (data.children && data.children.length > 0) {
|
|
|
- ElMessage.warning('该文件夹下存在子节点,无法删除');
|
|
|
+ ElMessage.warning(t('document.document.message.hasChildren'));
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
- await ElMessageBox.confirm(`确认删除 "${data.name}" 吗?`, '提示', {
|
|
|
- confirmButtonText: '确定',
|
|
|
- cancelButtonText: '取消',
|
|
|
+ await ElMessageBox.confirm(t('document.document.message.deleteConfirm', { name: data.name }), t('document.document.message.deleteTitle'), {
|
|
|
+ confirmButtonText: t('document.document.message.confirmButton'),
|
|
|
+ cancelButtonText: t('document.document.message.cancelButton'),
|
|
|
type: 'warning'
|
|
|
});
|
|
|
|
|
|
loading.value = true;
|
|
|
await delFolder(data.id);
|
|
|
- ElMessage.success('删除成功');
|
|
|
+ ElMessage.success(t('document.document.message.deleteSuccess'));
|
|
|
await getList();
|
|
|
} catch (error: any) {
|
|
|
// 用户取消删除或删除失败
|
|
|
if (error !== 'cancel') {
|
|
|
- console.error('删除失败:', error);
|
|
|
+ console.error(t('document.document.message.deleteFailed'), error);
|
|
|
}
|
|
|
} finally {
|
|
|
loading.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+// 菜单状态管理
|
|
|
+const activeMenu = ref<string | number | null>(null); // 当前激活的一级菜单
|
|
|
+const showSecondaryMenu = ref(false); // 是否显示二级菜单
|
|
|
+const primaryMenuStyle = ref<any>({}); // 一级菜单的样式(位置)
|
|
|
+const secondaryMenuStyle = ref<any>({}); // 二级菜单的样式(位置)
|
|
|
+const currentMenuData = ref<FolderListVO | null>(null); // 当前操作的菜单数据
|
|
|
+
|
|
|
+// 切换菜单显示
|
|
|
+const toggleMenu = (event: MouseEvent, data: FolderListVO) => {
|
|
|
+ // 只处理点击事件,忽略其他事件
|
|
|
+ if (event.type !== 'click') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ event.stopPropagation();
|
|
|
+ event.preventDefault();
|
|
|
+
|
|
|
+ const trigger = event.currentTarget as HTMLElement;
|
|
|
+ const rect = trigger.getBoundingClientRect();
|
|
|
+
|
|
|
+ if (activeMenu.value === data.id) {
|
|
|
+ // 如果点击的是同一个菜单,关闭它
|
|
|
+ closeAllMenus();
|
|
|
+ } else {
|
|
|
+ // 关闭之前的菜单,打开新菜单
|
|
|
+ // 计算一级菜单位置
|
|
|
+ primaryMenuStyle.value = {
|
|
|
+ left: `${rect.left}px`,
|
|
|
+ top: `${rect.bottom + 2}px`
|
|
|
+ };
|
|
|
+
|
|
|
+ activeMenu.value = data.id;
|
|
|
+ currentMenuData.value = data;
|
|
|
+ showSecondaryMenu.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 切换二级菜单显示
|
|
|
+const toggleSubmenu = (event: MouseEvent) => {
|
|
|
+ // 只处理点击事件,忽略其他事件
|
|
|
+ if (event.type !== 'click') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ event.stopPropagation();
|
|
|
+ event.preventDefault();
|
|
|
+
|
|
|
+ const target = event.currentTarget as HTMLElement;
|
|
|
+ const rect = target.getBoundingClientRect();
|
|
|
+
|
|
|
+ if (showSecondaryMenu.value) {
|
|
|
+ // 如果已经显示,则关闭
|
|
|
+ showSecondaryMenu.value = false;
|
|
|
+ } else {
|
|
|
+ // 先设置位置,再显示(避免位置计算前就显示)
|
|
|
+ secondaryMenuStyle.value = {
|
|
|
+ left: `${rect.right + 5}px`,
|
|
|
+ top: `${rect.top}px`
|
|
|
+ };
|
|
|
+
|
|
|
+ // 使用 nextTick 确保位置设置后再显示
|
|
|
+ nextTick(() => {
|
|
|
+ showSecondaryMenu.value = true;
|
|
|
+ });
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 处理菜单项点击
|
|
|
+const handleMenuItemClick = (command: string, data: FolderListVO | null) => {
|
|
|
+ if (!data) return;
|
|
|
+
|
|
|
+ // 执行命令
|
|
|
+ handleCommand(command, data);
|
|
|
+
|
|
|
+ // 关闭所有菜单
|
|
|
+ closeAllMenus();
|
|
|
+};
|
|
|
+
|
|
|
+// 关闭所有菜单
|
|
|
+const closeAllMenus = () => {
|
|
|
+ showSecondaryMenu.value = false;
|
|
|
+ activeMenu.value = null;
|
|
|
+ currentMenuData.value = null;
|
|
|
+ primaryMenuStyle.value = {};
|
|
|
+ secondaryMenuStyle.value = {};
|
|
|
+};
|
|
|
+
|
|
|
+// 点击页面其他地方关闭菜单
|
|
|
+const handleClickOutside = (event: Event) => {
|
|
|
+ // 如果没有激活的菜单,直接返回
|
|
|
+ if (!activeMenu.value && !showSecondaryMenu.value) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const target = event.target as HTMLElement;
|
|
|
+
|
|
|
+ // 检查点击是否在菜单内部或触发器上
|
|
|
+ const isClickInsideMenu = target.closest('.primary-menu') ||
|
|
|
+ target.closest('.secondary-menu') ||
|
|
|
+ target.closest('.menu-trigger');
|
|
|
+
|
|
|
+ // 如果点击在菜单外部,立即关闭所有菜单
|
|
|
+ if (!isClickInsideMenu) {
|
|
|
+ closeAllMenus();
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 处理滚动事件,滚动时关闭菜单
|
|
|
+const handleScroll = () => {
|
|
|
+ if (activeMenu.value || showSecondaryMenu.value) {
|
|
|
+ closeAllMenus();
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
// 初始化
|
|
|
onMounted(() => {
|
|
|
getList();
|
|
|
+ // 添加全局点击监听(捕获阶段)
|
|
|
+ document.addEventListener('click', handleClickOutside, true);
|
|
|
+ // 添加滚动监听
|
|
|
+ document.addEventListener('scroll', handleScroll, true);
|
|
|
+});
|
|
|
+
|
|
|
+// 清理
|
|
|
+onUnmounted(() => {
|
|
|
+ // 移除全局点击监听
|
|
|
+ document.removeEventListener('click', handleClickOutside, true);
|
|
|
+ // 移除滚动监听
|
|
|
+ document.removeEventListener('scroll', handleScroll, true);
|
|
|
+ // 清理菜单状态
|
|
|
+ closeAllMenus();
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
@@ -424,6 +642,7 @@ onMounted(() => {
|
|
|
|
|
|
.node-actions {
|
|
|
display: none;
|
|
|
+ position: relative;
|
|
|
}
|
|
|
|
|
|
&:hover .node-actions {
|
|
|
@@ -432,13 +651,17 @@ onMounted(() => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-.el-dropdown-link {
|
|
|
+.menu-trigger {
|
|
|
cursor: pointer;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
font-size: 16px;
|
|
|
+ padding: 4px;
|
|
|
+ border-radius: 4px;
|
|
|
+ transition: background-color 0.3s, color 0.3s;
|
|
|
|
|
|
&:hover {
|
|
|
+ background-color: #f5f7fa;
|
|
|
color: var(--el-color-primary);
|
|
|
}
|
|
|
}
|
|
|
@@ -452,4 +675,69 @@ onMounted(() => {
|
|
|
.detail-content {
|
|
|
max-width: 800px;
|
|
|
}
|
|
|
+
|
|
|
+/* 一级菜单样式 */
|
|
|
+.primary-menu {
|
|
|
+ position: fixed;
|
|
|
+ min-width: 120px;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
+ border-radius: 4px;
|
|
|
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
|
+ padding: 5px 0;
|
|
|
+ margin: 0;
|
|
|
+ list-style: none;
|
|
|
+ z-index: 2000;
|
|
|
+
|
|
|
+ .menu-item {
|
|
|
+ padding: 8px 16px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #606266;
|
|
|
+ transition: background-color 0.3s, color 0.3s;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ white-space: nowrap;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ color: var(--el-color-primary);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.has-submenu {
|
|
|
+ .arrow-icon {
|
|
|
+ margin-left: 8px;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 二级菜单样式 */
|
|
|
+.secondary-menu {
|
|
|
+ position: fixed;
|
|
|
+ min-width: 120px;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
+ border-radius: 4px;
|
|
|
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
|
+ padding: 5px 0;
|
|
|
+ margin: 0;
|
|
|
+ list-style: none;
|
|
|
+ z-index: 3000;
|
|
|
+
|
|
|
+ .menu-item {
|
|
|
+ padding: 8px 16px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #606266;
|
|
|
+ transition: background-color 0.3s, color 0.3s;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ color: var(--el-color-primary);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
</style>
|