Prechádzať zdrojové kódy

项目管理完成一小半

Huanyi 3 dní pred
rodič
commit
255d393431

+ 84 - 0
TopNav-Width-Test-Guide.md

@@ -0,0 +1,84 @@
+# 顶部菜单动态宽度检测测试指南
+
+## 功能说明
+顶部菜单现在会根据可用空间动态调整显示的菜单项数量,超出部分会自动放入"更多"菜单中。
+
+## 测试场景
+
+### 1. 窗口大小调整
+- **操作**: 拖动浏览器窗口调整大小
+- **预期**: 菜单项数量会实时调整,确保不与右侧"Select Company"输入框重叠
+- **间隔**: 20px 安全边距
+
+### 2. 侧边栏展开/收起
+- **操作**: 点击汉堡菜单图标切换侧边栏状态
+- **预期**: 
+  - 侧边栏收起时,顶部菜单可用空间增加,显示更多菜单项
+  - 侧边栏展开时,顶部菜单可用空间减少,部分菜单项移入"更多"
+- **延迟**: 300ms(等待动画完成)
+
+### 3. 语言切换
+- **操作**: 切换系统语言(中文/英文)
+- **预期**: 
+  - 菜单文本长度改变
+  - 自动重新计算可显示的菜单项数量
+  - 保持不与右侧元素重叠
+
+### 4. 路由切换
+- **操作**: 点击不同的菜单项切换页面
+- **预期**: 
+  - 切换页面时重新计算可用宽度
+  - 确保布局稳定
+- **延迟**: 150ms(防抖)
+
+### 5. 顶部导航模式切换
+- **操作**: 在设置中切换"顶部导航"开关
+- **预期**: 切换到顶部导航模式时,立即计算并正确显示菜单
+
+## 调试模式
+
+如果需要查看详细的计算过程,可以在代码中启用调试模式:
+
+```typescript
+// 在 TopNav/index.vue 中
+const DEBUG_MODE = true; // 改为 true
+```
+
+启用后,浏览器控制台会显示:
+- 可用宽度计算详情
+- 每个菜单项的宽度
+- 最终显示的菜单数量
+
+## 关键参数
+
+| 参数 | 值 | 说明 |
+|------|-----|------|
+| 安全边距 | 20px | 顶部菜单与右侧元素之间的间隔 |
+| "更多"按钮预留宽度 | 100px | 用于判断是否需要显示"更多"按钮 |
+| 侧边栏动画延迟 | 300ms | 等待侧边栏展开/收起动画完成 |
+| 路由切换防抖 | 150ms | 避免频繁计算 |
+| 窗口调整防抖 | 200ms | 优化性能 |
+
+## 已监听的变化
+
+✅ 窗口大小调整 (resize)  
+✅ 侧边栏展开/收起状态 (sidebar.opened)  
+✅ 侧边栏显示/隐藏 (sidebar.hide)  
+✅ 顶部导航模式切换 (topNav)  
+✅ 语言切换 (locale)  
+✅ 菜单内容变化 (topMenus)  
+✅ 路由变化 (route.path)  
+
+## 边界条件处理
+
+1. **最小宽度保护**: 可用宽度至少 100px
+2. **至少显示一项**: 即使空间不足,也会显示至少一个菜单项
+3. **右侧菜单未就绪**: 首次渲染时会延迟 100ms 重试
+4. **动画未完成**: 侧边栏动画完成后再计算
+
+## 注意事项
+
+- 所有计算都在 `nextTick()` 中执行,确保 DOM 已更新
+- 使用防抖机制避免过度计算
+- 支持动态菜单数量变化
+- 自适应不同分辨率和设备

+ 63 - 0
src/api/document/folder/index.ts

