Browse Source

- 文件夹基本完成
- 仪表盘架子搭建完成
- 文件管理待完成

Huanyi 2 days ago
parent
commit
cd329a63b3

+ 4 - 1
src/lang/en_US.ts

@@ -5,6 +5,7 @@ import monitor from './modules/monitor/index_en';
 import setting from './modules/setting/index_en';
 import tool from './modules/tool/en_US';
 import project from './modules/project/index_en';
+import document from './modules/document/index_en';
 
 export default {
   // 路由国际化
@@ -105,5 +106,7 @@ export default {
   // 工具模块
   ...tool,
   // 项目管理模块
-  project
+  project,
+  // 文档管理模块
+  document
 };

+ 84 - 0
src/lang/modules/document/document/en_US.ts

@@ -0,0 +1,84 @@
+// Document Management Module - English Translation
+export default {
+  // Page Header
+  header: {
+    title: 'Document Management',
+    backToList: 'Back to Project List'
+  },
+  // Buttons
+  button: {
+    newFolder: 'New Folder',
+    submit: 'Submit',
+    cancel: 'Cancel'
+  },
+  // Menu
+  menu: {
+    add: 'Add',
+    edit: 'Edit',
+    delete: 'Delete',
+    country: 'Country',
+    center: 'Center',
+    folder: 'Folder'
+  },
+  // Dialog Titles
+  dialog: {
+    addFolder: 'Add Folder',
+    addChild: 'Add Child Node',
+    editFolder: 'Edit Folder',
+    addCountry: 'Add Country',
+    addCenter: 'Add Center',
+    confirmEdit: 'Confirm Edit'
+  },
+  // Form
+  form: {
+    name: 'Name',
+    namePlaceholder: 'Please enter name',
+    type: 'Type',
+    typePlaceholder: 'Please select type',
+    restrictionLevel: 'Restriction Level',
+    restrictionLevelPlaceholder: 'Please enter restriction level',
+    noRestriction: 'No Restriction',
+    restricted: 'Restricted',
+    note: 'Note',
+    notePlaceholder: 'Please enter note'
+  },
+  // Type Labels
+  type: {
+    folder: 'Folder',
+    country: 'Country',
+    center: 'Center'
+  },
+  // Confirm Info
+  confirm: {
+    nameLabel: 'Name: ',
+    restrictionLevelLabel: 'Restriction Level: ',
+    noteLabel: 'Note: ',
+    noNote: 'None'
+  },
+  // Messages
+  message: {
+    projectIdNotExist: 'Project ID does not exist',
+    getFolderListFailed: 'Failed to get folder list',
+    getFolderInfoFailed: 'Failed to get folder information',
+    editSuccess: 'Edit successfully',
+    editFailed: 'Edit failed',
+    addSuccess: 'Add successfully',
+    addFailed: 'Add failed',
+    deleteSuccess: 'Delete successfully',
+    deleteFailed: 'Delete failed',
+    hasChildren: 'This folder contains child nodes and cannot be deleted',
+    deleteConfirm: 'Confirm to delete "{name}"?',
+    deleteTitle: 'Confirm',
+    confirmButton: 'Confirm',
+    cancelButton: 'Cancel'
+  },
+  // Validation Rules
+  rule: {
+    nameRequired: 'Please enter name',
+    typeRequired: 'Please select type'
+  },
+  // Empty State
+  empty: {
+    description: 'Document content display area'
+  }
+};

+ 84 - 0
src/lang/modules/document/document/zh_CN.ts

@@ -0,0 +1,84 @@
+// 文档管理模块 - 中文翻译
+export default {
+  // 页面标题
+  header: {
+    title: '文档管理',
+    backToList: '返回项目列表'
+  },
+  // 按钮
+  button: {
+    newFolder: '新建文件夹',
+    submit: '确 定',
+    cancel: '取 消'
+  },
+  // 菜单
+  menu: {
+    add: '添加',
+    edit: '编辑',
+    delete: '删除',
+    country: '国家',
+    center: '中心',
+    folder: '文件夹'
+  },
+  // 对话框标题
+  dialog: {
+    addFolder: '新增文件夹',
+    addChild: '新增子节点',
+    editFolder: '修改文件夹',
+    addCountry: '新增国家',
+    addCenter: '新增中心',
+    confirmEdit: '确认修改信息'
+  },
+  // 表单
+  form: {
+    name: '名称',
+    namePlaceholder: '请输入名称',
+    type: '类型',
+    typePlaceholder: '请选择类型',
+    restrictionLevel: '限制层级',
+    restrictionLevelPlaceholder: '请输入限制层级',
+    noRestriction: '不限制',
+    restricted: '限制',
+    note: '备注',
+    notePlaceholder: '请输入备注'
+  },
+  // 类型标签
+  type: {
+    folder: '文件夹',
+    country: '国家',
+    center: '中心'
+  },
+  // 确认信息
+  confirm: {
+    nameLabel: '名称:',
+    restrictionLevelLabel: '限制层级:',
+    noteLabel: '备注:',
+    noNote: '无'
+  },
+  // 提示信息
+  message: {
+    projectIdNotExist: '项目ID不存在',
+    getFolderListFailed: '获取文件夹列表失败',
+    getFolderInfoFailed: '获取文件夹信息失败',
+    editSuccess: '修改成功',
+    editFailed: '修改失败',
+    addSuccess: '新增成功',
+    addFailed: '新增失败',
+    deleteSuccess: '删除成功',
+    deleteFailed: '删除失败',
+    hasChildren: '该文件夹下存在子节点,无法删除',
+    deleteConfirm: '确认删除 "{name}" 吗?',
+    deleteTitle: '提示',
+    confirmButton: '确定',
+    cancelButton: '取消'
+  },
+  // 验证规则
+  rule: {
+    nameRequired: '请输入名称',
+    typeRequired: '请选择类型'
+  },
+  // 空状态
+  empty: {
+    description: '文档内容展示区域'
+  }
+};

+ 6 - 0
src/lang/modules/document/index.ts

@@ -0,0 +1,6 @@
+// 文档管理模块 - 统一导出
+import document from './document/zh_CN';
+
+export default {
+  document
+};

+ 6 - 0
src/lang/modules/document/index_en.ts

@@ -0,0 +1,6 @@
+// Document Management Module - Export (English)
+import document from './document/en_US';
+
+export default {
+  document
+};

+ 4 - 1
src/lang/zh_CN.ts

@@ -5,6 +5,7 @@ import monitor from './modules/monitor/index';
 import setting from './modules/setting/index';
 import tool from './modules/tool/zh_CN';
 import project from './modules/project/index';
+import document from './modules/document/index';
 
 export default {
   // 路由国际化
@@ -105,5 +106,7 @@ export default {
   // 工具模块
   ...tool,
   // 项目管理模块
-  project
+  project,
+  // 文档管理模块
+  document
 };

+ 7 - 0
src/views/dashboard/taskCenter/audit/index.vue

@@ -0,0 +1,7 @@
+<script setup lang="ts"></script>
+
+<template>
+  <div>文件审批任务</div>
+</template>
+
+<style scoped lang="scss"></style>

+ 7 - 0
src/views/dashboard/taskCenter/filing/index.vue

@@ -0,0 +1,7 @@
+<script setup lang="ts"></script>
+
+<template>
+  <div>文件归档任务</div>
+</template>
+
+<style scoped lang="scss"></style>

+ 7 - 0
src/views/dashboard/taskCenter/qualityControl/index.vue

@@ -0,0 +1,7 @@
+<script setup lang="ts"></script>
+
+<template>
+  <div>文件质控任务</div>
+</template>
+
+<style scoped lang="scss"></style>

+ 7 - 0
src/views/dashboard/taskCenter/questionResponse/index.vue

@@ -0,0 +1,7 @@
+<script setup lang="ts"></script>
+
+<template>
+  <div>质疑答复任务</div>
+</template>
+
+<style scoped lang="scss"></style>

+ 7 - 0
src/views/dashboard/taskCenter/submission/index.vue

@@ -0,0 +1,7 @@
+<script setup lang="ts"></script>
+
+<template>
+  <div>文件递交任务</div>
+</template>
+
+<style scoped lang="scss"></style>

+ 316 - 0
src/views/dashboard/workbench/index.vue

@@ -0,0 +1,316 @@
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue';
+import { Document, Upload, FolderOpened, MessageBox, Clock } from '@element-plus/icons-vue';
+import * as echarts from 'echarts';
+import type { EChartsOption } from 'echarts';
+
+// 统计数据
+const statistics = ref({
+  pendingUpload: 0,      // 待上传文件数
+  pendingArchive: 0,     // 待归档文件数
+  pendingSubmit: 0,      // 待递交文件数
+  overdueSubmit: 0,      // 逾期未递交文件数
+  pendingReview: 0       // 待审核文件数
+});
+
+// 卡片配置
+const cardConfigs = [
+  {
+    title: '待上传文件数',
+    key: 'pendingUpload',
+    icon: Upload,
+    color: '#409EFF',
+    bgColor: '#ecf5ff'
+  },
+  {
+    title: '待归档文件数',
+    key: 'pendingArchive',
+    icon: FolderOpened,
+    color: '#67C23A',
+    bgColor: '#f0f9ff'
+  },
+  {
+    title: '待递交文件数',
+    key: 'pendingSubmit',
+    icon: Document,
+    color: '#E6A23C',
+    bgColor: '#fdf6ec'
+  },
+  {
+    title: '逾期未递交文件数',
+    key: 'overdueSubmit',
+    icon: Clock,
+    color: '#F56C6C',
+    bgColor: '#fef0f0'
+  },
+  {
+    title: '待审核文件数',
+    key: 'pendingReview',
+    icon: MessageBox,
+    color: '#909399',
+    bgColor: '#f4f4f5'
+  }
+];
+
+// 图表实例
+const pieChartRef = ref<HTMLDivElement>();
+let pieChartInstance: echarts.ECharts | null = null;
+
+// 获取统计数据
+const fetchStatistics = async () => {
+  try {
+    // TODO: 替换为实际的API调用
+    // const res = await getWorkbenchStatistics();
+    // statistics.value = res.data;
+    
+    // 模拟数据
+    statistics.value = {
+      pendingUpload: 12,
+      pendingArchive: 8,
+      pendingSubmit: 15,
+      overdueSubmit: 3,
+      pendingReview: 6
+    };
+  } catch (error) {
+    console.error('获取统计数据失败:', error);
+  }
+};
+
+// 初始化饼状图
+const initPieChart = () => {
+  if (!pieChartRef.value) return;
+  
+  // 如果已存在实例,先销毁
+  if (pieChartInstance) {
+    pieChartInstance.dispose();
+  }
+  
+  pieChartInstance = echarts.init(pieChartRef.value);
+  
+  const option: EChartsOption = {
+    tooltip: {
+      trigger: 'item',
+      formatter: '{a} <br/>{b}: {c} ({d}%)'
+    },
+    legend: {
+      orient: 'vertical',
+      right: '10%',
+      top: 'center',
+      itemGap: 15,
+      textStyle: {
+        fontSize: 12
+      }
+    },
+    series: [
+      {
+        name: '文件统计',
+        type: 'pie',
+        radius: ['40%', '70%'],
+        center: ['35%', '50%'],
+        avoidLabelOverlap: false,
+        itemStyle: {
+          borderRadius: 10,
+          borderColor: '#fff',
+          borderWidth: 2
+        },
+        label: {
+          show: false,
+          position: 'center'
+        },
+        emphasis: {
+          label: {
+            show: true,
+            fontSize: 20,
+            fontWeight: 'bold'
+          }
+        },
+        labelLine: {
+          show: false
+        },
+        data: [
+          { value: statistics.value.pendingUpload, name: '待上传文件数', itemStyle: { color: '#409EFF' } },
+          { value: statistics.value.pendingArchive, name: '待归档文件数', itemStyle: { color: '#67C23A' } },
+          { value: statistics.value.pendingSubmit, name: '待递交文件数', itemStyle: { color: '#E6A23C' } },
+          { value: statistics.value.overdueSubmit, name: '逾期未递交文件数', itemStyle: { color: '#F56C6C' } },
+          { value: statistics.value.pendingReview, name: '待审核文件数', itemStyle: { color: '#909399' } }
+        ]
+      }
+    ]
+  };
+  
+  pieChartInstance.setOption(option);
+};
+
+// 监听统计数据变化,更新图表
+watch(statistics, () => {
+  if (pieChartInstance) {
+    initPieChart();
+  }
+}, { deep: true });
+
+// 窗口大小变化时调整图表
+const handleResize = () => {
+  pieChartInstance?.resize();
+};
+
+onMounted(async () => {
+  await fetchStatistics();
+  await nextTick();
+  initPieChart();
+  window.addEventListener('resize', handleResize);
+});
+
+onUnmounted(() => {
+  window.removeEventListener('resize', handleResize);
+  if (pieChartInstance) {
+    pieChartInstance.dispose();
+  }
+});
+</script>
+
+<template>
+  <div class="workbench-container">
+    <!-- 第一行:统计卡片 -->
+    <div class="statistics-cards">
+      <el-card 
+        v-for="card in cardConfigs" 
+        :key="card.key"
+        class="stat-card"
+        shadow="hover"
+      >
+        <div class="card-content">
+          <div class="icon-wrapper" :style="{ backgroundColor: card.bgColor }">
+            <el-icon :size="32" :color="card.color">
+              <component :is="card.icon" />
+            </el-icon>
+          </div>
+          <div class="info-wrapper">
+            <div class="title">{{ card.title }}</div>
+            <div class="count" :style="{ color: card.color }">
+              {{ statistics[card.key] }}
+            </div>
+          </div>
+        </div>
+      </el-card>
+    </div>
+    
+    <!-- 第二行:饼状图 -->
+    <div class="chart-section">
+      <el-card class="chart-card" shadow="hover">
+        <template #header>
+          <div class="card-header">
+            <span class="header-title">文件统计分布</span>
+          </div>
+        </template>
+        <div ref="pieChartRef" class="pie-chart"></div>
+      </el-card>
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.workbench-container {
+  padding: 20px;
+
+  .statistics-cards {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+    gap: 20px;
+    margin-bottom: 20px;
+
+    @media (max-width: 1400px) {
+      grid-template-columns: repeat(3, 1fr);
+    }
+
+    @media (max-width: 992px) {
+      grid-template-columns: repeat(2, 1fr);
+    }
+
+    @media (max-width: 576px) {
+      grid-template-columns: 1fr;
+    }
+
+    .stat-card {
+      cursor: pointer;
+      transition: transform 0.3s ease, box-shadow 0.3s ease;
+
+      &:hover {
+        transform: translateY(-5px);
+        box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
+      }
+
+      :deep(.el-card__body) {
+        padding: 20px;
+      }
+
+      .card-content {
+        display: flex;
+        align-items: center;
+        gap: 16px;
+
+        .icon-wrapper {
+          width: 64px;
+          height: 64px;
+          border-radius: 12px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          flex-shrink: 0;
+        }
+
+        .info-wrapper {
+          flex: 1;
+          min-width: 0;
+
+          .title {
+            font-size: 14px;
+            color: #606266;
+            margin-bottom: 8px;
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+          }
+
+          .count {
+            font-size: 28px;
+            font-weight: bold;
+            line-height: 1;
+          }
+        }
+      }
+    }
+  }
+
+  .chart-section {
+    margin-top: 20px;
+    
+    .chart-card {
+      :deep(.el-card__header) {
+        padding: 16px 20px;
+        border-bottom: 1px solid #ebeef5;
+      }
+
+      :deep(.el-card__body) {
+        padding: 20px;
+      }
+
+      .card-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+
+        .header-title {
+          font-size: 16px;
+          font-weight: 600;
+          color: #303133;
+        }
+      }
+
+      .pie-chart {
+        width: 100%;
+        height: 400px;
+      }
+    }
+  }
+}
+</style>

+ 359 - 71
src/views/document/folder/document.vue

@@ -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>

+ 4 - 4
src/views/document/folder/project.vue

@@ -113,11 +113,11 @@
         </el-table-column>
         <el-table-column :label="t('project.management.table.createTime')" align="center" prop="createTime" min-width="180" />
         <el-table-column :label="t('project.management.table.updateTime')" align="center" prop="updateTime" min-width="180" />
-        <el-table-column :label="t('project.management.table.actions')" align="center" width="150" fixed="right">
+        <el-table-column :label="t('project.management.table.actions')" align="center" width="80" fixed="right">
           <template #default="scope">
-            <el-button type="primary" link @click="handleEnterProject(scope.row)">
-              {{ t('project.management.table.enterProject') }}
-            </el-button>
+            <el-tooltip :content="t('project.management.table.enterProject')" placement="top">
+              <el-button v-hasPermi="['document:folder:entry']" type="primary" link icon="ArrowRight" @click="handleEnterProject(scope.row)" />
+            </el-tooltip>
           </template>
         </el-table-column>
       </el-table>

+ 11 - 10
src/views/project/management/detail/pages/centerInfo.vue

@@ -42,17 +42,18 @@
         <el-table-column label="创建时间" align="center" prop="createTime" width="180" />
         <el-table-column label="更新人" align="center" prop="updateBy" width="120" show-overflow-tooltip />
         <el-table-column label="更新时间" align="center" prop="updateTime" width="180" />
-        <el-table-column label="操作" align="center" width="120" fixed="right">
+        <el-table-column label="操作" align="center" width="80" fixed="right">
           <template #default="scope">
-            <el-button
-              v-hasPermi="['project:management:queryCenterInfoInviteMember']"
-              type="primary"
-              size="small"
-              link
-              @click="handleInviteMember(scope.row)"
-            >
-              邀请成员
-            </el-button>
+            <el-tooltip content="邀请成员" placement="top">
+              <el-button
+                v-hasPermi="['project:management:queryCenterInfoInviteMember']"
+                type="primary"
+                size="small"
+                link
+                icon="User"
+                @click="handleInviteMember(scope.row)"
+              />
+            </el-tooltip>
           </template>
         </el-table-column>
       </el-table>

+ 7 - 0
src/views/qualityControl/index.vue

@@ -0,0 +1,7 @@
+<script setup lang="ts"></script>
+
+<template>
+  <div>文件质控</div>
+</template>
+
+<style scoped lang="scss"></style>