@@ -0,0 +1,63 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { FolderVO, FolderForm, FolderQuery } from '@/api/document/folder/types';
+
+/**
+ * 查询文件夹管理列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listFolder = (query?: FolderQuery): AxiosPromise<FolderVO[]> => {
+  return request({
+    url: '/document/folder/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询文件夹管理详细
+ * @param id
+ */
+export const getFolder = (id: string | number): AxiosPromise<FolderVO> => {
+  return request({
+    url: '/document/folder/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增文件夹管理
+ * @param data
+ */
+export const addFolder = (data: FolderForm) => {
+  return request({
+    url: '/document/folder',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改文件夹管理
+ * @param data
+ */
+export const updateFolder = (data: FolderForm) => {
+  return request({
+    url: '/document/folder',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除文件夹管理
+ * @param id
+ */
+export const delFolder = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/document/folder/' + id,
+    method: 'delete'
+  });
+};

+ 118 - 0
src/api/document/folder/types.ts

@@ -0,0 +1,118 @@
+export interface FolderVO {
+  /**
+   * 序号
+   */
+  id: string | number;
+
+  /**
+   * 所属项目
+   */
+  projectId: string | number;
+
+  /**
+   * 父级
+   */
+  parentId: string | number;
+
+  /**
+   * 类型
+   */
+  type: number;
+
+  /**
+   * 名称
+   */
+  name: string;
+
+  /**
+   * 状态
+   */
+  status: number;
+
+  /**
+   * 创建时间
+   */
+  createTime: string;
+
+  /**
+   * 更新时间
+   */
+  updateTime: string;
+
+}
+
+export interface FolderForm extends BaseEntity {
+  /**
+   * 序号
+   */
+  id?: string | number;
+
+  /**
+   * 所属项目
+   */
+  projectId?: string | number;
+
+  /**
+   * 父级
+   */
+  parentId?: string | number;
+
+  /**
+   * 类型
+   */
+  type?: number;
+
+  /**
+   * 名称
+   */
+  name?: string;
+
+  /**
+   * 状态
+   */
+  status?: number;
+
+  /**
+   * 备注
+   */
+  note?: string;
+
+}
+
+export interface FolderQuery extends PageQuery {
+
+  /**
+   * 所属项目
+   */
+  projectId?: string | number;
+
+  /**
+   * 类型
+   */
+  type?: number;
+
+  /**
+   * 名称
+   */
+  name?: string;
+
+  /**
+   * 状态
+   */
+  status?: number;
+
+  /**
+   * 创建时间
+   */
+  createTime?: string;
+
+  /**
+   * 更新时间
+   */
+  updateTime?: string;
+
+  /**
+   * 日期范围参数
+   */
+  params?: any;
+}

+ 89 - 0
src/api/project/management/index.ts

@@ -0,0 +1,89 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { ManagementVO, ManagementForm, ManagementQuery, ProjectMemberVO, ProjectMemberQuery } from '@/api/project/management/types';
+
+/**
+ * 查询项目管理列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listManagement = (query?: ManagementQuery): AxiosPromise<ManagementVO[]> => {
+  return request({
+    url: '/project/management/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询项目管理详细
+ * @param id
+ */
+export const getManagement = (id: string | number): AxiosPromise<ManagementVO> => {
+  return request({
+    url: '/project/management/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增项目管理
+ * @param data
+ */
+export const addManagement = (data: ManagementForm) => {
+  return request({
+    url: '/project/management',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改项目管理
+ * @param data
+ */
+export const updateManagement = (data: ManagementForm) => {
+  return request({
+    url: '/project/management',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除项目管理
+ * @param id
+ */
+export const delManagement = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/project/management/' + id,
+    method: 'delete'
+  });
+};
+
+/**
+ * 更新项目状态
+ * @param id 项目ID
+ * @param status 状态值
+ */
+export const updateStatus = (id: string | number, status: number) => {
+  return request({
+    url: '/project/management/updateStatus',
+    method: 'put',
+    data: { id, status }
+  });
+};
+
+/**
+ * 查询项目成员列表
+ * @param query
+ * @returns {*}
+ */
+export const queryProjectMember = (query: ProjectMemberQuery): AxiosPromise<ProjectMemberVO[]> => {
+  return request({
+    url: '/project/management/queryProjectMember',
+    method: 'get',
+    params: query
+  });
+};

+ 278 - 0
src/api/project/management/types.ts

@@ -0,0 +1,278 @@
+export interface ManagementVO {
+  /**
+   * 序号
+   */
+  id: string | number;
+
+  /**
+   * 项目编号
+   */
+  code: string;
+
+  /**
+   * 名称
+   */
+  name: string;
+
+  /**
+   * 项目语言
+   */
+  language: string;
+
+  /**
+   * 项目类型
+   */
+  type: string;
+
+  /**
+   * 状态
+   */
+  status: number;
+
+  /**
+   * PD/GPD
+   */
+  pdGpd: string;
+
+  /**
+   * PM/GPM
+   */
+  pmGpm: string;
+
+  /**
+   * CTA/GCTA
+   */
+  ctaGcta: string;
+
+  /**
+   * 申办方
+   */
+  sponsor: string;
+
+  /**
+   * CRO
+   */
+  cro: string;
+
+  /**
+   * 开始时间
+   */
+  startTime: string;
+
+  /**
+   * 结束时间
+   */
+  endTime: string;
+
+  /**
+   * 创建时间
+   */
+  createTime: string;
+
+  /**
+   * 更新时间
+   */
+  updateTime: string;
+
+}
+
+export interface ManagementForm extends BaseEntity {
+  /**
+   * 序号
+   */
+  id?: string | number;
+
+  /**
+   * 项目编号
+   */
+  code?: string;
+
+  /**
+   * 名称
+   */
+  name?: string;
+
+  /**
+   * 图标
+   */
+  icon?: number;
+
+  /**
+   * 项目语言
+   */
+  language?: string;
+
+  /**
+   * 项目类型
+   */
+  type?: string;
+
+  /**
+   * 状态
+   */
+  status?: number;
+
+  /**
+   * PD/GPD
+   */
+  pdGpd?: string;
+
+  /**
+   * PM/GPM
+   */
+  pmGpm?: string;
+
+  /**
+   * CTA/GCTA
+   */
+  ctaGcta?: string;
+
+  /**
+   * 申办方
+   */
+  sponsor?: string;
+
+  /**
+   * CRO
+   */
+  cro?: string;
+
+  /**
+   * 备注
+   */
+  note?: string;
+
+  /**
+   * 开始时间
+   */
+  startTime?: string;
+
+  /**
+   * 结束时间
+   */
+  endTime?: string;
+
+}
+
+export interface ManagementQuery extends PageQuery {
+
+  /**
+   * 项目编号
+   */
+  code?: string;
+
+  /**
+   * 名称
+   */
+  name?: string;
+
+  /**
+   * 项目语言
+   */
+  language?: string;
+
+  /**
+   * 项目类型
+   */
+  type?: string;
+
+  /**
+   * 状态
+   */
+  status?: number;
+
+  /**
+   * PD/GPD
+   */
+  pdGpd?: string;
+
+  /**
+   * PM/GPM
+   */
+  pmGpm?: string;
+
+  /**
+   * CTA/GCTA
+   */
+  ctaGcta?: string;
+
+  /**
+   * 申办方
+   */
+  sponsor?: string;
+
+  /**
+   * CRO
+   */
+  cro?: string;
+
+  /**
+   * 开始时间
+   */
+  startTime?: string;
+
+  /**
+   * 结束时间
+   */
+  endTime?: string;
+
+  /**
+   * 创建时间
+   */
+  createTime?: string;
+
+  /**
+   * 更新时间
+   */
+  updateTime?: string;
+
+  /**
+   * 日期范围参数
+   */
+  params?: any;
+}
+
+/**
+ * 项目成员
+ */
+export interface ProjectMemberVO {
+  /**
+   * 序号
+   */
+  id: string | number;
+
+  /**
+   * 姓名
+   */
+  name: string;
+
+  /**
+   * 手机号
+   */
+  phoneNumber: string;
+
+  /**
+   * 部门
+   */
+  dept: string;
+
+  /**
+   * 角色
+   */
+  role: string;
+
+  /**
+   * 时间
+   */
+  time: string;
+}
+
+/**
+ * 项目成员查询参数
+ */
+export interface ProjectMemberQuery extends PageQuery {
+  /**
+   * 项目ID
+   */
+  id: string | number;
+}

+ 2 - 0
src/api/system/dict/data/types.ts

@@ -7,6 +7,8 @@ export interface DictDataQuery extends PageQuery {
 export interface DictDataVO extends BaseEntity {
   dictCode: string;
   dictLabel: string;
+  dictLabelZh?: string;
+  dictLabelEn?: string;
   dictValue: string;
   cssClass: string;
   listClass: ElTagType;

+ 2 - 0
src/api/system/user/types.ts

@@ -49,6 +49,7 @@ export interface UserVO extends BaseEntity {
   postIds: any;
   roleId: any;
   admin: boolean;
+  projects?: string;
 }
 
 /**
@@ -68,6 +69,7 @@ export interface UserForm {
   remark?: string;
   postIds: string[];
   roleIds: string[];
+  projects?: string;
 }
 
 export interface UserInfoVO {

+ 1 - 1
src/components/Breadcrumb/index.vue

@@ -48,7 +48,7 @@ const getBreadcrumb = () => {
   }
   // 判断是否为首页
   if (!isDashboard(matched[0])) {
-    matched = [{ path: '/index', meta: { title: '{"zh_CN":"首页","en_US":"Dashboard"}' } }].concat(matched);
+    matched = [{ path: '/index', meta: { title: '{"zh_CN":"首页","en_US":"Home Page"}' } }].concat(matched);
   }
   levelList.value = matched.filter((item) => item.meta && item.meta.title && item.meta.breadcrumb !== false);
 };

+ 228 - 15
src/components/TopNav/index.vue

@@ -1,10 +1,16 @@
 <template>
-  <el-menu :default-active="activeMenu" mode="horizontal" :ellipsis="false" @select="handleSelect">
+  <el-menu ref="menuRef" :default-active="activeMenu" mode="horizontal" :ellipsis="false" @select="handleSelect">
     <template v-for="(item, index) in topMenus">
-      <el-menu-item v-if="index < visibleNumber" :key="index" :style="{ '--theme': theme }" :index="item.path"
-        ><svg-icon v-if="item.meta && item.meta.icon && item.meta.icon !== '#'" :icon-class="item.meta ? item.meta.icon : ''" />
-        {{ item.meta?.title }}</el-menu-item
+      <el-menu-item 
+        v-if="index < visibleNumber" 
+        :key="index" 
+        :style="{ '--theme': theme }" 
+        :index="item.path"
+        :ref="(el) => setMenuItemRef(el, index)"
       >
+        <svg-icon v-if="item.meta && item.meta.icon && item.meta.icon !== '#'" :icon-class="item.meta ? item.meta.icon : ''" />
+        {{ getMenuTitle(item) }}
+      </el-menu-item>
     </template>
 
     <!-- 顶部菜单超出数量折叠 -->
@@ -12,7 +18,7 @@
       <template #title>{{ $t('components.topNav.moreMenu') }}</template>
       <template v-for="(item, index) in topMenus">
         <el-menu-item v-if="index >= visibleNumber" :key="index" :index="item.path"
-          ><svg-icon :icon-class="item.meta ? item.meta.icon : ''" /> {{ item.meta?.title }}</el-menu-item
+          ><svg-icon :icon-class="item.meta ? item.meta.icon : ''" /> {{ getMenuTitle(item) }}</el-menu-item
         >
       </template>
     </el-sub-menu>
@@ -26,27 +32,48 @@ import { useAppStore } from '@/store/modules/app';
 import { useSettingsStore } from '@/store/modules/settings';
 import { usePermissionStore } from '@/store/modules/permission';
 import { RouteRecordRaw } from 'vue-router';
+import { parseI18nName } from '@/utils/i18n';
+import { useI18n } from 'vue-i18n';
 
-// 顶部栏初始数
-const visibleNumber = ref<number>(-1);
+// 顶部栏可见菜单数量
+const visibleNumber = ref<number>(999); // 初始值设为较大数,确保首次渲染所有菜单用于测量
 // 当前激活菜单的 index
 const currentIndex = ref<string>();
 // 隐藏侧边栏路由
 const hideList = ['/index', '/user/profile'];
+// 菜单容器引用
+const menuRef = ref<any>(null);
+// 菜单项引用数组
+const menuItemRefs = ref<any[]>([]);
+// 是否首次计算
+const isFirstCalculation = ref(true);
+// 计算定时器,用于防抖
+let calculateTimer: any = null;
+// 是否启用调试模式(开发时可设为true)
+const DEBUG_MODE = false;
 
 const appStore = useAppStore();
 const settingsStore = useSettingsStore();
 const permissionStore = usePermissionStore();
 const route = useRoute();
 const router = useRouter();
+const { locale } = useI18n();
 
 // 主题颜色
 const theme = computed(() => settingsStore.theme);
 // 所有的路由信息
 const routers = computed(() => permissionStore.getTopbarRoutes());
+// 侧边栏状态
+const sidebarOpened = computed(() => appStore.sidebar.opened);
+// 侧边栏是否隐藏
+const sidebarHide = computed(() => appStore.sidebar.hide);
+// 是否显示顶部导航
+const topNav = computed(() => settingsStore.topNav);
 
 // 顶部显示菜单
 const topMenus = computed(() => {
+  // 添加 locale 依赖,确保语言切换时更新
+  const _ = locale.value;
   const topMenus: RouteRecordRaw[] = [];
   routers.value.map((menu) => {
     if (menu.hidden !== true) {
@@ -61,6 +88,11 @@ const topMenus = computed(() => {
   return topMenus;
 });
 
+// 解析菜单标题的国际化名称
+const getMenuTitle = (item: RouteRecordRaw): string => {
+  return parseI18nName(item.meta?.title);
+};
+
 // 设置子路由
 const childrenMenus = computed(() => {
   const childrenMenus: RouteRecordRaw[] = [];
@@ -103,9 +135,123 @@ const activeMenu = computed(() => {
   return activePath;
 });
 
-const setVisibleNumber = () => {
-  const width = document.body.getBoundingClientRect().width / 3;
-  visibleNumber.value = parseInt(String(width / 85));
+// 设置菜单项引用
+const setMenuItemRef = (el: any, index: number) => {
+  if (el) {
+    menuItemRefs.value[index] = el;
+  }
+};
+
+// 带防抖的计算菜单可见数量(用于频繁触发的场景)
+const debouncedCalculateVisibleMenus = (delay: number = 100) => {
+  if (calculateTimer) {
+    clearTimeout(calculateTimer);
+  }
+  calculateTimer = setTimeout(() => {
+    calculateVisibleMenus();
+  }, delay);
+};
+
+// 动态计算可见菜单数量
+const calculateVisibleMenus = () => {
+  nextTick(() => {
+    if (!menuRef.value || topMenus.value.length === 0) {
+      return;
+    }
+
+    const menuElement = menuRef.value.$el as HTMLElement;
+    if (!menuElement) {
+      return;
+    }
+
+    // 获取右侧菜单元素的位置
+    const rightMenu = document.querySelector('.navbar .right-menu') as HTMLElement;
+    if (!rightMenu) {
+      // 如果还没有找到右侧菜单,延迟重试
+      if (isFirstCalculation.value) {
+        setTimeout(() => calculateVisibleMenus(), 100);
+      }
+      return;
+    }
+
+    // 计算可用宽度:右侧菜单的左边界 - 顶部菜单的左边界 - 安全边距
+    const menuLeft = menuElement.getBoundingClientRect().left;
+    const rightMenuLeft = rightMenu.getBoundingClientRect().left;
+    const safeMargin = 20; // 安全边距,避免紧贴
+    const availableWidth = Math.max(100, rightMenuLeft - menuLeft - safeMargin); // 最小宽度100px
+
+    if (DEBUG_MODE) {
+      console.log('[TopNav] 计算可用宽度:', {
+        menuLeft,
+        rightMenuLeft,
+        safeMargin,
+        availableWidth,
+        menuCount: topMenus.value.length
+      });
+    }
+
+    let accumulatedWidth = 0;
+    let visibleCount = 0;
+
+    // 临时将所有菜单设为可见以测量宽度
+    if (isFirstCalculation.value) {
+      visibleNumber.value = topMenus.value.length;
+      nextTick(() => {
+        calculateActualWidths();
+      });
+      return;
+    }
+
+    calculateActualWidths();
+
+    function calculateActualWidths() {
+      const menuItems = menuElement.querySelectorAll('.el-menu-item:not(.is-disabled)');
+      // "更多"按钮的大概宽度(包含margin和padding)
+      const moreButtonWidth = 100;
+      
+      for (let i = 0; i < menuItems.length && i < topMenus.value.length; i++) {
+        const item = menuItems[i] as HTMLElement;
+        // 获取包含 margin 的实际宽度
+        const itemWidth = item.offsetWidth + 20; // 20px 是左右 margin (0 10px)
+        
+        // 如果不是最后一项,且剩余空间不足以容纳当前项+更多按钮,则停止
+        if (i < topMenus.value.length - 1) {
+          if (accumulatedWidth + itemWidth + moreButtonWidth <= availableWidth) {
+            accumulatedWidth += itemWidth;
+            visibleCount++;
+          } else {
+            break;
+          }
+        } else {
+          // 最后一项,直接判断是否有空间
+          if (accumulatedWidth + itemWidth <= availableWidth) {
+            accumulatedWidth += itemWidth;
+            visibleCount++;
+          }
+        }
+      }
+
+      // 如果所有菜单都能显示,则不需要"更多"按钮
+      if (visibleCount >= topMenus.value.length) {
+        visibleNumber.value = topMenus.value.length;
+      } else {
+        // 确保至少显示一个菜单项
+        visibleNumber.value = Math.max(1, visibleCount);
+      }
+
+      if (DEBUG_MODE) {
+        console.log('[TopNav] 计算结果:', {
+          totalMenus: topMenus.value.length,
+          visibleCount,
+          visibleNumber: visibleNumber.value,
+          accumulatedWidth,
+          availableWidth
+        });
+      }
+
+      isFirstCalculation.value = false;
+    }
+  });
 };
 
 const handleSelect = (key: string) => {
@@ -148,15 +294,82 @@ const activeRoutes = (key: string) => {
   return routes;
 };
 
+// 监听窗口大小变化
 onMounted(() => {
-  window.addEventListener('resize', setVisibleNumber);
-});
-onBeforeUnmount(() => {
-  window.removeEventListener('resize', setVisibleNumber);
+  // 使用防抖优化性能
+  let resizeTimer: any = null;
+  const handleResize = () => {
+    if (resizeTimer) {
+      clearTimeout(resizeTimer);
+    }
+    resizeTimer = setTimeout(() => {
+      isFirstCalculation.value = true;
+      calculateVisibleMenus();
+    }, 200);
+  };
+  
+  window.addEventListener('resize', handleResize);
+  
+  onBeforeUnmount(() => {
+    window.removeEventListener('resize', handleResize);
+    if (resizeTimer) {
+      clearTimeout(resizeTimer);
+    }
+  });
 });
 
+// 初始化计算
 onMounted(() => {
-  setVisibleNumber();
+  calculateVisibleMenus();
+});
+
+// 监听菜单变化和语言切换,重新计算
+watch([topMenus, locale], () => {
+  isFirstCalculation.value = true;
+  calculateVisibleMenus();
+}, { deep: true });
+
+// 监听侧边栏展开/收起状态变化,重新计算菜单宽度
+watch(sidebarOpened, () => {
+  // 使用 setTimeout 等待侧边栏动画完成(transition 时间约 0.28s)
+  setTimeout(() => {
+    isFirstCalculation.value = true;
+    calculateVisibleMenus();
+  }, 300);
+});
+
+// 监听侧边栏隐藏状态变化
+watch(sidebarHide, () => {
+  setTimeout(() => {
+    isFirstCalculation.value = true;
+    calculateVisibleMenus();
+  }, 300);
+});
+
+// 监听顶部导航模式切换
+watch(topNav, (newVal) => {
+  if (newVal) {
+    // 切换到顶部导航模式时,延迟计算确保 DOM 已更新
+    nextTick(() => {
+      isFirstCalculation.value = true;
+      calculateVisibleMenus();
+    });
+  }
+});
+
+// 监听路由变化,因为切换菜单可能影响布局
+watch(() => route.path, () => {
+  // 路由变化时使用防抖计算
+  nextTick(() => {
+    debouncedCalculateVisibleMenus(150);
+  });
+});
+
+// 清理定时器
+onBeforeUnmount(() => {
+  if (calculateTimer) {
+    clearTimeout(calculateTimer);
+  }
 });
 </script>
 

+ 2 - 2
src/lang/en_US.ts

@@ -9,7 +9,7 @@ import project from './modules/project/index_en';
 export default {
   // 路由国际化
   route: {
-    dashboard: 'Dashboard',
+    dashboard: 'Home Page',
     document: 'Document'
   },
   // 登录页面国际化
@@ -105,5 +105,5 @@ export default {
   // 工具模块
   ...tool,
   // 项目管理模块
-  ...project
+  project
 };

+ 72 - 13
src/lang/modules/project/management/en_US.ts

@@ -2,13 +2,13 @@
 export default {
   // Search Form
   search: {
-    code: 'Project Code',
+    code: 'Code',
     codePlaceholder: 'Please enter project code',
     name: 'Name',
     namePlaceholder: 'Please enter name',
-    language: 'Project Language',
+    language: 'Language',
     languagePlaceholder: 'Please select project language',
-    type: 'Project Type',
+    type: 'Type',
     typePlaceholder: 'Please select project type',
     pdGpd: 'PD/GPD',
     pdGpdPlaceholder: 'Please enter PD/GPD',
@@ -22,10 +22,18 @@ export default {
     croPlaceholder: 'Please enter CRO',
     note: 'Note',
     notePlaceholder: 'Please enter note',
+    createBy: 'Creator',
+    createByPlaceholder: 'Please enter creator',
+    startTime: 'Start Time',
+    startTimePlaceholder: 'Please select start time',
+    endTime: 'End Time',
+    endTimePlaceholder: 'Please select end time',
     createTime: 'Create Time',
     createTimePlaceholder: 'Please select create time',
     updateTime: 'Update Time',
     updateTimePlaceholder: 'Please select update time',
+    startDate: 'Start Date',
+    endDate: 'End Date',
     search: 'Search',
     reset: 'Reset'
   },
@@ -36,37 +44,52 @@ export default {
     delete: 'Delete',
     export: 'Export',
     submit: 'Submit',
-    cancel: 'Cancel'
+    cancel: 'Cancel',
+    updateStatus: 'Update Status'
   },
   // Table Columns
   table: {
     id: 'ID',
-    code: 'Project Code',
+    code: 'Code',
     name: 'Name',
     icon: 'Icon',
-    language: 'Project Language',
-    type: 'Project Type',
+    language: 'Language',
+    type: 'Type',
     status: 'Status',
+    startTime: 'Start Time',
+    endTime: 'End Time',
     pdGpd: 'PD/GPD',
     pmGpm: 'PM/GPM',
     ctaGcta: 'CTA/GCTA',
     sponsor: 'Sponsor',
     cro: 'CRO',
     note: 'Note',
+    createBy: 'Creator',
     createTime: 'Create Time',
     updateTime: 'Update Time',
     operation: 'Operation'
   },
+  // Project Status Enum
+  status: {
+    unstarted: 'Unstarted',
+    underway: 'Underway',
+    paused: 'Paused',
+    finished: 'Finished'
+  },
   // Form
   form: {
-    code: 'Project Code',
+    // Section Titles
+    sectionBasic: 'Basic Information',
+    sectionResponsible: 'Responsible Person',
+    sectionPartner: 'Partner Information',
+    code: 'Code',
     codePlaceholder: 'Please enter project code',
     name: 'Name',
     namePlaceholder: 'Please enter name',
     icon: 'Icon',
-    language: 'Project Language',
+    language: 'Language',
     languagePlaceholder: 'Please select project language',
-    type: 'Project Type',
+    type: 'Type',
     typePlaceholder: 'Please select project type',
     pdGpd: 'PD/GPD',
     pdGpdPlaceholder: 'Please enter PD/GPD',
@@ -79,7 +102,11 @@ export default {
     cro: 'CRO',
     croPlaceholder: 'Please enter CRO',
     note: 'Note',
-    notePlaceholder: 'Please enter note'
+    notePlaceholder: 'Please enter note',
+    startTime: 'Start Time',
+    startTimePlaceholder: 'Please select start time',
+    endTime: 'End Time',
+    endTimePlaceholder: 'Please select end time'
   },
   // Dialog Titles
   dialog: {
@@ -90,12 +117,16 @@ export default {
   message: {
     deleteConfirm: 'Are you sure you want to delete the project with code "{ids}"?',
     deleteSuccess: 'Delete successfully',
-    operationSuccess: 'Operation successful'
+    operationSuccess: 'Operation successful',
+    updateStatusSuccess: 'Status updated successfully',
+    selectStatus: 'Please select status'
   },
   // Tooltip
   tooltip: {
+    detail: 'View Detail',
     edit: 'Edit',
-    delete: 'Delete'
+    delete: 'Delete',
+    updateStatus: 'Update Status'
   },
   // Validation Rules
   rule: {
@@ -106,5 +137,33 @@ export default {
     languageRequired: 'Project language cannot be empty',
     typeRequired: 'Project type cannot be empty',
     statusRequired: 'Status cannot be empty'
+  },
+  // Detail Page Menu
+  detail: {
+    header: {
+      backToList: 'Back to List',
+      projectDetail: 'Project Detail',
+      edit: 'Edit',
+      delete: 'Delete'
+    },
+    menu: {
+      basicInfo: 'Basic Info',
+      centerInfo: 'Center Info',
+      memberInfo: 'Member Info',
+      projectMember: 'Project Member',
+      centerMember: 'Center Member'
+    },
+    content: {
+      projectId: 'Project ID',
+      currentMenu: 'Current Menu',
+      timeInfo: 'Time Information',
+      projectNotStarted: 'Not Started',
+      projectEnded: 'Completed',
+      projectProgress: 'Progress',
+      basicInfoTip: 'Project basic information will be displayed here...',
+      centerInfoTip: 'Center information will be displayed here...',
+      projectMemberTip: 'Project member list will be displayed here...',
+      centerMemberTip: 'Center member list will be displayed here...'
+    }
   }
 };

+ 63 - 4
src/lang/modules/project/management/zh_CN.ts

@@ -22,10 +22,18 @@ export default {
     croPlaceholder: '请输入CRO',
     note: '备注',
     notePlaceholder: '请输入备注',
+    createBy: '创建者',
+    createByPlaceholder: '请输入创建者',
+    startTime: '开始时间',
+    startTimePlaceholder: '请选择开始时间',
+    endTime: '结束时间',
+    endTimePlaceholder: '请选择结束时间',
     createTime: '创建时间',
     createTimePlaceholder: '请选择创建时间',
     updateTime: '更新时间',
     updateTimePlaceholder: '请选择更新时间',
+    startDate: '开始日期',
+    endDate: '结束日期',
     search: '搜索',
     reset: '重置'
   },
@@ -36,7 +44,8 @@ export default {
     delete: '删除',
     export: '导出',
     submit: '确 定',
-    cancel: '取 消'
+    cancel: '取 消',
+    updateStatus: '更新状态'
   },
   // 表格列
   table: {
@@ -47,18 +56,32 @@ export default {
     language: '项目语言',
     type: '项目类型',
     status: '状态',
+    startTime: '开始时间',
+    endTime: '结束时间',
     pdGpd: 'PD/GPD',
     pmGpm: 'PM/GPM',
     ctaGcta: 'CTA/GCTA',
     sponsor: '申办方',
     cro: 'CRO',
     note: '备注',
+    createBy: '创建者',
     createTime: '创建时间',
     updateTime: '更新时间',
     operation: '操作'
   },
+  // 项目状态枚举
+  status: {
+    unstarted: '未开始',
+    underway: '进行中',
+    paused: '暂停中',
+    finished: '已完成'
+  },
   // 表单
   form: {
+    // 分组标题
+    sectionBasic: '基本信息',
+    sectionResponsible: '负责人信息',
+    sectionPartner: '合作方信息',
     code: '项目编号',
     codePlaceholder: '请输入项目编号',
     name: '名称',
@@ -79,7 +102,11 @@ export default {
     cro: 'CRO',
     croPlaceholder: '请输入CRO',
     note: '备注',
-    notePlaceholder: '请输入备注'
+    notePlaceholder: '请输入内容',
+    startTime: '开始时间',
+    startTimePlaceholder: '请选择开始时间',
+    endTime: '结束时间',
+    endTimePlaceholder: '请选择结束时间'
   },
   // 对话框标题
   dialog: {
@@ -90,12 +117,16 @@ export default {
   message: {
     deleteConfirm: '是否确认删除项目编号为"{ids}"的数据项?',
     deleteSuccess: '删除成功',
-    operationSuccess: '操作成功'
+    operationSuccess: '操作成功',
+    updateStatusSuccess: '状态更新成功',
+    selectStatus: '请选择状态'
   },
   // Tooltip 提示
   tooltip: {
+    detail: '查看详情',
     edit: '修改',
-    delete: '删除'
+    delete: '删除',
+    updateStatus: '更新状态'
   },
   // 验证规则
   rule: {
@@ -106,5 +137,33 @@ export default {
     languageRequired: '项目语言不能为空',
     typeRequired: '项目类型不能为空',
     statusRequired: '状态不能为空'
+  },
+  // 详情页面菜单
+  detail: {
+    header: {
+      backToList: '返回列表',
+      projectDetail: '项目详情',
+      edit: '编辑',
+      delete: '删除'
+    },
+    menu: {
+      basicInfo: '项目基本信息',
+      centerInfo: '中心信息',
+      memberInfo: '成员信息',
+      projectMember: '项目成员',
+      centerMember: '中心成员'
+    },
+    content: {
+      projectId: '项目ID',
+      currentMenu: '当前菜单',
+      timeInfo: '时间信息',
+      projectNotStarted: '项目未开始',
+      projectEnded: '项目已结束',
+      projectProgress: '项目进度',
+      basicInfoTip: '这里将展示项目的基本信息...',
+      centerInfoTip: '这里将展示项目的中心信息...',
+      projectMemberTip: '这里将展示项目成员列表...',
+      centerMemberTip: '这里将展示中心成员列表...'
+    }
   }
 };

+ 1 - 1
src/lang/zh_CN.ts

@@ -105,5 +105,5 @@ export default {
   // 工具模块
   ...tool,
   // 项目管理模块
-  ...project
+  project
 };

+ 14 - 14
src/layout/components/Navbar.vue

@@ -26,11 +26,11 @@
         </el-select>
 
         <search-menu ref="searchMenuRef" />
-        <el-tooltip content="搜索" effect="dark" placement="bottom">
-          <div class="right-menu-item hover-effect" @click="openSearchMenu">
-            <svg-icon class-name="search-icon" icon-class="search" />
-          </div>
-        </el-tooltip>
+<!--        <el-tooltip content="搜索" effect="dark" placement="bottom">-->
+<!--          <div class="right-menu-item hover-effect" @click="openSearchMenu">-->
+<!--            <svg-icon class-name="search-icon" icon-class="search" />-->
+<!--          </div>-->
+<!--        </el-tooltip>-->
         <!-- 消息 -->
 <!--        <el-tooltip :content="proxy.$t('navbar.message')" effect="dark" placement="bottom">-->
 <!--          <div>-->
@@ -56,17 +56,17 @@
 <!--          <ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />-->
 <!--        </el-tooltip>-->
 
-        <el-tooltip :content="proxy.$t('navbar.full')" effect="dark" placement="bottom">
-          <screenfull id="screenfull" class="right-menu-item hover-effect" />
-        </el-tooltip>
+<!--        <el-tooltip :content="proxy.$t('navbar.full')" effect="dark" placement="bottom">-->
+<!--          <screenfull id="screenfull" class="right-menu-item hover-effect" />-->
+<!--        </el-tooltip>-->
 
         <el-tooltip :content="proxy.$t('navbar.language')" effect="dark" placement="bottom">
           <lang-select id="lang-select" class="right-menu-item hover-effect" />
         </el-tooltip>
 
-        <el-tooltip :content="proxy.$t('navbar.layoutSize')" effect="dark" placement="bottom">
-          <size-select id="size-select" class="right-menu-item hover-effect" />
-        </el-tooltip>
+<!--        <el-tooltip :content="proxy.$t('navbar.layoutSize')" effect="dark" placement="bottom">-->
+<!--          <size-select id="size-select" class="right-menu-item hover-effect" />-->
+<!--        </el-tooltip>-->
       </template>
       <div class="avatar-container">
         <el-dropdown class="right-menu-item hover-effect" trigger="click" @command="handleCommand">
@@ -81,9 +81,9 @@
               <router-link v-if="!dynamic" to="/user/profile">
                 <el-dropdown-item>{{ proxy.$t('navbar.personalCenter') }}</el-dropdown-item>
               </router-link>
-              <el-dropdown-item v-if="settingsStore.showSettings" command="setLayout">
-                <span>{{ proxy.$t('navbar.layoutSetting') }}</span>
-              </el-dropdown-item>
+<!--              <el-dropdown-item v-if="settingsStore.showSettings" command="setLayout">-->
+<!--                <span>{{ proxy.$t('navbar.layoutSetting') }}</span>-->
+<!--              </el-dropdown-item>-->
               <el-dropdown-item divided command="logout">
                 <span>{{ proxy.$t('navbar.logout') }}</span>
               </el-dropdown-item>

+ 1 - 1
src/router/index.ts

@@ -71,7 +71,7 @@ export const constantRoutes: RouteRecordRaw[] = [
         path: '/index',
         component: () => import('@/views/index.vue'),
         name: 'Index',
-        meta: { title: '{"zh_CN":"首页","en_US":"Dashboard"}', icon: 'dashboard', affix: true }
+        meta: { title: '{"zh_CN":"首页","en_US":"Home Page"}', icon: 'dashboard', affix: true }
       }
     ]
   },

+ 1 - 1
src/settings.ts

@@ -20,7 +20,7 @@ const setting: DefaultSettings = {
   /**
    * 是否显示顶部导航
    */
-  topNav: false,
+  topNav: true,
 
   /**
    * 是否显示 tagsView

+ 34 - 1
src/utils/dict.ts

@@ -1,5 +1,33 @@
 import { getDicts } from '@/api/system/dict/data';
 import { useDictStore } from '@/store/modules/dict';
+
+/**
+ * 将字典标签转换为国际化JSON格式
+ * @param dictLabel 主标签
+ * @param dictLabelZh 中文标签(可选)
+ * @param dictLabelEn 英文标签(可选)
+ * @returns 国际化JSON字符串或原标签
+ */
+const convertToI18nLabel = (dictLabel: string, dictLabelZh?: string, dictLabelEn?: string): string => {
+  // 如果已经是JSON格式,直接返回
+  if (dictLabel && (dictLabel.startsWith('{') || dictLabel.startsWith('{'))) {
+    return dictLabel;
+  }
+  
+  // 如果有中英文标签,构建JSON格式
+  if (dictLabelZh || dictLabelEn) {
+    const i18nObj: any = {};
+    if (dictLabelZh) i18nObj.zh_CN = dictLabelZh;
+    if (dictLabelEn) i18nObj.en_US = dictLabelEn;
+    // 如果没有提供中文,使用原标签作为中文
+    if (!dictLabelZh && dictLabel) i18nObj.zh_CN = dictLabel;
+    return JSON.stringify(i18nObj);
+  }
+  
+  // 否则返回原标签
+  return dictLabel;
+};
+
 /**
  * 获取字典数据
  */
@@ -16,7 +44,12 @@ export const useDict = (...args: string[]): { [key: string]: DictDataOption[] }
     } else {
       await getDicts(dictType).then((resp) => {
         res.value[dictType] = resp.data.map(
-          (p): DictDataOption => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass })
+          (p): DictDataOption => ({ 
+            label: convertToI18nLabel(p.dictLabel, (p as any).dictLabelZh, (p as any).dictLabelEn), 
+            value: p.dictValue, 
+            elTagType: p.listClass, 
+            elTagClass: p.cssClass 
+          })
         );
         useDictStore().setDict(dictType, res.value[dictType]);
       });

+ 7 - 0
src/views/document/folder/index.vue

@@ -0,0 +1,7 @@
+<template>
+  <div>
+  </div>
+</template>
+
+<script setup>
+</script>

+ 65 - 0
src/views/project/management/detail/components/header.vue

@@ -0,0 +1,65 @@
+<template>
+  <div class="detail-header">
+    <div class="header-left">
+      <el-button type="primary" plain icon="Back" @click="handleBack" size="small">{{ t('project.management.detail.header.backToList') }}</el-button>
+      <el-divider direction="vertical" />
+      <span class="project-title">{{ projectName || 'Loading...' }}</span>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { inject } from 'vue';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
+
+// Props
+defineProps<{
+  projectId?: string | number | null;
+  projectName?: string;
+}>();
+
+// Emit
+const emit = defineEmits<{
+  back: [];
+}>();
+
+// 接收切换组件的方法
+const switchComponent = inject<any>('switchComponent');
+const ListComponent = inject<any>('ListComponent');
+
+// 返回列表
+const handleBack = () => {
+  if (switchComponent && ListComponent) {
+    switchComponent(ListComponent);
+  }
+  emit('back');
+}
+</script>
+
+<style scoped>
+/* Header 样式 */
+.detail-header {
+  width: 100%;
+  height: 60px;
+  background-color: #ffffff;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  display: flex;
+  align-items: center;
+  padding: 0 24px;
+  z-index: 10;
+}
+
+.header-left {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.project-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+}
+</style>

+ 112 - 0
src/views/project/management/detail/components/sidebar.vue

@@ -0,0 +1,112 @@
+<template>
+  <div class="detail-sidebar">
+    <el-menu
+      :default-active="activeMenu"
+      class="sidebar-menu"
+      @select="handleMenuSelect"
+    >
+      <!-- 项目基本信息 -->
+      <el-menu-item 
+        index="basicInfo" 
+        v-hasPermi="['project:management:queryBasicInfo']"
+      >
+        <el-icon><Document /></el-icon>
+        <span>{{ t('project.management.detail.menu.basicInfo') }}</span>
+      </el-menu-item>
+
+      <!-- 中心信息 -->
+      <el-menu-item 
+        index="centerInfo" 
+        v-hasPermi="['project:management:queryCenterInfo']"
+      >
+        <el-icon><OfficeBuilding /></el-icon>
+        <span>{{ t('project.management.detail.menu.centerInfo') }}</span>
+      </el-menu-item>
+
+      <!-- 成员信息 - 带子菜单 -->
+      <el-sub-menu 
+        index="memberInfo" 
+        v-hasPermi="['project:management:queryMemberInfo']"
+      >
+        <template #title>
+          <el-icon><User /></el-icon>
+          <span>{{ t('project.management.detail.menu.memberInfo') }}</span>
+        </template>
+        
+        <!-- 项目成员 -->
+        <el-menu-item 
+          index="projectMember" 
+          v-hasPermi="['project:management:queryProjectMember']"
+        >
+          <el-icon><UserFilled /></el-icon>
+          <span>{{ t('project.management.detail.menu.projectMember') }}</span>
+        </el-menu-item>
+
+        <!-- 中心成员 -->
+        <el-menu-item 
+          index="centerMember" 
+          v-hasPermi="['project:management:queryCenterMember']"
+        >
+          <el-icon><Avatar /></el-icon>
+          <span>{{ t('project.management.detail.menu.centerMember') }}</span>
+        </el-menu-item>
+      </el-sub-menu>
+    </el-menu>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { Document, OfficeBuilding, User, UserFilled, Avatar } from '@element-plus/icons-vue';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
+
+// Props
+defineProps<{
+  activeMenu?: string;
+}>();
+
+// Emit
+const emit = defineEmits<{
+  menuSelect: [index: string];
+}>();
+
+// 菜单选择事件
+const handleMenuSelect = (index: string) => {
+  emit('menuSelect', index);
+}
+</script>
+
+<style scoped>
+/* Sidebar 样式 */
+.detail-sidebar {
+  width: 200px;
+  background-color: #ffffff;
+  border-radius: 4px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  overflow-y: auto;
+}
+
+.sidebar-menu {
+  border-right: none;
+}
+
+.sidebar-menu .el-menu-item {
+  height: 48px;
+  line-height: 48px;
+}
+
+/* 滚动条样式 */
+.detail-sidebar::-webkit-scrollbar {
+  width: 6px;
+}
+
+.detail-sidebar::-webkit-scrollbar-thumb {
+  background-color: rgba(144, 147, 153, 0.3);
+  border-radius: 3px;
+}
+
+.detail-sidebar::-webkit-scrollbar-thumb:hover {
+  background-color: rgba(144, 147, 153, 0.5);
+}
+</style>

+ 153 - 0
src/views/project/management/detail/index.vue

@@ -0,0 +1,153 @@
+<template>
+  <div class="project-detail-layout" v-loading="loading">
+    <!-- Header 顶部区域 -->
+    <DetailHeader :project-id="projectId" :project-name="projectData?.name" @back="handleBack" />
+
+    <!-- Main 主体区域 -->
+    <div class="detail-main">
+      <!-- Sidebar 侧边栏 -->
+      <DetailSidebar :active-menu="activeMenu" @menu-select="handleMenuSelect" />
+
+      <!-- Content 内容区域 -->
+      <div class="detail-content">
+        <el-card shadow="never" class="content-card">
+          <!-- 动态加载页面组件 -->
+          <component :is="currentPageComponent" />
+        </el-card>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, inject, computed, onMounted, provide } from 'vue';
+import { useI18n } from 'vue-i18n';
+import DetailHeader from './components/header.vue';
+import DetailSidebar from './components/sidebar.vue';
+
+// 导入页面组件
+import BasicInfoPage from './pages/basicInfo.vue';
+import CenterInfoPage from './pages/centerInfo.vue';
+import ProjectMemberPage from './pages/projectMember.vue';
+import CenterMemberPage from './pages/centerMember.vue';
+
+// 导入API
+import { getManagement } from '@/api/project/management';
+import { ManagementVO } from '@/api/project/management/types';
+
+const { t } = useI18n();
+
+// 接收从父组件传递的项目ID
+const projectId = inject<any>('projectId', ref(null));
+
+// 当前选中的菜单(默认显示基本信息)
+const activeMenu = ref('basicInfo');
+
+// 页面加载状态
+const loading = ref(false);
+
+// 项目详细数据
+const projectData = ref<ManagementVO>();
+
+// 页面组件映射
+const pageComponents: Record<string, any> = {
+  basicInfo: BasicInfoPage,
+  centerInfo: CenterInfoPage,
+  projectMember: ProjectMemberPage,
+  centerMember: CenterMemberPage
+};
+
+// 当前显示的页面组件
+const currentPageComponent = computed(() => {
+  return pageComponents[activeMenu.value] || BasicInfoPage;
+});
+
+// 返回列表
+const handleBack = () => {
+  // Header 组件内部已处理返回逻辑
+}
+
+// 菜单选择事件
+const handleMenuSelect = (index: string) => {
+  activeMenu.value = index;
+}
+
+// 获取项目详细信息
+const getProjectDetail = async () => {
+  if (!projectId.value) {
+    console.warn('项目ID不存在');
+    return;
+  }
+  
+  try {
+    loading.value = true;
+    const res = await getManagement(projectId.value);
+    projectData.value = res.data;
+    console.log('项目详情数据:', projectData.value);
+  } catch (error) {
+    console.error('获取项目详情失败:', error);
+  } finally {
+    loading.value = false;
+  }
+}
+
+// 通过 provide 将项目数据提供给子组件
+provide('projectData', projectData);
+provide('loading', loading);
+
+// 组件挂载时获取数据
+onMounted(() => {
+  getProjectDetail();
+});
+</script>
+
+<style scoped>
+.project-detail-layout {
+  display: flex;
+  flex-direction: column;
+  height: calc(100vh - 84px); /* 根据实际顶部导航栏高度调整 */
+  background-color: #f0f2f5;
+}
+
+/* Main 主体区域 */
+.detail-main {
+  display: flex;
+  flex: 1;
+  overflow: hidden;
+  margin-top: 16px;
+  padding: 0 16px;
+  gap: 16px;
+}
+
+/* Content 样式 */
+.detail-content {
+  flex: 1;
+  overflow-y: auto;
+  background-color: #ffffff;
+  border-radius: 4px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+
+.content-card {
+  height: 100%;
+}
+
+.content-card :deep(.el-card__body) {
+  height: 100%;
+  padding: 24px;
+}
+
+/* 滚动条样式 */
+.detail-content::-webkit-scrollbar {
+  width: 6px;
+}
+
+.detail-content::-webkit-scrollbar-thumb {
+  background-color: rgba(144, 147, 153, 0.3);
+  border-radius: 3px;
+}
+
+.detail-content::-webkit-scrollbar-thumb:hover {
+  background-color: rgba(144, 147, 153, 0.5);
+}
+</style>

+ 404 - 0
src/views/project/management/detail/pages/basicInfo.vue

@@ -0,0 +1,404 @@
+<template>
+  <div class="basic-info-page">
+    <div v-if="projectData" class="info-container">
+      <!-- 基本信息卡片 -->
+      <el-card shadow="hover" class="info-card">
+        <template #header>
+          <div class="card-header">
+            <span class="header-title">{{ t('project.management.form.sectionBasic') }}</span>
+          </div>
+        </template>
+        <el-row :gutter="24">
+          <el-col :span="8">
+            <div class="info-item">
+              <label class="info-label">{{ t('project.management.table.code') }}</label>
+              <div class="info-value">{{ projectData.code || '-' }}</div>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="info-item">
+              <label class="info-label">{{ t('project.management.table.name') }}</label>
+              <div class="info-value">{{ projectData.name || '-' }}</div>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="info-item">
+              <label class="info-label">{{ t('project.management.table.language') }}</label>
+              <div class="info-value">
+                <dict-tag :options="project_language" :value="projectData.language" />
+              </div>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="info-item">
+              <label class="info-label">{{ t('project.management.table.type') }}</label>
+              <div class="info-value">
+                <dict-tag :options="project_type" :value="projectData.type" />
+              </div>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="info-item">
+              <label class="info-label">{{ t('project.management.table.status') }}</label>
+              <div class="info-value">{{ projectData.status || '-' }}</div>
+            </div>
+          </el-col>
+        </el-row>
+      </el-card>
+
+      <!-- 负责人信息卡片 -->
+      <el-card shadow="hover" class="info-card">
+        <template #header>
+          <div class="card-header">
+            <span class="header-title">{{ t('project.management.form.sectionResponsible') }}</span>
+          </div>
+        </template>
+        <el-row :gutter="24">
+          <el-col :span="8">
+            <div class="info-item">
+              <label class="info-label">{{ t('project.management.table.pdGpd') }}</label>
+              <div class="info-value">{{ projectData.pdGpd || '-' }}</div>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="info-item">
+              <label class="info-label">{{ t('project.management.table.pmGpm') }}</label>
+              <div class="info-value">{{ projectData.pmGpm || '-' }}</div>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="info-item">
+              <label class="info-label">{{ t('project.management.table.ctaGcta') }}</label>
+              <div class="info-value">{{ projectData.ctaGcta || '-' }}</div>
+            </div>
+          </el-col>
+        </el-row>
+      </el-card>
+
+      <!-- 合作方信息卡片 -->
+      <el-card shadow="hover" class="info-card">
+        <template #header>
+          <div class="card-header">
+            <span class="header-title">{{ t('project.management.form.sectionPartner') }}</span>
+          </div>
+        </template>
+        <el-row :gutter="24">
+          <el-col :span="8">
+            <div class="info-item">
+              <label class="info-label">{{ t('project.management.table.sponsor') }}</label>
+              <div class="info-value">{{ projectData.sponsor || '-' }}</div>
+            </div>
+          </el-col>
+          <el-col :span="8">
+            <div class="info-item">
+              <label class="info-label">{{ t('project.management.table.cro') }}</label>
+              <div class="info-value">{{ projectData.cro || '-' }}</div>
+            </div>
+          </el-col>
+        </el-row>
+      </el-card>
+
+      <!-- 时间信息卡片 -->
+      <el-card shadow="hover" class="info-card">
+        <template #header>
+          <div class="card-header">
+            <span class="header-title">{{ t('project.management.detail.content.timeInfo') }}</span>
+          </div>
+        </template>
+        
+        <!-- 项目时间线进度条 -->
+        <div class="timeline-section">
+          <div class="timeline-header">
+            <div class="timeline-label">
+              <span class="label-text">{{ t('project.management.table.startTime') }}</span>
+              <span class="time-value">{{ parseTime(projectData.startTime, '{y}-{m}-{d}') || '-' }}</span>
+            </div>
+            <div class="timeline-label">
+              <span class="label-text">{{ t('project.management.table.endTime') }}</span>
+              <span class="time-value">{{ parseTime(projectData.endTime, '{y}-{m}-{d}') || '-' }}</span>
+            </div>
+          </div>
+          
+          <!-- 进度条 -->
+          <div class="progress-container">
+            <el-progress 
+              :percentage="projectProgress" 
+              :color="progressColor"
+              :stroke-width="12"
+              :show-text="false"
+            />
+          </div>
+        </div>
+
+        <!-- 创建和更新时间 -->
+        <el-divider style="margin: 20px 0" />
+        <el-row :gutter="24">
+          <el-col :span="12">
+            <div class="info-item">
+              <label class="info-label">{{ t('project.management.table.createTime') }}</label>
+              <div class="info-value text-secondary">{{ parseTime(projectData.createTime, '{y}-{m}-{d} {h}:{i}:{s}') || '-' }}</div>
+            </div>
+          </el-col>
+          <el-col :span="12">
+            <div class="info-item">
+              <label class="info-label">{{ t('project.management.table.updateTime') }}</label>
+              <div class="info-value text-secondary">{{ parseTime(projectData.updateTime, '{y}-{m}-{d} {h}:{i}:{s}') || '-' }}</div>
+            </div>
+          </el-col>
+        </el-row>
+      </el-card>
+
+      <!-- 备注信息 -->
+      <el-card shadow="hover" class="info-card" v-if="projectData.note">
+        <template #header>
+          <div class="card-header">
+            <span class="header-title">{{ t('project.management.table.note') }}</span>
+          </div>
+        </template>
+        <div class="note-content">{{ projectData.note }}</div>
+      </el-card>
+    </div>
+
+    <el-empty v-else description="暂无数据" class="empty-container" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { inject, computed } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { ManagementVO } from '@/api/project/management/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { t } = useI18n();
+const { project_type, project_language } = toRefs<any>(proxy?.useDict('project_type', 'project_language'));
+
+// 接收从父组件传递的项目数据
+const projectData = inject<any>('projectData');
+
+// 计算项目进度
+const projectProgress = computed(() => {
+  if (!projectData?.value?.startTime || !projectData?.value?.endTime) {
+    return 0;
+  }
+  
+  const start = new Date(projectData.value.startTime).getTime();
+  const end = new Date(projectData.value.endTime).getTime();
+  const now = Date.now();
+  
+  if (now < start) {
+    return 0; // 项目未开始
+  }
+  
+  if (now > end) {
+    return 100; // 项目已结束
+  }
+  
+  const total = end - start;
+  const elapsed = now - start;
+  const progress = (elapsed / total) * 100;
+  
+  return Math.min(Math.max(Math.round(progress), 0), 100);
+});
+
+// 进度条颜色
+const progressColor = computed(() => {
+  const progress = projectProgress.value;
+  if (progress >= 100) {
+    return '#909399'; // 灰色 - 已完成
+  } else if (progress >= 75) {
+    return '#F56C6C'; // 红色 - 即将结束
+  } else if (progress >= 50) {
+    return '#E6A23C'; // 橙色 - 进行中后期
+  } else if (progress >= 25) {
+    return '#409EFF'; // 蓝色 - 进行中
+  } else {
+    return '#67C23A'; // 绿色 - 刚开始
+  }
+});
+
+</script>
+
+<style scoped>
+.basic-info-page {
+  height: 100%;
+  overflow-y: auto;
+}
+
+.info-container {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+/* 卡片样式 */
+.info-card {
+  border-radius: 8px;
+  transition: all 0.3s ease;
+}
+
+.info-card:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
+}
+
+.info-card :deep(.el-card__header) {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  padding: 16px 20px;
+  border-bottom: none;
+}
+
+.card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.header-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #ffffff;
+  letter-spacing: 0.5px;
+}
+
+.info-card :deep(.el-card__body) {
+  padding: 24px;
+}
+
+/* 信息项样式 */
+.info-item {
+  margin-bottom: 20px;
+  padding-bottom: 16px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.info-item:last-child {
+  margin-bottom: 0;
+  padding-bottom: 0;
+  border-bottom: none;
+}
+
+.info-label {
+  display: block;
+  font-size: 13px;
+  color: #909399;
+  margin-bottom: 8px;
+  font-weight: 500;
+}
+
+.info-value {
+  font-size: 15px;
+  color: #303133;
+  font-weight: 500;
+  word-break: break-all;
+}
+
+.text-secondary {
+  color: #909399;
+  font-size: 14px;
+  font-weight: normal;
+}
+
+/* 备注内容 */
+.note-content {
+  padding: 16px;
+  background-color: #f9fafb;
+  border-radius: 6px;
+  border-left: 4px solid #667eea;
+  line-height: 1.8;
+  color: #606266;
+  font-size: 14px;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+
+/* 空状态 */
+.empty-container {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 400px;
+}
+
+/* 滚动条美化 */
+.basic-info-page::-webkit-scrollbar {
+  width: 6px;
+}
+
+.basic-info-page::-webkit-scrollbar-thumb {
+  background-color: rgba(144, 147, 153, 0.3);
+  border-radius: 3px;
+}
+
+.basic-info-page::-webkit-scrollbar-thumb:hover {
+  background-color: rgba(144, 147, 153, 0.5);
+}
+
+/* 时间线进度条样式 */
+.timeline-section {
+  margin-bottom: 10px;
+}
+
+.timeline-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  margin-bottom: 16px;
+}
+
+.timeline-label {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.label-text {
+  font-size: 13px;
+  color: #909399;
+  font-weight: 500;
+}
+
+.time-value {
+  font-size: 16px;
+  color: #303133;
+  font-weight: 600;
+}
+
+.progress-container {
+  position: relative;
+}
+
+.progress-container :deep(.el-progress__text) {
+  display: none;
+}
+
+.progress-container :deep(.el-progress-bar__outer) {
+  border-radius: 6px;
+  background-color: #f0f0f0;
+}
+
+.progress-container :deep(.el-progress-bar__inner) {
+  border-radius: 6px;
+  transition: all 0.6s ease;
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+  .info-card :deep(.el-card__body) {
+    padding: 16px;
+  }
+  
+  .info-item {
+    margin-bottom: 16px;
+    padding-bottom: 12px;
+  }
+  
+  .timeline-header {
+    flex-direction: column;
+    gap: 12px;
+  }
+  
+  .time-value {
+    font-size: 14px;
+  }
+}
+</style>

+ 55 - 0
src/views/project/management/detail/pages/centerInfo.vue

@@ -0,0 +1,55 @@
+<template>
+  <div class="center-info-page">
+    <h3>{{ t('project.management.detail.menu.centerInfo') }}</h3>
+    <el-divider />
+    <div class="info-section">
+      <p><strong>{{ t('project.management.detail.content.projectId') }}:</strong> {{ projectId }}</p>
+      <p class="info-tip">{{ t('project.management.detail.content.centerInfoTip') }}</p>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { inject } from 'vue';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
+
+// 接收从父组件传递的项目ID
+const projectId = inject<any>('projectId');
+</script>
+
+<style scoped>
+.center-info-page {
+  height: 100%;
+}
+
+.info-section {
+  padding: 16px 0;
+}
+
+.info-section p {
+  line-height: 2;
+  font-size: 14px;
+  color: #606266;
+  margin: 8px 0;
+}
+
+.info-section strong {
+  color: #303133;
+  margin-right: 8px;
+}
+
+h3 {
+  font-size: 18px;
+  font-weight: 600;
+  color: #303133;
+  margin: 0;
+}
+
+.info-tip {
+  color: #909399;
+  font-style: italic;
+  margin-top: 16px;
+}
+</style>

+ 55 - 0
src/views/project/management/detail/pages/centerMember.vue

@@ -0,0 +1,55 @@
+<template>
+  <div class="center-member-page">
+    <h3>{{ t('project.management.detail.menu.centerMember') }}</h3>
+    <el-divider />
+    <div class="info-section">
+      <p><strong>{{ t('project.management.detail.content.projectId') }}:</strong> {{ projectId }}</p>
+      <p class="info-tip">{{ t('project.management.detail.content.centerMemberTip') }}</p>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { inject } from 'vue';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
+
+// 接收从父组件传递的项目ID
+const projectId = inject<any>('projectId');
+</script>
+
+<style scoped>
+.center-member-page {
+  height: 100%;
+}
+
+.info-section {
+  padding: 16px 0;
+}
+
+.info-section p {
+  line-height: 2;
+  font-size: 14px;
+  color: #606266;
+  margin: 8px 0;
+}
+
+.info-section strong {
+  color: #303133;
+  margin-right: 8px;
+}
+
+h3 {
+  font-size: 18px;
+  font-weight: 600;
+  color: #303133;
+  margin: 0;
+}
+
+.info-tip {
+  color: #909399;
+  font-style: italic;
+  margin-top: 16px;
+}
+</style>

+ 109 - 0
src/views/project/management/detail/pages/projectMember.vue

@@ -0,0 +1,109 @@
+<template>
+  <div class="project-member-page">
+    <h3>{{ t('project.management.detail.menu.projectMember') }}</h3>
+    <el-divider />
+
+    <el-card shadow="never">
+      <el-table 
+        v-loading="loading" 
+        border 
+        :data="memberList"
+        style="width: 100%"
+      >
+        <el-table-column type="index" label="序号" width="60" align="center" />
+        <el-table-column label="姓名" align="center" prop="name" />
+        <el-table-column label="手机号" align="center" prop="phoneNumber" />
+        <el-table-column label="部门" align="center" prop="dept" />
+        <el-table-column label="角色" align="center" prop="role" />
+        <el-table-column label="时间" align="center" prop="time" width="180" />
+      </el-table>
+
+      <pagination 
+        v-show="total > 0" 
+        :total="total" 
+        v-model:page="queryParams.pageNum" 
+        v-model:limit="queryParams.pageSize" 
+        @pagination="getList" 
+      />
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { inject, ref, onMounted } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { queryProjectMember } from '@/api/project/management';
+import { ProjectMemberVO, ProjectMemberQuery } from '@/api/project/management/types';
+
+const { t } = useI18n();
+
+// 接收从父组件传递的项目ID
+const projectId = inject<any>('projectId');
+
+// 列表数据
+const memberList = ref<ProjectMemberVO[]>([]);
+const loading = ref(true);
+const total = ref(0);
+
+// 查询参数
+const queryParams = ref<ProjectMemberQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  id: projectId?.value || 0
+});
+
+/** 查询项目成员列表 */
+const getList = async () => {
+  loading.value = true;
+  try {
+    queryParams.value.id = projectId?.value || 0;
+    const res = await queryProjectMember(queryParams.value);
+    memberList.value = res.rows;
+    total.value = res.total;
+  } catch (error) {
+    console.error('Failed to fetch project members:', error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 组件挂载时加载数据
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style scoped>
+.project-member-page {
+  height: 100%;
+}
+
+.info-section {
+  padding: 16px 0;
+}
+
+.info-section p {
+  line-height: 2;
+  font-size: 14px;
+  color: #606266;
+  margin: 8px 0;
+}
+
+.info-section strong {
+  color: #303133;
+  margin-right: 8px;
+}
+
+h3 {
+  font-size: 18px;
+  font-weight: 600;
+  color: #303133;
+  margin: 0;
+}
+
+.info-tip {
+  color: #909399;
+  font-style: italic;
+  margin-top: 16px;
+}
+</style>

+ 37 - 0
src/views/project/management/index.vue

@@ -0,0 +1,37 @@
+<template>
+  <div class="p-2">
+    <!-- 组件容器:默认显示列表页面,可扩展其他页面如详情、统计等 -->
+    <component :is="currentComponent" />
+  </div>
+</template>
+
+<script setup name="Management" lang="ts">
+import { shallowRef, ref, provide } from 'vue';
+import ListComponent from './list.vue';
+import DetailComponent from './detail/index.vue';
+
+// 当前显示的组件,默认为列表页
+const currentComponent = shallowRef(ListComponent);
+
+// 当前选中的项目ID(用于传递给详情页)
+const projectId = ref<string | number | null>(null);
+
+// 切换组件的方法
+const switchComponent = (component: any, params?: any) => {
+  if (params?.projectId) {
+    projectId.value = params.projectId;
+  }
+  currentComponent.value = component;
+}
+
+// 通过 provide 向子组件提供数据和方法
+provide('switchComponent', switchComponent);
+provide('projectId', projectId);
+provide('ListComponent', ListComponent);
+provide('DetailComponent', DetailComponent);
+
+// 暴露给父组件或其他地方使用的方法(如果需要)
+defineExpose({
+  switchComponent
+});
+</script>

+ 631 - 0
src/views/project/management/list.vue

@@ -0,0 +1,631 @@
+<template>
+  <div>
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="100px">
+            <el-form-item :label="t('project.management.search.code')" prop="code">
+              <el-input v-model="queryParams.code" :placeholder="t('project.management.search.codePlaceholder')" clearable @keyup.enter="handleQuery" style="width: 240px;" />
+            </el-form-item>
+            <el-form-item :label="t('project.management.search.name')" prop="name">
+              <el-input v-model="queryParams.name" :placeholder="t('project.management.search.namePlaceholder')" clearable @keyup.enter="handleQuery" style="width: 240px;" />
+            </el-form-item>
+            <el-form-item :label="t('project.management.search.language')" prop="language">
+              <el-select v-model="queryParams.language" :placeholder="t('project.management.search.languagePlaceholder')" clearable style="width: 240px;">
+                <el-option v-for="dict in project_language" :key="dict.value" :label="parseI18nName(dict.label)" :value="dict.value"/>
+              </el-select>
+            </el-form-item>
+            <el-form-item :label="t('project.management.search.type')" prop="type">
+              <el-select v-model="queryParams.type" :placeholder="t('project.management.search.typePlaceholder')" clearable style="width: 240px;">
+                <el-option v-for="dict in project_type" :key="dict.value" :label="parseI18nName(dict.label)" :value="dict.value"/>
+              </el-select>
+            </el-form-item>
+            <el-form-item :label="t('project.management.search.pdGpd')" prop="pdGpd">
+              <el-input v-model="queryParams.pdGpd" :placeholder="t('project.management.search.pdGpdPlaceholder')" clearable @keyup.enter="handleQuery" style="width: 240px;" />
+            </el-form-item>
+            <el-form-item :label="t('project.management.search.pmGpm')" prop="pmGpm">
+              <el-input v-model="queryParams.pmGpm" :placeholder="t('project.management.search.pmGpmPlaceholder')" clearable @keyup.enter="handleQuery" style="width: 240px;" />
+            </el-form-item>
+            <el-form-item :label="t('project.management.search.ctaGcta')" prop="ctaGcta">
+              <el-input v-model="queryParams.ctaGcta" :placeholder="t('project.management.search.ctaGctaPlaceholder')" clearable @keyup.enter="handleQuery" style="width: 240px;" />
+            </el-form-item>
+            <el-form-item :label="t('project.management.search.sponsor')" prop="sponsor">
+              <el-input v-model="queryParams.sponsor" :placeholder="t('project.management.search.sponsorPlaceholder')" clearable @keyup.enter="handleQuery" style="width: 240px;" />
+            </el-form-item>
+            <el-form-item :label="t('project.management.search.cro')" prop="cro">
+              <el-input v-model="queryParams.cro" :placeholder="t('project.management.search.croPlaceholder')" clearable @keyup.enter="handleQuery" style="width: 240px;" />
+            </el-form-item>
+            <el-form-item :label="t('project.management.search.startTime')">
+              <el-date-picker
+                v-model="dateRangeStartTime"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                type="daterange"
+                range-separator="-"
+                :start-placeholder="t('project.management.search.startDate')"
+                :end-placeholder="t('project.management.search.endDate')"
+                :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+                style="width: 240px;"
+              />
+            </el-form-item>
+            <el-form-item :label="t('project.management.search.endTime')">
+              <el-date-picker
+                v-model="dateRangeEndTime"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                type="daterange"
+                range-separator="-"
+                :start-placeholder="t('project.management.search.startDate')"
+                :end-placeholder="t('project.management.search.endDate')"
+                :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+                style="width: 240px;"
+              />
+            </el-form-item>
+            <el-form-item :label="t('project.management.search.createTime')">
+              <el-date-picker
+                v-model="dateRangeCreateTime"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                type="daterange"
+                range-separator="-"
+                :start-placeholder="t('project.management.search.startDate')"
+                :end-placeholder="t('project.management.search.endDate')"
+                :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+                style="width: 240px;"
+              />
+            </el-form-item>
+            <el-form-item :label="t('project.management.search.updateTime')">
+              <el-date-picker
+                v-model="dateRangeUpdateTime"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                type="daterange"
+                range-separator="-"
+                :start-placeholder="t('project.management.search.startDate')"
+                :end-placeholder="t('project.management.search.endDate')"
+                :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+                style="width: 240px;"
+              />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">{{ t('project.management.search.search') }}</el-button>
+              <el-button icon="Refresh" @click="resetQuery">{{ t('project.management.search.reset') }}</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="never">
+      <template #header>
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['project:management:add']">{{ t('project.management.button.add') }}</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['project:management:remove']">{{ t('project.management.button.delete') }}</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['project:management:export']">{{ t('project.management.button.export') }}</el-button>
+          </el-col>
+          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="managementList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column :label="t('project.management.table.id')" align="center" prop="id" v-if="true" />
+        <el-table-column :label="t('project.management.table.code')" align="center" prop="code" width="150" />
+        <el-table-column :label="t('project.management.table.name')" align="center" prop="name" width="200" />
+        <el-table-column :label="t('project.management.table.language')" align="center" prop="language" width="90">
+          <template #default="scope">
+            <dict-tag :options="project_language" :value="scope.row.language" />
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('project.management.table.type')" align="center" prop="type" width="140">
+          <template #default="scope">
+            <dict-tag :options="project_type" :value="scope.row.type" />
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('project.management.table.status')" align="center" prop="status" width="100">
+          <template #default="scope">
+            <el-tag :type="getStatusType(scope.row.status)">{{ getStatusText(scope.row.status) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('project.management.table.pdGpd')" align="center" prop="pdGpd" width="150" />
+        <el-table-column :label="t('project.management.table.pmGpm')" align="center" prop="pmGpm" width="150" />
+        <el-table-column :label="t('project.management.table.ctaGcta')" align="center" prop="ctaGcta" width="150" />
+        <el-table-column :label="t('project.management.table.sponsor')" align="center" prop="sponsor" width="150" />
+        <el-table-column :label="t('project.management.table.cro')" align="center" prop="cro" width="150" />
+        <el-table-column :label="t('project.management.table.startTime')" align="center" prop="startTime" width="100">
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d}') }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('project.management.table.endTime')" align="center" prop="endTime" width="100">
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d}') }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('project.management.table.createTime')" align="center" prop="createTime" width="180">
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('project.management.table.updateTime')" align="center" prop="updateTime" width="180">
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.updateTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('project.management.table.operation')" align="center" fixed="right"  class-name="small-padding fixed-width" width="320">
+          <template #default="scope">
+            <el-tooltip :content="t('project.management.tooltip.detail')" placement="top">
+              <el-button link type="primary" icon="View" @click="handleViewDetail(scope.row)" v-hasPermi="['project:management:query']"></el-button>
+            </el-tooltip>
+            <el-tooltip :content="t('project.management.tooltip.edit')" placement="top">
+              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['project:management:edit']"></el-button>
+            </el-tooltip>
+            <el-tooltip :content="t('project.management.tooltip.delete')" placement="top">
+              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['project:management:remove']"></el-button>
+            </el-tooltip>
+            <el-tooltip :content="t('project.management.tooltip.updateStatus')" placement="top">
+              <el-button link type="primary" icon="Refresh" @click="handleUpdateStatus(scope.row)" v-hasPermi="['project:management:updateStatus']"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+
+    <!-- 更新状态对话框 -->
+    <el-dialog :title="t('project.management.tooltip.updateStatus')" v-model="statusDialog.visible" width="500px" append-to-body>
+      <el-form ref="statusFormRef" :model="statusForm" :rules="statusRules" label-width="100px">
+        <el-form-item :label="t('project.management.table.code')" prop="code">
+          <el-input v-model="statusForm.code" disabled />
+        </el-form-item>
+        <el-form-item :label="t('project.management.table.name')" prop="name">
+          <el-input v-model="statusForm.name" disabled />
+        </el-form-item>
+        <el-form-item :label="t('project.management.table.status')" prop="status">
+          <el-select v-model="statusForm.status" :placeholder="t('project.management.message.selectStatus')" style="width: 100%;">
+            <el-option :label="t('project.management.status.unstarted')" :value="0" />
+            <el-option :label="t('project.management.status.underway')" :value="1" />
+            <el-option :label="t('project.management.status.paused')" :value="2" />
+            <el-option :label="t('project.management.status.finished')" :value="3" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="statusButtonLoading" type="primary" @click="submitStatusForm">{{ t('project.management.button.submit') }}</el-button>
+          <el-button @click="statusDialog.visible = false">{{ t('project.management.button.cancel') }}</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 添加或修改项目管理对话框 -->
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="900px" append-to-body>
+      <el-form ref="managementFormRef" :model="form" :rules="rules" label-width="100px">
+        <!-- 图标 - 居中显示在最顶部 -->
+        <el-row :gutter="20" justify="center">
+          <el-col :span="24" style="text-align: center; margin-bottom: 20px;">
+            <image-upload v-model="form.icon" :limit="1" style="display: inline-block;"/>
+          </el-col>
+        </el-row>
+
+        <el-divider content-position="left">{{ t('project.management.form.sectionBasic') }}</el-divider>
+
+        <!-- 第二行:项目编号和名称 -->
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item :label="t('project.management.form.code')" prop="code">
+              <el-input v-model="form.code" :placeholder="t('project.management.form.codePlaceholder')" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item :label="t('project.management.form.name')" prop="name">
+              <el-input v-model="form.name" :placeholder="t('project.management.form.namePlaceholder')" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <!-- 第三行:语言和类型 -->
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item :label="t('project.management.form.language')" prop="language">
+              <el-select v-model="form.language" :placeholder="t('project.management.form.languagePlaceholder')" style="width: 100%;">
+                <el-option
+                    v-for="dict in project_language"
+                    :key="dict.value"
+                    :label="parseI18nName(dict.label)"
+                    :value="dict.value"
+                ></el-option>
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item :label="t('project.management.form.type')" prop="type">
+              <el-select v-model="form.type" :placeholder="t('project.management.form.typePlaceholder')" style="width: 100%;">
+                <el-option
+                    v-for="dict in project_type"
+                    :key="dict.value"
+                    :label="parseI18nName(dict.label)"
+                    :value="dict.value"
+                ></el-option>
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-divider content-position="left">{{ t('project.management.form.sectionResponsible') }}</el-divider>
+
+        <!-- 第四行:PD/GPD、PM/GPM -->
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item :label="t('project.management.form.pdGpd')" prop="pdGpd">
+              <el-input v-model="form.pdGpd" :placeholder="t('project.management.form.pdGpdPlaceholder')" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item :label="t('project.management.form.pmGpm')" prop="pmGpm">
+              <el-input v-model="form.pmGpm" :placeholder="t('project.management.form.pmGpmPlaceholder')" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <!-- 第五行:CTA/GCTA -->
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item :label="t('project.management.form.ctaGcta')" prop="ctaGcta">
+              <el-input v-model="form.ctaGcta" :placeholder="t('project.management.form.ctaGctaPlaceholder')" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-divider content-position="left">{{ t('project.management.form.sectionPartner') }}</el-divider>
+
+        <!-- 第六行:Sponsor、CRO -->
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item :label="t('project.management.form.sponsor')" prop="sponsor">
+              <el-input v-model="form.sponsor" :placeholder="t('project.management.form.sponsorPlaceholder')" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item :label="t('project.management.form.cro')" prop="cro">
+              <el-input v-model="form.cro" :placeholder="t('project.management.form.croPlaceholder')" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-divider></el-divider>
+
+        <!-- 第七行:开始时间、结束时间 -->
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item :label="t('project.management.form.startTime')" prop="startTime">
+              <el-date-picker clearable
+                v-model="form.startTime"
+                type="date"
+                value-format="YYYY-MM-DD"
+                :placeholder="t('project.management.form.startTimePlaceholder')"
+                style="width: 100%;">
+              </el-date-picker>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item :label="t('project.management.form.endTime')" prop="endTime">
+              <el-date-picker clearable
+                v-model="form.endTime"
+                type="date"
+                value-format="YYYY-MM-DD"
+                :placeholder="t('project.management.form.endTimePlaceholder')"
+                style="width: 100%;">
+              </el-date-picker>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <!-- 第八行:备注 -->
+        <el-row :gutter="20">
+          <el-col :span="24">
+            <el-form-item :label="t('project.management.form.note')" prop="note">
+              <el-input v-model="form.note" type="textarea" :rows="3" :placeholder="t('project.management.form.notePlaceholder')" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">{{ t('project.management.button.submit') }}</el-button>
+          <el-button @click="cancel">{{ t('project.management.button.cancel') }}</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { inject } from 'vue';
+import { listManagement, getManagement, delManagement, addManagement, updateManagement, updateStatus } from '@/api/project/management';
+import { ManagementVO, ManagementQuery, ManagementForm } from '@/api/project/management/types';
+import { useI18n } from 'vue-i18n';
+import { parseI18nName } from '@/utils/i18n';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { t } = useI18n();
+const { project_type, project_language } = toRefs<any>(proxy?.useDict('project_type', 'project_language'));
+
+// 项目状态枚举映射
+const projectStatusMap: Record<number, string> = {
+  0: 'unstarted',
+  1: 'underway',
+  2: 'paused',
+  3: 'finished'
+};
+
+// 获取状态文本(国际化)
+const getStatusText = (status: number | undefined) => {
+  if (status === undefined || status === null) return '-';
+  const statusKey = projectStatusMap[status];
+  return statusKey ? t(`project.management.status.${statusKey}`) : status;
+};
+
+// 获取状态标签类型
+const getStatusType = (status: number | undefined): 'success' | 'info' | 'warning' | 'primary' | 'danger' => {
+  const typeMap: Record<number, 'success' | 'info' | 'warning' | 'primary' | 'danger'> = {
+    0: 'info',
+    1: 'success',
+    2: 'warning',
+    3: 'primary'
+  };
+  if (status === undefined || status === null) return 'info';
+  return typeMap[status] || 'info';
+};
+
+// 注入父组件提供的方法和组件
+const switchComponent = inject<any>('switchComponent');
+const DetailComponent = inject<any>('DetailComponent');
+
+const managementList = ref<ManagementVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const dateRangeStartTime = ref<[DateModelType, DateModelType]>(['', '']);
+const dateRangeEndTime = ref<[DateModelType, DateModelType]>(['', '']);
+const dateRangeCreateTime = ref<[DateModelType, DateModelType]>(['', '']);
+const dateRangeUpdateTime = ref<[DateModelType, DateModelType]>(['', '']);
+
+const queryFormRef = ref<ElFormInstance>();
+const managementFormRef = ref<ElFormInstance>();
+const statusFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+
+const statusDialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+
+const statusForm = ref<{
+  id?: string | number;
+  code?: string;
+  name?: string;
+  status?: number;
+}>({
+  id: undefined,
+  code: undefined,
+  name: undefined,
+  status: undefined
+});
+
+const statusButtonLoading = ref(false);
+
+const statusRules = {
+  status: [
+    { required: true, message: t('project.management.rule.statusRequired'), trigger: 'change' }
+  ]
+};
+
+const initFormData: ManagementForm = {
+  id: undefined,
+  code: undefined,
+  name: undefined,
+  icon: undefined,
+  language: undefined,
+  type: undefined,
+  status: undefined,
+  pdGpd: undefined,
+  pmGpm: undefined,
+  ctaGcta: undefined,
+  sponsor: undefined,
+  cro: undefined,
+  note: undefined,
+  startTime: undefined,
+  endTime: undefined,
+}
+const data = reactive<PageData<ManagementForm, ManagementQuery>>({
+  form: {...initFormData},
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    code: undefined,
+    name: undefined,
+    language: undefined,
+    type: undefined,
+    status: undefined,
+    pdGpd: undefined,
+    pmGpm: undefined,
+    ctaGcta: undefined,
+    sponsor: undefined,
+    cro: undefined,
+    params: {
+      startTime: undefined,
+      endTime: undefined,
+      createTime: undefined,
+      updateTime: undefined,
+    }
+  },
+  rules: {
+    id: [
+      { required: true, message: t('project.management.rule.idRequired'), trigger: "blur" }
+    ],
+    code: [
+      { required: true, message: t('project.management.rule.codeRequired'), trigger: "blur" }
+    ],
+    name: [
+      { required: true, message: t('project.management.rule.nameRequired'), trigger: "blur" }
+    ],
+    language: [
+      { required: true, message: t('project.management.rule.languageRequired'), trigger: "change" }
+    ],
+    type: [
+      { required: true, message: t('project.management.rule.typeRequired'), trigger: "change" }
+    ],
+  }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询项目管理列表 */
+const getList = async () => {
+  loading.value = true;
+  queryParams.value.params = {};
+  proxy?.addDateRange(queryParams.value, dateRangeStartTime.value, 'StartTime');
+  proxy?.addDateRange(queryParams.value, dateRangeEndTime.value, 'EndTime');
+  proxy?.addDateRange(queryParams.value, dateRangeCreateTime.value, 'CreateTime');
+  proxy?.addDateRange(queryParams.value, dateRangeUpdateTime.value, 'UpdateTime');
+  const res = await listManagement(queryParams.value);
+  managementList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+}
+
+/** 取消按钮 */
+const cancel = () => {
+  reset();
+  dialog.visible = false;
+}
+
+/** 表单重置 */
+const reset = () => {
+  form.value = {...initFormData};
+  managementFormRef.value?.resetFields();
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  dateRangeStartTime.value = ['', ''];
+  dateRangeEndTime.value = ['', ''];
+  dateRangeCreateTime.value = ['', ''];
+  dateRangeUpdateTime.value = ['', ''];
+  queryFormRef.value?.resetFields();
+  handleQuery();
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: ManagementVO[]) => {
+  ids.value = selection.map(item => item.id);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+}
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  reset();
+  dialog.visible = true;
+  dialog.title = t('project.management.dialog.add');
+}
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: ManagementVO) => {
+  reset();
+  const _id = row?.id || ids.value[0]
+  const res = await getManagement(_id);
+  Object.assign(form.value, res.data);
+  dialog.visible = true;
+  dialog.title = t('project.management.dialog.edit');
+}
+
+/** 提交按钮 */
+const submitForm = () => {
+  managementFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      buttonLoading.value = true;
+      if (form.value.id) {
+        await updateManagement(form.value).finally(() =>  buttonLoading.value = false);
+      } else {
+        await addManagement(form.value).finally(() =>  buttonLoading.value = false);
+      }
+      proxy?.$modal.msgSuccess(t('project.management.message.operationSuccess'));
+      dialog.visible = false;
+      await getList();
+    }
+  });
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: ManagementVO) => {
+  const _ids = row?.id || ids.value;
+  await proxy?.$modal.confirm(t('project.management.message.deleteConfirm', { ids: _ids })).finally(() => loading.value = false);
+  await delManagement(_ids);
+  proxy?.$modal.msgSuccess(t('project.management.message.deleteSuccess'));
+  await getList();
+}
+
+/** 导出按钮操作 */
+const handleExport = () => {
+  proxy?.download('project/management/export', {
+    ...queryParams.value
+  }, `management_${new Date().getTime()}.xlsx`)
+}
+
+/** 查看详情按钮操作 */
+const handleViewDetail = (row: ManagementVO) => {
+  if (switchComponent && DetailComponent) {
+    switchComponent(DetailComponent, { projectId: row.id });
+  }
+}
+
+/** 更新状态按钮操作 */
+const handleUpdateStatus = (row: ManagementVO) => {
+  statusForm.value = {
+    id: row.id,
+    code: row.code,
+    name: row.name,
+    status: row.status
+  };
+  statusDialog.visible = true;
+};
+
+/** 提交状态更新 */
+const submitStatusForm = () => {
+  statusFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      statusButtonLoading.value = true;
+      try {
+        await updateStatus(statusForm.value.id!, statusForm.value.status!);
+        proxy?.$modal.msgSuccess(t('project.management.message.updateStatusSuccess'));
+        statusDialog.visible = false;
+        await getList();
+      } finally {
+        statusButtonLoading.value = false;
+      }
+    }
+  });
+};
+
+onMounted(() => {
+  getList();
+});
+</script>

+ 105 - 1
src/views/system/user/index.vue

@@ -242,6 +242,36 @@
             </el-form-item>
           </el-col>
         </el-row>
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="项目">
+              <div style="width: 100%;">
+                <el-checkbox 
+                  v-model="isAllProjectsSelected" 
+                  @change="handleSelectAllProjects"
+                  style="margin-bottom: 10px;"
+                >
+                  全选
+                </el-checkbox>
+                <el-checkbox-group 
+                  v-model="selectedProjectIds"
+                  @change="handleProjectChange"
+                  :disabled="isAllProjectsSelected"
+                  style="width: 100%; max-height: 300px; overflow-y: auto; display: flex; flex-direction: column;"
+                >
+                  <el-checkbox 
+                    v-for="item in projectOptions"
+                    :key="item.id"
+                    :label="item.id"
+                    style="margin: 5px 0;"
+                  >
+                    {{ item.name }}
+                  </el-checkbox>
+                </el-checkbox-group>
+              </div>
+            </el-form-item>
+          </el-col>
+        </el-row>
         <el-row>
           <el-col :span="24">
             <el-form-item :label="t('user.form.remark')">
@@ -307,6 +337,7 @@ import { checkPermi } from '@/utils/permission';
 import { useUserStore } from '@/store/modules/user';
 import { useI18n } from 'vue-i18n';
 import { parseI18nName } from '@/utils/i18n';
+import request from '@/utils/request';
 
 const router = useRouter();
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -326,6 +357,9 @@ const enabledDeptOptions = ref<DeptTreeVO[]>([]);
 const initPassword = ref<string>('');
 const postOptions = ref<PostVO[]>([]);
 const roleOptions = ref<RoleVO[]>([]);
+const projectOptions = ref<Array<{id: number, name: string}>>([]);
+const selectedProjectIds = ref<number[]>([]);
+const isAllProjectsSelected = ref(false);
 /*** 用户导入参数 */
 const upload = reactive<ImportOption>({
   // 是否显示弹出层(用户导入)
@@ -376,7 +410,8 @@ const initFormData: UserForm = {
   status: '0',
   remark: '',
   postIds: [],
-  roleIds: []
+  roleIds: [],
+  projects: ''
 };
 
 const initData: PageData<UserForm, UserQuery> = {
@@ -476,6 +511,47 @@ const filterDisabledDept = (deptList: DeptTreeVO[]) => {
   });
 };
 
+/** 获取项目列表 */
+const getProjectList = async () => {
+  try {
+    const response = await request({
+      url: '/project/management/listOnUser',
+      method: 'get'
+    });
+    if (response.code === 200) {
+      projectOptions.value = response.data || [];
+    }
+  } catch (error) {
+    console.error('获取项目列表失败:', error);
+    projectOptions.value = [];
+  }
+};
+
+/** 处理全选项目 */
+const handleSelectAllProjects = (checked: boolean) => {
+  // 无论选中还是取消全选,都清空手动选择的项目
+  selectedProjectIds.value = [];
+};
+
+/** 处理项目选择变化 */
+const handleProjectChange = (value: number[]) => {
+  // 手动选择项目时,取消全选状态
+  if (value.length > 0) {
+    isAllProjectsSelected.value = false;
+  }
+};
+
+/** 监听项目选择变化,更新 form.value.projects */
+watch([isAllProjectsSelected, selectedProjectIds], () => {
+  if (isAllProjectsSelected.value) {
+    form.value.projects = '*';
+  } else if (selectedProjectIds.value.length > 0) {
+    form.value.projects = selectedProjectIds.value.join(',');
+  } else {
+    form.value.projects = '';
+  }
+}, { deep: true });
+
 /** 节点单击事件 */
 const handleNodeClick = (data: DeptVO) => {
   queryParams.value.deptId = data.id;
@@ -609,6 +685,8 @@ function submitFileForm() {
 /** 重置操作表单 */
 const reset = () => {
   form.value = { ...initFormData };
+  selectedProjectIds.value = [];
+  isAllProjectsSelected.value = false;
   userFormRef.value?.resetFields();
 };
 /** 取消按钮 */
@@ -621,6 +699,7 @@ const cancel = () => {
 const handleAdd = async () => {
   reset();
   const { data } = await api.getUser();
+  await getProjectList();
   dialog.visible = true;
   dialog.title = t('user.dialog.add');
   postOptions.value = data.posts;
@@ -633,6 +712,7 @@ const handleUpdate = async (row?: UserForm) => {
   reset();
   const userId = row?.userId || ids.value[0];
   const { data } = await api.getUser(userId);
+  await getProjectList();
   dialog.visible = true;
   dialog.title = t('user.dialog.edit');
   Object.assign(form.value, data.user);
@@ -643,12 +723,33 @@ const handleUpdate = async (row?: UserForm) => {
   form.value.postIds = data.postIds;
   form.value.roleIds = data.roleIds;
   form.value.password = '';
+  
+  // 初始化项目选择
+  if (data.user.projects === '*') {
+    // 全选状态
+    isAllProjectsSelected.value = true;
+    selectedProjectIds.value = [];
+  } else if (data.user.projects) {
+    // 手动选择状态
+    isAllProjectsSelected.value = false;
+    selectedProjectIds.value = data.user.projects.split(',').map((id: string) => parseInt(id)).filter((id: number) => !isNaN(id));
+  } else {
+    // 未选择,确保 projects 为空字符串而不是 null 或 undefined
+    isAllProjectsSelected.value = false;
+    selectedProjectIds.value = [];
+    form.value.projects = '';
+  }
 };
 
 /** 提交按钮 */
 const submitForm = () => {
   userFormRef.value?.validate(async (valid: boolean) => {
     if (valid) {
+      // 确保 projects 字段为空字符串而不是 null 或 undefined
+      if (!form.value.projects) {
+        form.value.projects = '';
+      }
+      
       if (form.value.userId) {
         // 自己编辑自己的情况下 不允许编辑角色部门岗位
         if (form.value.userId == useUserStore().userId) {
@@ -684,6 +785,9 @@ const resetForm = () => {
 
   form.value.id = undefined;
   form.value.status = '1';
+  form.value.projects = '';
+  selectedProjectIds.value = [];
+  isAllProjectsSelected.value = false;
 };
 onMounted(() => {
   getDeptTree(); // 初始化部门数据

+ 150 - 0
国际化处理说明-项目管理.md

@@ -0,0 +1,150 @@
+# 项目管理模块国际化说明
+
+## 已完成内容
+
+### 1. 静态页面国际化
+
+已完成以下部分的国际化:
+
+#### 搜索表单
+- 项目编号、名称、项目语言、项目类型
+- PD/GPD、PM/GPM、CTA/GCTA
+- 申办方、CRO
+- 开始时间、结束时间、创建时间、更新时间
+- 搜索、重置按钮
+
+#### 操作按钮
+- 新增、修改、删除、导出按钮
+
+#### 表格列
+- 序号、项目编号、名称、图标
+- 项目语言、项目类型、状态
+- PD/GPD、PM/GPM、CTA/GCTA
+- 申办方、CRO、备注
+- 创建者、创建时间、更新时间
+- 操作列(修改、删除tooltip)
+
+#### 表单对话框
+- 对话框标题(添加/修改项目)
+- 所有表单字段标签和占位符
+- 确定、取消按钮
+
+#### 提示信息
+- 删除确认提示
+- 操作成功、删除成功提示
+
+#### 验证规则
+- 序号、项目编号、名称、项目语言、项目类型的必填验证提示
+
+### 2. 国际化文件位置
+
+- **中文**: `src/lang/modules/project/management/zh_CN.ts`
+- **英文**: `src/lang/modules/project/management/en_US.ts`
+
+### 3. 使用方式
+
+在Vue组件中:
+
+```typescript
+import { useI18n } from 'vue-i18n';
+import { parseI18nName } from '@/utils/i18n';
+
+const { t } = useI18n();
+
+// 使用国际化函数
+t('project.management.search.code')
+t('project.management.button.add')
+t('project.management.table.name')
+
+// 解析字典的国际化名称
+parseI18nName(dict.label)
+```
+
+## 字典国际化
+
+### 需要国际化的字典
+
+1. **project_type** (项目类型)
+2. **project_language** (项目语言)
+
+### 字典国际化方法
+
+字典数据存储在数据库中,其`label`字段应该是JSON格式,包含多语言数据:
+
+```json
+{
+  "zh_CN": "中文名称",
+  "en_US": "English Name"
+}
+```
+
+### 数据库字典配置示例
+
+#### project_type (项目类型)
+
+| 字典值 | 字典标签(JSON格式) |
+|--------|---------------------|
+| 1 | `{"zh_CN":"临床试验","en_US":"Clinical Trial"}` |
+| 2 | `{"zh_CN":"真实世界研究","en_US":"Real World Study"}` |
+| 3 | `{"zh_CN":"上市后研究","en_US":"Post-Marketing Study"}` |
+
+#### project_language (项目语言)
+
+| 字典值 | 字典标签(JSON格式) |
+|--------|---------------------|
+| zh_CN | `{"zh_CN":"中文","en_US":"Chinese"}` |
+| en_US | `{"zh_CN":"英文","en_US":"English"}` |
+| zh_TW | `{"zh_CN":"繁体中文","en_US":"Traditional Chinese"}` |
+
+### 在管理后台配置字典
+
+1. 进入系统管理 -> 字典管理
+2. 找到对应的字典类型(`project_type`、`project_language`)
+3. 编辑字典数据的标签字段,使用JSON格式:
+   ```json
+   {"zh_CN":"中文标签","en_US":"English Label"}
+   ```
+
+### 前端处理
+
+前端代码已使用`parseI18nName(dict.label)`函数处理字典显示:
+
+```vue
+<!-- 下拉选择 -->
+<el-option 
+  v-for="dict in project_language" 
+  :key="dict.value" 
+  :label="parseI18nName(dict.label)" 
+  :value="dict.value"
+/>
+
+<!-- 表格显示 -->
+<dict-tag :options="project_language" :value="scope.row.language"/>
+```
+
+`parseI18nName`函数会:
+1. 解析JSON格式的label
+2. 根据当前语言环境返回对应的翻译
+3. 如果解析失败或找不到对应语言,返回原始值
+
+## 注意事项
+
+1. **字典标签格式**:必须是有效的JSON格式
+2. **语言代码**:使用`zh_CN`和`en_US`作为键名
+3. **回退机制**:如果当前语言没有翻译,会依次尝试zh_CN、en_US,最后使用原值
+4. **DictTag组件**:已自动支持国际化,无需修改
+
+## 类型错误说明
+
+当前存在一个与国际化无关的TypeScript类型错误:
+- 位置:第175行 `<image-upload v-model="form.icon"/>`
+- 原因:`form.icon`的类型定义与`image-upload`组件预期类型不匹配
+- 影响:不影响运行,仅是类型检查警告
+- 建议:需要更新`ManagementForm`类型定义或`image-upload`组件的props类型
+
+## 测试建议
+
+1. 切换语言测试所有文本是否正确显示
+2. 验证字典数据的国际化显示
+3. 测试表单验证提示信息
+4. 测试操作提示消息(成功、删除确认等)