Просмотр исходного кода

- [x] 归档:一个确认
- [x] 交接:编辑账号
- [x] 审核环节:直接通过/驳回编辑即可
质控:
- [x] 加上完成度:进度条形式,任务为发起人自己完成质控,审核完即可,无需等重复递交
- [x] 任务只需要查看,新增时不管
- [x] 开始时间不需要,变成开始按钮
递交:只选择到中心,文件名、生效日期已输入形式

Huanyi 3 месяцев назад
Родитель
Сommit
ea3a4a6839
34 измененных файлов с 1340 добавлено и 724 удалено
  1. 48 1
      src/api/document/document/index.ts
  2. 147 12
      src/api/document/document/types.ts
  3. 3 3
      src/api/home/taskCenter/audit/types.ts
  4. 1 0
      src/api/home/taskCenter/submission/types.ts
  5. 12 0
      src/api/project/management/index.ts
  6. 1 8
      src/components/ArchiveConfirmDialog/index.vue
  7. 5 5
      src/components/AuditLogDialog/index.vue
  8. 188 0
      src/components/FolderSelector/index.vue
  9. 27 2
      src/lang/modules/document/document/en_US.ts
  10. 27 2
      src/lang/modules/document/document/zh_CN.ts
  11. 2 4
      src/lang/modules/home/taskCenter/zh_CN.ts
  12. 1 0
      src/lang/modules/project/management/en_US.ts
  13. 1 0
      src/lang/modules/project/management/zh_CN.ts
  14. 1 1
      src/lang/modules/system/profile/zh_CN.ts
  15. 2 2
      src/lang/modules/system/user/zh_CN.ts
  16. 70 0
      src/router/index.ts
  17. 4 1
      src/utils/request.ts
  18. 22 13
      src/views/Qc/task/detail.vue
  19. 0 379
      src/views/Qc/task/index.vue
  20. 151 9
      src/views/Qc/task/list.vue
  21. 424 83
      src/views/document/folder/document/DocumentList.vue
  22. 31 2
      src/views/document/folder/document/FolderTree.vue
  23. 59 28
      src/views/document/folder/document/index.vue
  24. 0 28
      src/views/document/folder/index.vue
  25. 9 21
      src/views/document/folder/project.vue
  26. 3 3
      src/views/home/taskCenter/audit/index.vue
  27. 0 7
      src/views/home/taskCenter/questionResponse/index.vue
  28. 20 7
      src/views/home/taskCenter/submission/index.vue
  29. 7 9
      src/views/project/management/detail/components/header.vue
  30. 9 7
      src/views/project/management/detail/components/sidebar.vue
  31. 30 35
      src/views/project/management/detail/index.vue
  32. 0 37
      src/views/project/management/index.vue
  33. 27 7
      src/views/project/management/list.vue
  34. 8 8
      src/views/search/index.vue

+ 48 - 1
src/api/document/document/index.ts

@@ -1,5 +1,5 @@
 import request from '@/utils/request';
-import { DocumentForm, DocumentQuery, DocumentVO, DocumentMarkForm, DocumentAuditForm, DocumentSubmitForm } from './types';
+import { DocumentForm, DocumentQuery, DocumentVO, DocumentMarkForm, DocumentAuditForm, DocumentSubmitForm, DocumentSpecifyForm } from './types';
 import { AxiosPromise } from 'axios';
 import axios from 'axios';
 import FileSaver from 'file-saver';
@@ -148,3 +148,50 @@ export const downloadDocumentFile = async (ossId: number, fileName?: string) =>
     loadingInstance.close();
   }
 };
+
+/**
+ * 统计临时文件夹文档数量
+ * @param projectId 项目ID
+ */
+export const countTempDocuments = (projectId: number | string): AxiosPromise<number> => {
+  return request({
+    url: '/document/document/countTemp',
+    method: 'get',
+    params: { projectId }
+  });
+};
+
+/**
+ * 删除临时文件夹中的文档
+ * @param documentId 文档ID
+ */
+export const removeTempDocument = (documentId: number | string) => {
+  return request({
+    url: `/document/document/removeTemp/${documentId}`,
+    method: 'delete'
+  });
+};
+
+/**
+ * 查询可指定的文档列表
+ * @param query 查询参数
+ */
+export const listDocumentOnSpecify = (query: any): AxiosPromise<any> => {
+  return request({
+    url: '/document/document/listOnSpecify',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 指定文档
+ * @param data 指定文档表单数据
+ */
+export const specifyDocument = (data: DocumentSpecifyForm) => {
+  return request({
+    url: '/document/document/specify',
+    method: 'put',
+    data: data
+  });
+};

+ 147 - 12
src/api/document/document/types.ts

@@ -9,26 +9,41 @@ export interface DocumentVO {
    */
   folderId: string | number;
 
+  /**
+   * 文件夹名称
+   */
+  folderName?: string;
+
   /**
    * 文档标识
    */
   specification?: string;
 
+  /**
+   * 文档标识标签
+   */
+  specificationLabel?: string;
+
   /**
    * 计划文件名称/文件名称
    */
   name: string;
 
   /**
-   * 文件名
+   * 实际文档名称
    */
-  fileName?: string;
+  actualDocumentName?: string;
 
   /**
    * 计划文件类型
    */
   planType?: string;
 
+  /**
+   * 计划文件类型标签
+   */
+  planDocumentTypeLabel?: string;
+
   /**
    * 状态
    */
@@ -45,9 +60,9 @@ export interface DocumentVO {
   url?: string;
 
   /**
-   * 文件本体OSS ID
+   * 实际文档ID
    */
-  ossId?: string | number;
+  actualDocument?: string | number;
 
   /**
    * 递交时间
@@ -75,24 +90,44 @@ export interface DocumentVO {
   createBy?: string | number;
 
   /**
-   * 计划递交人
+   * 递交人名称
    */
-  submitter?: string;
+  submitterName?: string;
 
   /**
-   * 计划递交人ID
+   * 递交人ID
    */
-  planSubmitter?: string | number;
+  submitter?: string | number;
 
   /**
-   * 递交人ID
+   * 计划递交人名称
    */
-  submitter?: string | number;
+  planSubmitterName?: string;
+
+  /**
+   * 计划递交人ID
+   */
+  planSubmitter?: string | number;
 
   /**
    * 文档类型:0-非计划文档,1-计划文档
    */
   type?: number;
+
+  /**
+   * 规格类型
+   */
+  specificationType?: number;
+
+  /**
+   * 是否需要寄送
+   */
+  sendFlag?: boolean;
+
+  /**
+   * 寄送状态
+   */
+  sendStatus?: boolean;
 }
 
 export interface DocumentForm extends BaseEntity {
@@ -116,6 +151,16 @@ export interface DocumentForm extends BaseEntity {
    */
   submitterId?: string | number;
 
+  /**
+   * 递交人ID
+   */
+  submitter?: string | number;
+
+  /**
+   * 计划递交人ID
+   */
+  planSubmitter?: string | number;
+
   /**
    * 所属文件夹ID
    */
@@ -132,9 +177,9 @@ export interface DocumentForm extends BaseEntity {
   planType?: string;
 
   /**
-   * 文件本体OSS ID
+   * 实际文档ID
    */
-  ossId?: string | number;
+  actualDocument?: string | number | null;
 
   /**
    * 状态
@@ -160,6 +205,11 @@ export interface DocumentForm extends BaseEntity {
    * 是否需要寄送
    */
   sendFlag?: boolean;
+
+  /**
+   * 生效日期
+   */
+  effectiveDate?: string;
 }
 
 export interface DocumentQuery extends PageQuery {
@@ -222,4 +272,89 @@ export interface DocumentSubmitForm {
    * 文件本体OSS ID
    */
   ossId: string | number;
+
+  /**
+   * 生效日期
+   */
+  effectiveDate?: string;
+}
+
+/**
+ * 指定文档查询参数
+ */
+export interface DocumentSpecifyQuery extends PageQuery {
+  /**
+   * 项目ID
+   */
+  projectId: string | number;
+
+  /**
+   * 文件名(模糊检索)
+   */
+  name?: string;
+}
+
+/**
+ * 指定文档列表项
+ */
+export interface DocumentSpecifyVO {
+  /**
+   * 文档ID
+   */
+  id: string | number;
+
+  /**
+   * 文档名称
+   */
+  name: string;
+
+  /**
+   * 文件夹名称
+   */
+  folder: string;
+
+  /**
+   * 状态
+   */
+  status: number;
+
+  /**
+   * 递交截止时间
+   */
+  deadline: string;
+
+  /**
+   * 计划递交人
+   */
+  planSubmitter: string;
+
+  /**
+   * 创建人
+   */
+  createBy: string;
+
+  /**
+   * 创建时间
+   */
+  createTime: string;
+}
+
+/**
+ * 指定文档表单
+ */
+export interface DocumentSpecifyForm {
+  /**
+   * 临时文档ID
+   */
+  documentId: number | string;
+
+  /**
+   * 指定递交缺失的文档ID(可选)
+   */
+  missingDocumentId?: number | string;
+
+  /**
+   * 指定文件夹ID(可选)
+   */
+  folderId?: number | string;
 }

+ 3 - 3
src/api/home/taskCenter/audit/types.ts

@@ -9,9 +9,9 @@ export interface AuditTaskVO {
   submitTime: string;
   createBy: string;
   createTime: string;
-  ossId: number;
-  ossUrl?: string; // OSS 文件 URL
-  fileName?: string; // 文件名(可选,如果后端返回)
+  actualDocument: number; // 实际文档ID
+  actualDocumentUrl?: string; // 实际文档 URL
+  actualDocumentName?: string; // 实际文档名称
 }
 
 export interface AuditTaskQuery extends PageQuery {

+ 1 - 0
src/api/home/taskCenter/submission/types.ts

@@ -23,4 +23,5 @@ export interface SubmissionTaskQuery extends PageQuery {
 export interface SubmissionTaskSubmitForm {
   documentId: number;
   ossId: number;
+  effectiveDate?: string;
 }

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

@@ -270,3 +270,15 @@ export const editProjectMember = (data: EditProjectMemberForm) => {
     data: data
   });
 };
+
+/**
+ * 导出项目报告
+ * @param projectId 项目ID
+ */
+export const exportProjectReport = (projectId: string | number) => {
+  return request({
+    url: `/project/management/export/${projectId}`,
+    method: 'post',
+    responseType: 'blob'
+  });
+};

+ 1 - 8
src/components/ArchiveConfirmDialog/index.vue

@@ -132,14 +132,7 @@ const dialogVisible = ref(false);
 const confirmDialogVisible = ref(false);
 const loading = ref(false);
 const selectedFolderId = ref<number | string | undefined>(undefined);
-const treeRef = ref();
-const isSettingChecked = ref(false); // 添加标志位防止循环触发
-
-// 树配置
-const treeProps = {
-  children: 'children',
-  label: 'name'
-};
+const folderSelectorRef = ref();
 
 // 默认展开的节点
 const defaultExpandedKeys = computed(() => {

+ 5 - 5
src/components/AuditLogDialog/index.vue

@@ -57,11 +57,11 @@
         <el-table-column :label="operationLabel" width="140" align="center" fixed="right">
           <template #default="scope">
             <el-button
-              v-if="scope.row.ossId"
+              v-if="scope.row.uploadVersion"
               type="primary"
               icon="Download"
               style="padding: 0 5px; font-size: 10px; height: 24px; --el-button-icon-span-gap: 2px"
-              @click="handleDownload(scope.row.ossId, scope.row.documentName)"
+              @click="handleDownload(scope.row.uploadVersion, scope.row.documentName)"
             >
               {{ downloadButtonText }}
             </el-button>
@@ -192,9 +192,9 @@ watch(
 );
 
 /** 下载历史文件 */
-const handleDownload = async (ossId: number, fileName: string) => {
-  if (!ossId) return;
-  await downloadDocumentFile(ossId, fileName);
+const handleDownload = async (uploadVersion: number, fileName: string) => {
+  if (!uploadVersion) return;
+  await downloadDocumentFile(uploadVersion, fileName);
 };
 </script>
 

+ 188 - 0
src/components/FolderSelector/index.vue

@@ -0,0 +1,188 @@
+<template>
+  <div class="folder-selector">
+    <el-alert v-if="!treeData || treeData.length === 0" title="暂无可选文件夹" type="warning" :closable="false" style="margin-bottom: 10px" />
+    <el-tree
+      ref="treeRef"
+      :data="treeData"
+      :props="treeProps"
+      node-key="id"
+      :default-expanded-keys="defaultExpandedKeys"
+      :default-checked-keys="defaultCheckedKeys"
+      show-checkbox
+      check-strictly
+      :check-on-click-node="true"
+      @check="handleCheck"
+      class="folder-tree"
+    >
+      <template #default="{ node, data }">
+        <span class="custom-tree-node">
+          <el-icon v-if="data.type === 1"><OfficeBuilding /></el-icon>
+          <el-icon v-else-if="data.type === 2"><Location /></el-icon>
+          <el-icon v-else><Folder /></el-icon>
+          <span class="node-label">{{ node.label }}</span>
+        </span>
+      </template>
+    </el-tree>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch, computed } from 'vue';
+import { OfficeBuilding, Location, Folder } from '@element-plus/icons-vue';
+
+interface FolderNode {
+  id: number | string;
+  name: string;
+  type?: number;
+  children?: FolderNode[];
+}
+
+interface Props {
+  treeData?: FolderNode[];
+  defaultFolderId?: number | string;
+}
+
+interface Emits {
+  (e: 'update:selectedFolderId', value: number | string | undefined): void;
+  (e: 'change', value: number | string | undefined): void;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  treeData: () => []
+});
+const emit = defineEmits<Emits>();
+
+const treeRef = ref();
+const selectedFolderId = ref<number | string | undefined>(undefined);
+const isSettingChecked = ref(false);
+
+// 树配置
+const treeProps = {
+  children: 'children',
+  label: 'name'
+};
+
+// 默认展开的节点
+const defaultExpandedKeys = computed(() => {
+  if (props.defaultFolderId) {
+    return [props.defaultFolderId];
+  }
+  return [];
+});
+
+// 默认选中的节点
+const defaultCheckedKeys = computed(() => {
+  if (props.defaultFolderId) {
+    return [props.defaultFolderId];
+  }
+  return [];
+});
+
+// 监听默认文件夹ID变化
+watch(
+  () => props.defaultFolderId,
+  (val) => {
+    if (val) {
+      selectedFolderId.value = val;
+      setTimeout(() => {
+        if (treeRef.value) {
+          treeRef.value.setCheckedKeys([val]);
+        }
+      }, 100);
+    }
+  },
+  { immediate: true }
+);
+
+// 处理复选框选中(实现单选效果)
+const handleCheck = (data: FolderNode, checked: any) => {
+  // 如果是程序设置的,跳过处理
+  if (isSettingChecked.value) {
+    return;
+  }
+
+  const clickedId = data.id;
+  const checkedKeys = checked.checkedKeys || [];
+
+  // 设置标志位,防止循环触发
+  isSettingChecked.value = true;
+
+  // 实现单选:只保留当前点击的节点
+  if (checkedKeys.includes(clickedId)) {
+    // 如果当前节点在选中列表中,说明是选中操作
+    selectedFolderId.value = clickedId;
+    // 清除所有选中,只保留当前节点
+    treeRef.value?.setCheckedKeys([clickedId]);
+    emit('update:selectedFolderId', clickedId);
+    emit('change', clickedId);
+  } else {
+    // 如果当前节点不在选中列表中,说明是取消选中操作
+    // 不允许取消选中,重新选中该节点
+    treeRef.value?.setCheckedKeys([selectedFolderId.value || clickedId]);
+  }
+
+  // 延迟重置标志位
+  setTimeout(() => {
+    isSettingChecked.value = false;
+  }, 100);
+};
+
+// 获取选中的文件夹ID
+const getSelectedFolderId = (): number | string | undefined => {
+  return selectedFolderId.value;
+};
+
+// 设置选中的文件夹
+const setSelectedFolderId = (id: number | string | undefined) => {
+  selectedFolderId.value = id;
+  if (id && treeRef.value) {
+    treeRef.value.setCheckedKeys([id]);
+  }
+};
+
+// 清除选中
+const clearSelection = () => {
+  selectedFolderId.value = undefined;
+  if (treeRef.value) {
+    treeRef.value.setCheckedKeys([]);
+  }
+};
+
+// 暴露方法
+defineExpose({
+  getSelectedFolderId,
+  setSelectedFolderId,
+  clearSelection
+});
+</script>
+
+<style scoped lang="scss">
+.folder-selector {
+  .folder-tree {
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    padding: 10px;
+    max-height: 400px;
+    overflow-y: auto;
+
+    :deep(.el-tree-node__content) {
+      height: 32px;
+    }
+
+    .custom-tree-node {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+      flex: 1;
+
+      .el-icon {
+        font-size: 16px;
+      }
+
+      .node-label {
+        font-size: 14px;
+      }
+    }
+  }
+}
+</style>

+ 27 - 2
src/lang/modules/document/document/en_US.ts

@@ -20,7 +20,8 @@ export default {
     archive: 'Archive',
     confirm: 'Confirm',
     next: 'Next',
-    back: 'Back'
+    back: 'Back',
+    specify: 'Specify'
   },
   // Menu
   menu: {
@@ -45,6 +46,7 @@ export default {
     auditDocument: 'Audit Document',
     submitDocument: 'Submit Document',
     auditLog: 'Audit Log',
+    specifyDocument: 'Specify Document',
     archiveDialog: {
       title: 'Archive Confirmation',
       confirmMessage: 'Confirm that the file has been successfully archived?',
@@ -173,12 +175,35 @@ export default {
   },
   // Submit Form
   submitForm: {
-    file: 'File'
+    file: 'File',
+    effectiveDate: 'Effective Date',
+    effectiveDatePlaceholder: 'Please select effective date'
   },
   // Submit Validation Rules
   submitRule: {
     fileRequired: 'Please upload file'
   },
+  // Specify Form
+  specifyForm: {
+    type: 'Specify Type',
+    specifyMissing: 'Specify Missing Submission',
+    specifyFolder: 'Specify Folder',
+    searchPlaceholder: 'Search by file name',
+    documentName: 'Document Name',
+    folder: 'Folder',
+    status: 'Status',
+    deadline: 'Deadline',
+    planSubmitter: 'Plan Submitter',
+    createBy: 'Created By',
+    createTime: 'Create Time',
+    action: 'Action',
+    select: 'Select',
+    selected: 'Selected'
+  },
+  // Specify Validation Rules
+  specifyRule: {
+    typeRequired: 'Please select specify type'
+  },
   // Empty State
   empty: {
     description: 'Please select a folder to view documents'

+ 27 - 2
src/lang/modules/document/document/zh_CN.ts

@@ -20,7 +20,8 @@ export default {
     archive: '归档',
     confirm: '确认',
     next: '下一步',
-    back: '返回'
+    back: '返回',
+    specify: '指定'
   },
   // 菜单
   menu: {
@@ -45,6 +46,7 @@ export default {
     auditDocument: '审核文档',
     submitDocument: '递交文档',
     auditLog: '审核记录',
+    specifyDocument: '指定文档',
     archiveDialog: {
       title: '归档确认',
       confirmMessage: '确认该文件成功归档?',
@@ -173,12 +175,35 @@ export default {
   },
   // 递交表单
   submitForm: {
-    file: '文件'
+    file: '文件',
+    effectiveDate: '生效日期',
+    effectiveDatePlaceholder: '请选择生效日期'
   },
   // 递交验证规则
   submitRule: {
     fileRequired: '请上传文件'
   },
+  // 指定表单
+  specifyForm: {
+    type: '指定类型',
+    specifyMissing: '指定递交缺失',
+    specifyFolder: '指定文件夹',
+    searchPlaceholder: '请输入文件名搜索',
+    documentName: '文档名称',
+    folder: '文件夹',
+    status: '状态',
+    deadline: '递交截止时间',
+    planSubmitter: '计划递交人',
+    createBy: '创建人',
+    createTime: '创建时间',
+    action: '操作',
+    select: '选择',
+    selected: '已选择'
+  },
+  // 指定验证规则
+  specifyRule: {
+    typeRequired: '请选择指定类型'
+  },
   // 空状态
   empty: {
     description: '请选择文件夹查看文档列表'

+ 2 - 4
src/lang/modules/home/taskCenter/zh_CN.ts

@@ -112,7 +112,8 @@ export default {
       title: '递交文档',
       file: '文件',
       confirm: '确认递交',
-      cancel: '取消'
+      cancel: '取消',
+      auditLog: '审核记录'
     },
     message: {
       getListFailed: '获取任务列表失败',
@@ -122,9 +123,6 @@ export default {
     rule: {
       fileRequired: '请上传文件'
     },
-    dialog: {
-      auditLog: '审核记录'
-    },
     auditLog: {
       result: '审核结果',
       selectResult: '请选择审核结果',

+ 1 - 0
src/lang/modules/project/management/en_US.ts

@@ -43,6 +43,7 @@ export default {
     edit: 'Edit',
     delete: 'Delete',
     export: 'Export',
+    exportReport: 'Export Report',
     submit: 'Submit',
     cancel: 'Cancel',
     updateStatus: 'Update Status'

+ 1 - 0
src/lang/modules/project/management/zh_CN.ts

@@ -43,6 +43,7 @@ export default {
     edit: '修改',
     delete: '删除',
     export: '导出',
+    exportReport: '导出报告',
     submit: '确 定',
     cancel: '取 消',
     updateStatus: '更新状态'

+ 1 - 1
src/lang/modules/system/profile/zh_CN.ts

@@ -64,7 +64,7 @@ export default {
     oldPasswordRequired: '旧密码不能为空',
     newPasswordRequired: '新密码不能为空',
     passwordLength: '长度在 6 到 20 个字符',
-    passwordPattern: '不能包含非法字符:< > " \' \\ |',
+    passwordPattern: '不能包含非法字符:< > " \' \\\\ |',
     confirmPasswordRequired: '确认密码不能为空',
     passwordMismatch: '两次输入的密码不一致'
   },

+ 2 - 2
src/lang/modules/system/user/zh_CN.ts

@@ -89,7 +89,7 @@ export default {
     nickNameRequired: '用户昵称不能为空',
     passwordRequired: '用户密码不能为空',
     passwordLength: '用户密码长度必须介于 5 和 20 之间',
-    passwordPattern: '不能包含非法字符:< > " \' \\ |',
+    passwordPattern: '不能包含非法字符:< > " \' \\\\ |',
     emailFormat: '请输入正确的邮箱地址',
     phonenumberFormat: '请输入正确的手机号码',
     roleIdsRequired: '用户角色不能为空'
@@ -110,7 +110,7 @@ export default {
     resetPwdText: '请输入"{userName}"的新密码',
     resetPwdSuccess: '修改成功,新密码是:{password}',
     resetPwdLengthError: '用户密码长度必须介于 5 和 20 之间',
-    resetPwdPatternError: '不能包含非法字符:< > " \' \\ |',
+    resetPwdPatternError: '不能包含非法字符:< > " \' \\\\ |',
     confirmButton: '确定',
     cancelButton: '取消',
     operationSuccess: '操作成功'

+ 70 - 0
src/router/index.ts

@@ -99,6 +99,76 @@ export const constantRoutes: RouteRecordRaw[] = [
     redirect: '/demo/permission-tree',
     meta: { title: '演示', icon: 'example' },
     children: demoRoutes
+  },
+  // 项目管理详情页路由
+  {
+    path: '/project/management/detail',
+    component: Layout,
+    hidden: true,
+    redirect: '/project/management/detail/basicInfo',
+    children: [
+      {
+        path: '',
+        component: () => import('@/views/project/management/detail/index.vue'),
+        name: 'ProjectManagementDetail',
+        redirect: '/project/management/detail/basicInfo',
+        meta: { title: '{"zh_CN":"项目详情","en_US":"Project Detail"}', activeMenu: '/project/management' },
+        children: [
+          {
+            path: 'basicInfo',
+            component: () => import('@/views/project/management/detail/pages/basicInfo.vue'),
+            name: 'ProjectDetailBasicInfo',
+            meta: { title: '{"zh_CN":"基本信息","en_US":"Basic Info"}', activeMenu: '/project/management' }
+          },
+          {
+            path: 'centerInfo',
+            component: () => import('@/views/project/management/detail/pages/centerInfo.vue'),
+            name: 'ProjectDetailCenterInfo',
+            meta: { title: '{"zh_CN":"中心信息","en_US":"Center Info"}', activeMenu: '/project/management' }
+          },
+          {
+            path: 'projectMember',
+            component: () => import('@/views/project/management/detail/pages/projectMember.vue'),
+            name: 'ProjectDetailProjectMember',
+            meta: { title: '{"zh_CN":"项目成员","en_US":"Project Member"}', activeMenu: '/project/management' }
+          },
+          {
+            path: 'centerMember',
+            component: () => import('@/views/project/management/detail/pages/centerMember.vue'),
+            name: 'ProjectDetailCenterMember',
+            meta: { title: '{"zh_CN":"中心成员","en_US":"Center Member"}', activeMenu: '/project/management' }
+          }
+        ]
+      }
+    ]
+  },
+  // 文档管理页面路由
+  {
+    path: '/document/folder/document',
+    component: Layout,
+    hidden: true,
+    children: [
+      {
+        path: '',
+        component: () => import('@/views/document/folder/document/index.vue'),
+        name: 'DocumentManagement',
+        meta: { title: '{"zh_CN":"文档管理","en_US":"Document Management"}', activeMenu: '/document/folder' }
+      }
+    ]
+  },
+  // QC任务详情页路由
+  {
+    path: '/qc/task/detail',
+    component: Layout,
+    hidden: true,
+    children: [
+      {
+        path: '',
+        component: () => import('@/views/qc/task/detail.vue'),
+        name: 'QcTaskDetail',
+        meta: { title: '{"zh_CN":"任务详情","en_US":"Task Detail"}', activeMenu: '/qc/task' }
+      }
+    ]
   }
 ];
 

+ 4 - 1
src/utils/request.ts

@@ -183,7 +183,10 @@ export function download(url: string, params: any, fileName: string) {
         return tansParams(params);
       }
     ],
-    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+    headers: {
+      'Content-Type': 'application/x-www-form-urlencoded',
+      'Content-Language': getLanguage()
+    },
     responseType: 'blob'
   }).then(async (resp: any) => {
     const isLogin = blobValidate(resp);

+ 22 - 13
src/views/Qc/task/detail.vue

@@ -94,7 +94,8 @@
 </template>
 
 <script setup name="TaskDetail" lang="ts">
-import { ref, onMounted, watch } from 'vue';
+import { ref, onMounted, watch, computed } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
 import { getTask, listTaskDetail } from '@/api/qc/task';
 import { TaskVO } from '@/api/qc/task/types';
 
@@ -126,24 +127,27 @@ const taskItemsQuery = ref({
 });
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-
-const props = defineProps<{
-  taskId?: string | number;
-}>();
-
-const emit = defineEmits<{
-  back: [];
-}>();
+const route = useRoute();
+const router = useRouter();
+
+// 从路由参数获取任务ID
+const taskId = computed(() => {
+  const id = route.query.id;
+  if (!id) return undefined;
+  const numId = typeof id === 'string' ? parseInt(id) : id;
+  console.log('Task Detail - taskId:', numId, 'from route.query:', route.query.id);
+  return numId;
+});
 
 const loading = ref(true);
 const taskDetail = ref<TaskVO | null>(null);
 
 const fetchTaskDetail = async () => {
-  if (!props.taskId) return;
+  if (!taskId.value) return;
 
   try {
     loading.value = true;
-    const res = await getTask(props.taskId);
+    const res = await getTask(taskId.value);
     taskDetail.value = res.data;
     // 获取任务列表
     await fetchTaskItems();
@@ -188,12 +192,13 @@ const handleTaskItemsPagination = (pageNum: number, pageSize: number) => {
 };
 
 const handleBack = () => {
-  emit('back');
+  router.push('/qc/task');
 };
 
 watch(
-  () => props.taskId,
+  () => route.query.id,
   (newId) => {
+    console.log('Route query id changed:', newId);
     if (newId) {
       fetchTaskDetail();
     }
@@ -201,6 +206,10 @@ watch(
 );
 
 onMounted(() => {
+  console.log('Task Detail mounted');
+  console.log('Route:', route);
+  console.log('Route query:', route.query);
+  console.log('TaskId:', taskId.value);
   fetchTaskDetail();
 });
 </script>

+ 0 - 379
src/views/Qc/task/index.vue

@@ -1,379 +0,0 @@
-<template>
-  <div class="p-2">
-    <!-- 列表视图 -->
-    <TaskList v-if="currentView === 'list'" @add="handleAdd" @view="handleView" />
-
-    <!-- 详情视图 -->
-    <TaskDetail v-else-if="currentView === 'detail'" :taskId="currentTaskId" @back="handleBack" />
-
-    <!-- 添加或修改文档质控任务对话框 -->
-    <el-dialog :title="dialog.title" v-model="dialog.visible" width="900px" append-to-body>
-      <el-form ref="taskFormRef" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="任务名称" prop="name">
-          <el-input v-model="form.name" placeholder="请输入任务名称" />
-        </el-form-item>
-        <el-form-item label="发起人" prop="initiator">
-          <el-input v-model="initiatorName" placeholder="请选择发起人" readonly @click="handleSelectInitiator"
-            style="cursor: pointer">
-            <template #suffix>
-              <el-icon>
-                <User />
-              </el-icon>
-            </template>
-          </el-input>
-        </el-form-item>
-        <el-form-item label="质控项目" prop="projectId">
-          <el-select v-model="form.projectId" filterable remote reserve-keyword placeholder="请输入项目名称搜索"
-            :remote-method="remoteSearchProject" :loading="projectLoading" style="width: 100%">
-            <el-option v-for="item in projectList" :key="item.id" :label="item.name" :value="item.id" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="占比" prop="proportion">
-          <el-input-number v-model="form.proportion" :min="0" :precision="0" :step="1" placeholder="请输入占比(整数)"
-            style="width: 100%" controls-position="right" />
-        </el-form-item>
-
-        <!-- 质控任务列表(仅修改时显示) -->
-        <el-form-item v-if="form.id && taskDetailList.length > 0" label="质控任务">
-          <el-table :data="taskDetailList" border style="width: 100%" max-height="400">
-            <el-table-column prop="documentId" label="文档序号" width="100" align="center" />
-            <el-table-column prop="documentName" label="文档名称" min-width="150" />
-            <el-table-column label="质控人" width="150" align="center">
-              <template #default="scope">
-                <span :style="{ color: scope.row.executorStatus && scope.row.executorStatus !== '0' ? 'red' : '' }">
-                  {{ scope.row.executorName }}
-                </span>
-              </template>
-            </el-table-column>
-            <el-table-column label="状态" width="100" align="center">
-              <template #default="scope">
-                <el-tag v-if="scope.row.executorStatus" :type="scope.row.executorStatus === '0' ? 'success' : 'danger'">
-                  {{ getUserStatusText(scope.row.executorStatus) }}
-                </el-tag>
-                <span v-else>-</span>
-              </template>
-            </el-table-column>
-            <el-table-column label="操作" width="120" align="center">
-              <template #default="scope">
-                <el-button v-if="scope.row.executorStatus && scope.row.executorStatus !== '0'" type="warning"
-                  icon="RefreshRight"
-                  style="padding: 0 5px; font-size: 10px; height: 24px; --el-button-icon-span-gap: 2px"
-                  @click="handleChangeExecutor(scope.row)">
-                  更换质控人
-                </el-button>
-              </template>
-            </el-table-column>
-          </el-table>
-        </el-form-item>
-
-        <el-form-item label="截止时间" prop="deadline">
-          <el-date-picker clearable v-model="form.deadline" type="date" value-format="YYYY-MM-DD" placeholder="请选择截止时间"
-            style="width: 100%">
-          </el-date-picker>
-        </el-form-item>
-        <el-form-item label="备注" prop="note">
-          <el-input v-model="form.note" type="textarea" placeholder="请输入内容" />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
-          <el-button @click="cancel">取 消</el-button>
-        </div>
-      </template>
-    </el-dialog>
-
-    <!-- 更换质控人对话框 -->
-    <UserSelect ref="userSelectRef" :multiple="false" @confirm-call-back="handleUserSelectCallback" />
-
-    <!-- 选择发起人对话框 -->
-    <UserSelect ref="initiatorSelectRef" :multiple="false" @confirm-call-back="handleInitiatorSelectCallback" />
-  </div>
-</template>
-
-<script setup name="Task" lang="ts">
-import { addTask, updateTask, generateTaskDetail, getTask } from '@/api/qc/task';
-import { TaskVO, TaskForm, TaskDetailVO } from '@/api/qc/task/types';
-import { getProjectByName } from '@/api/project/management';
-import { useUserStore } from '@/store/modules/user';
-import UserSelect from '@/components/UserSelect/index.vue';
-import { UserVO } from '@/api/system/user/types';
-import TaskList from './list.vue';
-import TaskDetail from './detail.vue';
-
-const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const userStore = useUserStore();
-
-// 视图状态管理
-const currentView = ref('list'); // 'list' 或 'detail'
-const currentTaskId = ref<string | number>();
-
-const buttonLoading = ref(false);
-
-// 项目列表
-const projectList = ref<Array<{ id: number; name: string }>>([]);
-const projectLoading = ref(false);
-
-// 质控任务详情列表
-const taskDetailList = ref<TaskDetailVO[]>([]);
-// 当前选中要更换质控人的任务详情
-const currentTaskDetail = ref<TaskDetailVO | null>(null);
-
-// 用户选择组件引用
-const userSelectRef = ref<InstanceType<typeof UserSelect>>();
-const initiatorSelectRef = ref<InstanceType<typeof UserSelect>>();
-
-// 发起人昵称(用于显示)
-const initiatorName = ref<string>('');
-
-const taskFormRef = ref<ElFormInstance>();
-
-const dialog = reactive<DialogOption>({
-  visible: false,
-  title: ''
-});
-
-// 用户状态枚举映射
-const userStatusMap = {
-  '0': '正常',
-  '1': '停用',
-  '2': '删除'
-};
-
-/** 获取用户状态文本 */
-const getUserStatusText = (status: string) => {
-  return userStatusMap[status] || '未知';
-};
-
-const initFormData: TaskForm = {
-  id: undefined,
-  name: undefined,
-  initiator: undefined,
-  projectId: undefined,
-  proportion: undefined,
-  deadline: undefined,
-  status: undefined,
-  note: undefined
-};
-const data = reactive<PageData<TaskForm, any>>({
-  form: { ...initFormData },
-  rules: {
-    name: [{ required: true, message: '质控名称不能为空', trigger: 'blur' }],
-    initiator: [{ required: true, message: '发起人不能为空', trigger: 'blur' }],
-    projectId: [{ required: true, message: '质控项目不能为空', trigger: 'change' }],
-    proportion: [
-      { required: true, message: '占比不能为空', trigger: 'blur' },
-      { type: 'number', message: '占比必须为数字', trigger: 'blur' },
-      {
-        validator: (rule: any, value: any, callback: any) => {
-          if (value !== undefined && value !== null && value !== '') {
-            if (!Number.isInteger(Number(value))) {
-              callback(new Error('占比必须为整数'));
-            } else if (value < 0) {
-              callback(new Error('占比不能为负数'));
-            } else {
-              callback();
-            }
-          } else {
-            callback();
-          }
-        },
-        trigger: 'blur'
-      }
-    ],
-
-    deadline: [{ required: true, message: '截止时间不能为空', trigger: 'blur' }],
-    status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
-  }
-});
-
-const { form, rules } = toRefs(data);
-
-/** 远程搜索项目 */
-const remoteSearchProject = async (query: string) => {
-  if (query) {
-    projectLoading.value = true;
-    try {
-      const res = await getProjectByName(query);
-      projectList.value = res.data || [];
-    } catch (error) {
-      console.error('搜索项目失败:', error);
-      projectList.value = [];
-    } finally {
-      projectLoading.value = false;
-    }
-  } else {
-    projectList.value = [];
-  }
-};
-
-/** 项目选择变化时加载质控任务详情(仅修改时) */
-const handleProjectChange = async (projectId: number) => {
-  // 新增时不加载质控任务详情
-  if (!form.value.id) {
-    return;
-  }
-
-  if (!projectId) {
-    taskDetailList.value = [];
-    return;
-  }
-
-  try {
-    const res = await generateTaskDetail(projectId);
-    if (res.code === 200 && res.data) {
-      taskDetailList.value = res.data;
-    } else {
-      taskDetailList.value = [];
-      proxy?.$modal.msgWarning('获取质控任务详情失败');
-    }
-  } catch (error) {
-    console.error('获取质控任务详情失败:', error);
-    taskDetailList.value = [];
-    proxy?.$modal.msgError('获取质控任务详情失败');
-  }
-};
-
-/** 更换质控人 */
-const handleChangeExecutor = (row: TaskDetailVO) => {
-  currentTaskDetail.value = row;
-  userSelectRef.value?.open();
-};
-
-/** 用户选择回调 */
-const handleUserSelectCallback = (users: UserVO[]) => {
-  if (users && users.length > 0 && currentTaskDetail.value) {
-    const selectedUser = users[0];
-    currentTaskDetail.value.executor = selectedUser.userId;
-    currentTaskDetail.value.executorName = selectedUser.nickName || selectedUser.userName;
-    currentTaskDetail.value.executorStatus = '0'; // 新选择的用户状态为正常
-    proxy?.$modal.msgSuccess('更换质控人成功');
-  }
-};
-
-/** 选择发起人 */
-const handleSelectInitiator = () => {
-  initiatorSelectRef.value?.open();
-};
-
-/** 发起人选择回调 */
-const handleInitiatorSelectCallback = (users: UserVO[]) => {
-  if (users && users.length > 0) {
-    const selectedUser = users[0];
-    form.value.initiator = selectedUser.userId;
-    initiatorName.value = selectedUser.nickName || selectedUser.userName;
-  }
-};
-
-/** 取消按钮 */
-const cancel = () => {
-  reset();
-  dialog.visible = false;
-};
-
-/** 表单重置 */
-const reset = () => {
-  form.value = { ...initFormData };
-  projectList.value = [];
-  taskDetailList.value = [];
-  currentTaskDetail.value = null;
-  initiatorName.value = '';
-  taskFormRef.value?.resetFields();
-};
-
-/** 新增按钮操作 */
-const handleAdd = () => {
-  reset();
-  // 设置默认发起人为当前用户
-  form.value.initiator = userStore.userId;
-  initiatorName.value = userStore.nickname || userStore.name;
-  dialog.visible = true;
-  dialog.title = '添加文档质控任务';
-};
-
-/** 查看任务详情 */
-const handleView = (taskId: string | number) => {
-  currentTaskId.value = taskId;
-  currentView.value = 'detail';
-};
-
-/** 返回列表 */
-const handleBack = () => {
-  currentView.value = 'list';
-  currentTaskId.value = undefined;
-};
-
-/** 修改按钮操作 */
-const handleUpdate = async (row: TaskVO) => {
-  reset();
-  const res = await getTask(row.id);
-  Object.assign(form.value, res.data);
-
-  // 设置发起人昵称
-  if (res.data.initiatorName) {
-    initiatorName.value = res.data.initiatorName;
-  } else if (res.data.initiator) {
-    // 如果后端没有返回昵称,使用ID作为显示(或者可以调用接口获取)
-    initiatorName.value = String(res.data.initiator);
-  }
-
-  // 如果有项目ID,加载项目信息到下拉列表
-  if (form.value.projectId) {
-    try {
-      const projectRes = await getProjectByName('');
-      projectList.value = projectRes.data || [];
-    } catch (error) {
-      console.error('加载项目信息失败:', error);
-    }
-  }
-
-  // 直接使用返回的 details 数据
-  if (res.data.details && res.data.details.length > 0) {
-    taskDetailList.value = res.data.details;
-  }
-
-  dialog.visible = true;
-  dialog.title = '修改文档质控任务';
-};
-
-/** 提交按钮 */
-const submitForm = () => {
-  taskFormRef.value?.validate(async (valid: boolean) => {
-    if (valid) {
-      buttonLoading.value = true;
-
-      // 构建提交数据
-      const submitData: any = {
-        ...form.value
-      };
-
-      // 仅修改时才包含 details
-      if (form.value.id && taskDetailList.value.length > 0) {
-        submitData.details = taskDetailList.value.map((item) => ({
-          id: item.id,
-          taskId: item.taskId || form.value.id,
-          documentId: item.documentId,
-          executor: item.executor,
-          projectId: form.value.projectId,
-          status: item.status,
-          note: item.note,
-          createDept: item.createDept || form.value.createDept,
-          createBy: item.createBy || form.value.createBy,
-          createTime: item.createTime || form.value.createTime,
-          updateBy: item.updateBy || form.value.updateBy,
-          updateTime: item.updateTime || form.value.updateTime,
-          params: item.params || {}
-        }));
-      }
-
-      if (form.value.id) {
-        await updateTask(submitData).finally(() => (buttonLoading.value = false));
-      } else {
-        await addTask(submitData).finally(() => (buttonLoading.value = false));
-      }
-      proxy?.$modal.msgSuccess('操作成功');
-      dialog.visible = false;
-    }
-  });
-};
-</script>

+ 151 - 9
src/views/Qc/task/list.vue

@@ -123,19 +123,66 @@
       <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
         v-model:limit="queryParams.pageSize" @pagination="getList" />
     </el-card>
+
+    <!-- 新增/编辑任务对话框 -->
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="800px" append-to-body @close="handleDialogClose">
+      <el-form ref="taskFormRef" :model="taskForm" :rules="taskRules" label-width="100px">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="任务名称" prop="name">
+              <el-input v-model="taskForm.name" placeholder="请输入任务名称" clearable />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="质控项目" prop="projectId">
+              <el-select v-model="taskForm.projectId" placeholder="请选择质控项目" clearable filterable style="width: 100%">
+                <el-option v-for="project in projectList" :key="project.id" :label="project.name"
+                  :value="project.id" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="截止日期" prop="deadline">
+              <el-date-picker v-model="taskForm.deadline" type="date" placeholder="请选择截止日期"
+                value-format="YYYY-MM-DD" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="抽检比例" prop="proportion">
+              <el-input-number v-model="taskForm.proportion" :min="1" :max="100" placeholder="请输入抽检比例"
+                style="width: 100%" />
+              <span style="margin-left: 8px; color: #909399">%</span>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="备注" prop="note">
+              <el-input v-model="taskForm.note" type="textarea" :rows="3" placeholder="请输入备注" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="dialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="submitForm" :loading="submitLoading">确定</el-button>
+        </div>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup name="TaskList" lang="ts">
-import { listTask, getTask, delTask, startTask } from '@/api/qc/task';
-import { TaskVO, TaskQuery } from '@/api/qc/task/types';
+import { listTask, getTask, delTask, startTask, addTask, updateTask } from '@/api/qc/task';
+import { TaskVO, TaskQuery, TaskForm } from '@/api/qc/task/types';
+import { listProject } from '@/api/document/folder';
+import { useUserStore } from '@/store/modules/user';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-
-const emit = defineEmits<{
-  add: [];
-  view: [taskId: string | number];
-}>();
+const userStore = useUserStore();
 
 const taskList = ref<TaskVO[]>([]);
 const loading = ref(true);
@@ -150,6 +197,33 @@ const dateRangeCreateTime = ref<[DateModelType, DateModelType]>(['', '']);
 const dateRangeUpdateTime = ref<[DateModelType, DateModelType]>(['', '']);
 
 const queryFormRef = ref<ElFormInstance>();
+const taskFormRef = ref<ElFormInstance>();
+
+// 对话框相关
+const dialogVisible = ref(false);
+const dialogTitle = ref('');
+const submitLoading = ref(false);
+const projectList = ref<any[]>([]);
+
+// 表单数据
+const taskForm = ref<TaskForm>({
+  id: undefined,
+  name: undefined,
+  initiator: undefined,
+  projectId: undefined,
+  proportion: undefined,
+  deadline: undefined,
+  status: 0,
+  note: undefined
+});
+
+// 表单验证规则
+const taskRules = {
+  name: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
+  projectId: [{ required: true, message: '请选择质控项目', trigger: 'change' }],
+  deadline: [{ required: true, message: '请选择截止日期', trigger: 'change' }],
+  proportion: [{ required: true, message: '请输入抽检比例', trigger: 'blur' }]
+};
 
 const data = reactive<PageData<any, TaskQuery>>({
   queryParams: {
@@ -209,12 +283,80 @@ const handleSelectionChange = (selection: TaskVO[]) => {
 
 /** 新增按钮操作 */
 const handleAdd = () => {
-  emit('add');
+  resetForm();
+  dialogTitle.value = '新增质控任务';
+  dialogVisible.value = true;
+  loadProjectList();
+};
+
+/** 加载项目列表 */
+const loadProjectList = async () => {
+  try {
+    const res = await listProject({ pageNum: 1, pageSize: 1000 });
+    projectList.value = res.rows || [];
+  } catch (error) {
+    console.error('加载项目列表失败:', error);
+  }
+};
+
+/** 重置表单 */
+const resetForm = () => {
+  taskForm.value = {
+    id: undefined,
+    name: undefined,
+    initiator: undefined,
+    projectId: undefined,
+    proportion: undefined,
+    deadline: undefined,
+    status: 0,
+    note: undefined
+  };
+  taskFormRef.value?.resetFields();
+};
+
+/** 对话框关闭 */
+const handleDialogClose = () => {
+  resetForm();
+};
+
+/** 提交表单 */
+const submitForm = async () => {
+  if (!taskFormRef.value) return;
+  
+  await taskFormRef.value.validate(async (valid) => {
+    if (valid) {
+      submitLoading.value = true;
+      try {
+        // 设置发起人为当前登录用户
+        const submitData = {
+          ...taskForm.value,
+          initiator: userStore.userId
+        };
+        
+        if (submitData.id) {
+          await updateTask(submitData);
+          proxy?.$modal.msgSuccess('修改成功');
+        } else {
+          await addTask(submitData);
+          proxy?.$modal.msgSuccess('新增成功');
+        }
+        dialogVisible.value = false;
+        await getList();
+      } catch (error) {
+        console.error('提交失败:', error);
+      } finally {
+        submitLoading.value = false;
+      }
+    }
+  });
 };
 
 /** 查看任务详情 */
 const handleView = (taskId: string | number) => {
-  emit('view', taskId);
+  proxy?.$router.push({
+    path: '/qc/task/detail',
+    query: { id: taskId }
+  });
 };
 
 /** 开始任务 */

+ 424 - 83
src/views/document/folder/document/DocumentList.vue

@@ -64,9 +64,9 @@
       </el-table-column>
       <el-table-column prop="url" :label="t('document.document.documentList.url')" min-width="200">
         <template #default="scope">
-          <div v-if="scope.row.fileName" class="file-name-cell">
-            <svg-icon :icon-class="getFileIconClass(scope.row.fileName)" class="file-icon" :size="18" />
-            <span class="file-name-text show-overflow-tooltip">{{ scope.row.fileName }}</span>
+          <div v-if="scope.row.actualDocumentName" class="file-name-cell">
+            <svg-icon :icon-class="getFileIconClass(scope.row.actualDocumentName)" class="file-icon" :size="18" />
+            <span class="file-name-text show-overflow-tooltip">{{ scope.row.actualDocumentName }}</span>
           </div>
           <span v-else>-</span>
         </template>
@@ -87,73 +87,96 @@
       <!-- 操作列 -->
       <el-table-column :label="t('document.document.documentList.action')" width="480" align="center" fixed="right">
         <template #default="scope">
-          <el-button
-            v-if="scope.row.url && scope.row.status === 1"
-            v-hasPermi="['document:document:audit']"
-            type="primary"
-            :icon="Select"
-            style="padding: 0 5px; font-size: 10px; height: 24px; --el-button-icon-span-gap: 2px"
-            @click="handleAuditClick(scope.row)"
-          >
-            {{ t('document.document.button.audit') }}
-          </el-button>
-          <el-button
-            v-if="(scope.row.status === 0 || scope.row.status === 2) && scope.row.planSubmitter === userStore.userId"
-            v-hasPermi="['document:document:submit']"
-            type="success"
-            :icon="Upload"
-            style="padding: 0 5px; font-size: 10px; height: 24px; --el-button-icon-span-gap: 2px"
-            @click="handleSubmit(scope.row)"
-          >
-            {{ t('document.document.button.submit') }}
-          </el-button>
-          <el-button
-            v-if="(scope.row.status === 0 || scope.row.status === 2) && scope.row.createBy === userStore.userId"
-            v-hasPermi="['document:document:confirmSubmit']"
-            type="danger"
-            icon="Delete"
-            style="padding: 0 5px; font-size: 10px; height: 24px; --el-button-icon-span-gap: 2px"
-            @click="handleConfirmSubmit(scope.row)"
-          >
-            {{ t('document.document.button.confirmSubmit') }}
-          </el-button>
-          <el-button
-            v-hasPermi="['document:document:mark']"
-            type="warning"
-            icon="Flag"
-            style="padding: 0 5px; font-size: 10px; height: 24px; --el-button-icon-span-gap: 2px"
-            @click="handleMark(scope.row)"
-          >
-            {{ t('document.document.button.mark') }}
-          </el-button>
-          <el-button
-            v-if="scope.row.ossId"
-            type="info"
-            icon="Download"
-            style="padding: 0 5px; font-size: 10px; height: 24px; --el-button-icon-span-gap: 2px"
-            @click="handleDownload(scope.row)"
-          >
-            {{ t('document.document.button.download') }}
-          </el-button>
-          <el-button
-            v-hasPermi="['document:document:logAudit']"
-            type="primary"
-            icon="DocumentCopy"
-            style="padding: 0 5px; font-size: 10px; height: 24px; --el-button-icon-span-gap: 2px"
-            @click="handleViewAuditLog(scope.row)"
-          >
-            {{ t('document.document.button.viewAuditLog') }}
-          </el-button>
-          <el-button
-            v-if="scope.row.status === 3"
-            v-hasPermi="['document:document:filing']"
-            type="success"
-            icon="UploadFilled"
-            style="padding: 0 5px; font-size: 10px; height: 24px; --el-button-icon-span-gap: 2px"
-            @click="handleArchive(scope.row)"
-          >
-            {{ t('document.document.button.archive') }}
-          </el-button>
+          <template v-if="scope.row.folderId === 0">
+            <!-- 临时文件夹只显示删除按钮 -->
+            <el-button
+              v-hasPermi="['document:document:specify']"
+              type="primary"
+              icon="Position"
+              style="padding: 0 5px; font-size: 10px; height: 24px; --el-button-icon-span-gap: 2px"
+              @click="handleSpecify(scope.row)"
+            >
+              {{ t('document.document.button.specify') }}
+            </el-button>
+            <el-button
+              v-hasPermi="['document:document:removeTemp']"
+              type="danger"
+              icon="Delete"
+              style="padding: 0 5px; font-size: 10px; height: 24px; --el-button-icon-span-gap: 2px"
+              @click="handleDeleteTemp(scope.row)"
+            >
+              {{ t('document.document.menu.delete') }}
+            </el-button>
+          </template>
+          <template v-else>
+            <el-button
+              v-if="scope.row.actualDocument && scope.row.status === 1 && scope.row.createBy === userStore.userId"
+              v-hasPermi="['document:document:audit']"
+              type="primary"
+              :icon="Select"
+              style="padding: 0 5px; font-size: 10px; height: 24px; --el-button-icon-span-gap: 2px"
+              @click="handleAuditClick(scope.row)"
+            >
+              {{ t('document.document.button.audit') }}
+            </el-button>
+            <el-button
+              v-if="(scope.row.status === 0 || scope.row.status === 2) && scope.row.planSubmitter === userStore.userId"
+              v-hasPermi="['document:document:submit']"
+              type="success"
+              :icon="Upload"
+              style="padding: 0 5px; font-size: 10px; height: 24px; --el-button-icon-span-gap: 2px"
+              @click="handleSubmit(scope.row)"
+            >
+              {{ t('document.document.button.submit') }}
+            </el-button>
+            <el-button
+              v-if="(scope.row.status === 0 || scope.row.status === 2) && scope.row.createBy === userStore.userId"
+              v-hasPermi="['document:document:confirmSubmit']"
+              type="danger"
+              icon="Delete"
+              style="padding: 0 5px; font-size: 10px; height: 24px; --el-button-icon-span-gap: 2px"
+              @click="handleConfirmSubmit(scope.row)"
+            >
+              {{ t('document.document.button.confirmSubmit') }}
+            </el-button>
+            <el-button
+              v-hasPermi="['document:document:mark']"
+              type="warning"
+              icon="Flag"
+              style="padding: 0 5px; font-size: 10px; height: 24px; --el-button-icon-span-gap: 2px"
+              @click="handleMark(scope.row)"
+            >
+              {{ t('document.document.button.mark') }}
+            </el-button>
+            <el-button
+              v-if="scope.row.actualDocument"
+              type="info"
+              icon="Download"
+              style="padding: 0 5px; font-size: 10px; height: 24px; --el-button-icon-span-gap: 2px"
+              @click="handleDownload(scope.row)"
+            >
+              {{ t('document.document.button.download') }}
+            </el-button>
+            <el-button
+              v-hasPermi="['document:document:logAudit']"
+              type="primary"
+              icon="DocumentCopy"
+              style="padding: 0 5px; font-size: 10px; height: 24px; --el-button-icon-span-gap: 2px"
+              @click="handleViewAuditLog(scope.row)"
+            >
+              {{ t('document.document.button.viewAuditLog') }}
+            </el-button>
+            <el-button
+              v-if="scope.row.status === 3"
+              v-hasPermi="['document:document:filing']"
+              type="success"
+              icon="UploadFilled"
+              style="padding: 0 5px; font-size: 10px; height: 24px; --el-button-icon-span-gap: 2px"
+              @click="handleArchive(scope.row)"
+            >
+              {{ t('document.document.button.archive') }}
+            </el-button>
+          </template>
         </template>
       </el-table-column>
     </el-table>
@@ -191,10 +214,19 @@
 
     <!-- 递交文档对话框 -->
     <el-dialog v-model="submitDialog.visible" :title="submitDialog.title" width="500px" append-to-body>
-      <el-alert title="仅支持上传 PDF 格式文件" type="info" :closable="false" style="margin-bottom: 20px" />
       <el-form ref="submitFormRef" :model="submitForm" :rules="submitRules" label-width="120px">
-        <el-form-item :label="t('document.document.submitForm.file')" prop="ossId">
-          <fileUpload v-model="submitForm.ossId" :limit="1" :action="'/common/resource/oss/upload'" accept=".pdf" />
+        <el-form-item :label="t('document.document.submitForm.file')" prop="actualDocument">
+          <fileUpload v-model="submitForm.actualDocument" :limit="1" :file-type="['pdf']" :is-show-tip="false" />
+          <div style="color: #909399; font-size: 12px; margin-top: 5px;">仅支持上传 PDF 格式文件,大小不超过 5MB</div>
+        </el-form-item>
+        <el-form-item :label="t('document.document.submitForm.effectiveDate')" prop="effectiveDate">
+          <el-date-picker
+            v-model="submitForm.effectiveDate"
+            type="date"
+            value-format="YYYY-MM-DD"
+            :placeholder="t('document.document.submitForm.effectiveDatePlaceholder')"
+            style="width: 100%"
+          />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -231,6 +263,104 @@
         <el-button type="primary" :loading="auditDialog.loading" @click="handleAuditConfirm">确定</el-button>
       </template>
     </el-dialog>
+
+    <!-- 指定文档对话框 -->
+    <el-dialog v-model="specifyDialog.visible" :title="t('document.document.dialog.specifyDocument')" width="900px" append-to-body>
+      <el-form ref="specifyFormRef" :model="specifyForm" :rules="specifyRules" label-width="120px">
+        <el-form-item :label="t('document.document.specifyForm.type')" prop="type">
+          <el-radio-group v-model="specifyForm.type">
+            <el-radio label="missing">{{ t('document.document.specifyForm.specifyMissing') }}</el-radio>
+            <el-radio label="folder">{{ t('document.document.specifyForm.specifyFolder') }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+
+      <!-- 指定递交缺失时显示文档列表 -->
+      <div v-if="specifyForm.type === 'missing'" style="margin-top: 20px;">
+        <!-- 搜索栏 -->
+        <el-form :inline="true" style="margin-bottom: 10px;">
+          <el-form-item>
+            <el-input
+              v-model="specifySearchName"
+              :placeholder="t('document.document.specifyForm.searchPlaceholder')"
+              clearable
+              style="width: 240px"
+              @keyup.enter="handleSpecifySearch"
+            />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" icon="Search" @click="handleSpecifySearch">{{ t('document.document.button.search') }}</el-button>
+            <el-button icon="Refresh" @click="handleSpecifyReset">{{ t('document.document.button.reset') }}</el-button>
+          </el-form-item>
+        </el-form>
+
+        <!-- 文档列表 -->
+        <el-table v-loading="specifyDocumentLoading" :data="specifyDocumentList" border style="width: 100%">
+          <el-table-column prop="name" :label="t('document.document.specifyForm.documentName')" min-width="150" show-overflow-tooltip />
+          <el-table-column prop="folder" :label="t('document.document.specifyForm.folder')" width="120" show-overflow-tooltip />
+          <el-table-column prop="status" :label="t('document.document.specifyForm.status')" width="100" align="center">
+            <template #default="scope">
+              <DocumentStatusTag :status="scope.row.status" />
+            </template>
+          </el-table-column>
+          <el-table-column prop="deadline" :label="t('document.document.specifyForm.deadline')" width="160" align="center">
+            <template #default="scope">
+              <span v-if="scope.row.deadline">{{ parseTime(scope.row.deadline) }}</span>
+              <span v-else>-</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="planSubmitter" :label="t('document.document.specifyForm.planSubmitter')" width="120" align="center" />
+          <el-table-column prop="createBy" :label="t('document.document.specifyForm.createBy')" width="120" align="center" />
+          <el-table-column prop="createTime" :label="t('document.document.specifyForm.createTime')" width="160" align="center">
+            <template #default="scope">
+              <span v-if="scope.row.createTime">{{ parseTime(scope.row.createTime) }}</span>
+              <span v-else>-</span>
+            </template>
+          </el-table-column>
+          <el-table-column :label="t('document.document.specifyForm.action')" width="100" align="center" fixed="right">
+            <template #default="scope">
+              <el-button
+                v-if="selectedDocumentId !== scope.row.id"
+                type="primary"
+                size="small"
+                @click="handleSelectDocument(scope.row.id)"
+              >
+                {{ t('document.document.specifyForm.select') }}
+              </el-button>
+              <el-tag v-else type="success">{{ t('document.document.specifyForm.selected') }}</el-tag>
+            </template>
+          </el-table-column>
+        </el-table>
+
+        <!-- 分页 -->
+        <pagination
+          v-show="specifyDocumentTotal > 0"
+          v-model:page="specifyQueryParams.pageNum"
+          v-model:limit="specifyQueryParams.pageSize"
+          :total="specifyDocumentTotal"
+          @pagination="getSpecifyDocumentList"
+        />
+      </div>
+
+      <!-- 指定文件夹时显示文件夹树 -->
+      <div v-else-if="specifyForm.type === 'folder'" style="margin-top: 20px;">
+        <div style="margin-bottom: 10px; color: #606266; font-size: 14px;">
+          请选择要指定的文件夹:
+        </div>
+        <FolderSelector
+          ref="folderSelectorRef"
+          :tree-data="treeData"
+          @change="handleFolderChange"
+        />
+      </div>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="specifyButtonLoading" type="primary" @click="submitSpecifyForm">{{ t('document.document.button.submit') }}</el-button>
+          <el-button @click="cancelSpecify">{{ t('document.document.button.cancel') }}</el-button>
+        </div>
+      </template>
+    </el-dialog>
   </div>
   <el-empty v-else :description="t('document.document.empty.description')"> </el-empty>
 </template>
@@ -238,8 +368,8 @@
 <script setup lang="ts">
 import { ref, reactive, nextTick, getCurrentInstance, computed, watch, toRefs } from 'vue';
 import { useI18n } from 'vue-i18n';
-import { listDocument, markDocument, submitDocument, confirmSubmit, filingDocument, downloadDocumentFile } from '@/api/document/document';
-import { DocumentQuery, DocumentVO, DocumentMarkForm, DocumentSubmitForm } from '@/api/document/document/types';
+import { listDocument, markDocument, submitDocument, confirmSubmit, filingDocument, downloadDocumentFile, removeTempDocument, listDocumentOnSpecify, specifyDocument } from '@/api/document/document';
+import { DocumentQuery, DocumentVO, DocumentMarkForm, DocumentSubmitForm, DocumentSpecifyVO, DocumentSpecifyForm } from '@/api/document/document/types';
 import { FolderListVO } from '@/api/document/folder/types';
 import { Select, Upload } from '@element-plus/icons-vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
@@ -252,6 +382,7 @@ import fileUpload from '@/components/FileUpload/index.vue';
 import AuditLogDialog from '@/components/AuditLogDialog/index.vue';
 import { listDocumentAuditLog, auditDocument } from '@/api/document/document';
 import DocumentStatusTag from '@/components/DocumentStatusTag/index.vue';
+import FolderSelector from '@/components/FolderSelector/index.vue';
 
 interface Props {
   selectedFolder: FolderListVO | null;
@@ -261,6 +392,10 @@ interface Props {
 
 const props = defineProps<Props>();
 
+const emit = defineEmits<{
+  refreshTree: [];
+}>();
+
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { t } = useI18n();
 const userStore = useUserStore();
@@ -314,16 +449,18 @@ const submitButtonLoading = ref(false);
 
 // 递交表单数据
 interface SubmitForm {
-  ossId: string;
+  actualDocument: string;
+  effectiveDate?: string;
 }
 
 const submitForm = ref<SubmitForm>({
-  ossId: ''
+  actualDocument: '',
+  effectiveDate: undefined
 });
 
 // 递交表单验证规则
 const submitRules = reactive({
-  ossId: [
+  actualDocument: [
     {
       required: true,
       message: t('document.document.submitRule.fileRequired'),
@@ -376,6 +513,54 @@ const auditRules = reactive({
   ]
 });
 
+// 指定文档对话框
+const specifyDialog = reactive({
+  visible: false,
+  title: ''
+});
+
+// 指定表单ref
+const specifyFormRef = ref<FormInstance>();
+const specifyButtonLoading = ref(false);
+
+// 指定表单数据
+const specifyForm = ref({
+  type: 'missing',
+  folderId: undefined as number | undefined
+});
+
+// 指定表单验证规则
+const specifyRules = reactive({
+  type: [
+    {
+      required: true,
+      message: t('document.document.specifyRule.typeRequired'),
+      trigger: 'change'
+    }
+  ]
+});
+
+// 指定文档列表数据
+const specifyDocumentList = ref<DocumentSpecifyVO[]>([]);
+const specifyDocumentLoading = ref(false);
+const specifyDocumentTotal = ref(0);
+const specifySearchName = ref('');
+const specifyQueryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  projectId: props.projectId,
+  name: ''
+});
+
+// 选中的文档ID
+const selectedDocumentId = ref<string | number | null>(null);
+
+// 文件夹选择器引用
+const folderSelectorRef = ref();
+
+// 选中的文件夹ID
+const selectedFolderId = ref<number | string | undefined>(undefined);
+
 /**
  * 根据 specificationType 获取对应的字典
  * @param specificationType 规格类型 (0: 中心文件, 1: 项目文件)
@@ -485,19 +670,20 @@ const handleAuditConfirm = async () => {
 
 // 下载文档
 const handleDownload = async (row: DocumentVO) => {
-  if (!row.ossId) {
+  if (!row.actualDocument) {
     ElMessage.warning(t('document.document.message.noFileToDownload'));
     return;
   }
 
-  await downloadDocumentFile(row.ossId, row.fileName || row.name);
+  await downloadDocumentFile(row.actualDocument, row.actualDocumentName || row.name);
 };
 
 // 递交文档
 const handleSubmit = (row: DocumentVO) => {
   currentDocument.value = row;
   submitForm.value = {
-    ossId: ''
+    actualDocument: '',
+    effectiveDate: undefined
   };
   submitDialog.visible = true;
   submitDialog.title = t('document.document.dialog.submitDocument');
@@ -510,7 +696,8 @@ const handleSubmit = (row: DocumentVO) => {
 const cancelSubmit = () => {
   submitDialog.visible = false;
   submitForm.value = {
-    ossId: ''
+    actualDocument: '',
+    effectiveDate: undefined
   };
   currentDocument.value = null;
 };
@@ -523,7 +710,8 @@ const submitSubmitForm = () => {
       try {
         const submitData: DocumentSubmitForm = {
           documentId: currentDocument.value.id,
-          ossId: submitForm.value.ossId
+          ossId: submitForm.value.actualDocument,
+          effectiveDate: submitForm.value.effectiveDate
         };
         await submitDocument(submitData);
         proxy?.$modal.msgSuccess(t('document.document.message.submitSuccess'));
@@ -573,6 +761,159 @@ const handleArchive = async (row: DocumentVO) => {
   }
 };
 
+// 删除临时文件夹中的文档
+const handleDeleteTemp = async (row: DocumentVO) => {
+  try {
+    await proxy?.$modal.confirm('确认要删除该临时文档吗?');
+    await removeTempDocument(row.id);
+    ElMessage.success('删除成功');
+    await getDocumentList();
+    // 触发父组件刷新树(更新临时文件夹气泡数量)
+    emit('refreshTree');
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('删除临时文档失败:', error);
+      ElMessage.error('删除临时文档失败');
+    }
+  }
+};
+
+// 指定文档
+const handleSpecify = (row: DocumentVO) => {
+  currentDocument.value = row;
+  specifyForm.value = {
+    type: 'missing',
+    folderId: undefined
+  };
+  selectedDocumentId.value = null;
+  selectedFolderId.value = undefined;
+  specifySearchName.value = '';
+  specifyQueryParams.pageNum = 1;
+  specifyQueryParams.name = '';
+  specifyQueryParams.projectId = props.projectId;
+  specifyDialog.visible = true;
+  nextTick(() => {
+    specifyFormRef.value?.clearValidate();
+    // 如果选择的是指定递交缺失,则加载列表
+    if (specifyForm.value.type === 'missing') {
+      getSpecifyDocumentList();
+    }
+  });
+};
+
+// 获取可指定的文档列表
+const getSpecifyDocumentList = async () => {
+  if (!props.projectId) return;
+  
+  specifyDocumentLoading.value = true;
+  try {
+    const res = await listDocumentOnSpecify(specifyQueryParams);
+    specifyDocumentList.value = res.rows || [];
+    specifyDocumentTotal.value = res.total || 0;
+  } catch (error) {
+    console.error('获取可指定文档列表失败:', error);
+    ElMessage.error('获取可指定文档列表失败');
+  } finally {
+    specifyDocumentLoading.value = false;
+  }
+};
+
+// 搜索指定文档
+const handleSpecifySearch = () => {
+  specifyQueryParams.name = specifySearchName.value;
+  specifyQueryParams.pageNum = 1;
+  getSpecifyDocumentList();
+};
+
+// 重置搜索
+const handleSpecifyReset = () => {
+  specifySearchName.value = '';
+  specifyQueryParams.name = '';
+  specifyQueryParams.pageNum = 1;
+  getSpecifyDocumentList();
+};
+
+// 选择文档
+const handleSelectDocument = (id: string | number) => {
+  selectedDocumentId.value = id;
+};
+
+// 监听指定类型变化
+watch(() => specifyForm.value.type, (newType) => {
+  selectedDocumentId.value = null;
+  selectedFolderId.value = undefined;
+  if (newType === 'missing' && specifyDialog.visible) {
+    getSpecifyDocumentList();
+  } else if (newType === 'folder' && folderSelectorRef.value) {
+    folderSelectorRef.value.clearSelection();
+  }
+});
+
+// 取消指定
+const cancelSpecify = () => {
+  specifyDialog.visible = false;
+  specifyForm.value = {
+    type: 'missing',
+    folderId: undefined
+  };
+  selectedDocumentId.value = null;
+  selectedFolderId.value = undefined;
+  specifyDocumentList.value = [];
+  currentDocument.value = null;
+};
+
+// 文件夹选择变化处理
+const handleFolderChange = (folderId: number | string | undefined) => {
+  selectedFolderId.value = folderId;
+};
+
+// 提交指定表单
+const submitSpecifyForm = () => {
+  specifyFormRef.value?.validate(async (valid: boolean) => {
+    if (valid && currentDocument.value) {
+      // 如果是指定递交缺失,需要选择一个文档
+      if (specifyForm.value.type === 'missing' && !selectedDocumentId.value) {
+        ElMessage.warning('请选择一个文档');
+        return;
+      }
+      
+      // 如果是指定文件夹,需要选择一个文件夹
+      if (specifyForm.value.type === 'folder' && !selectedFolderId.value) {
+        ElMessage.warning('请选择一个文件夹');
+        return;
+      }
+      
+      specifyButtonLoading.value = true;
+      try {
+        // 构建请求数据
+        const specifyData: DocumentSpecifyForm = {
+          documentId: currentDocument.value.id
+        };
+        
+        if (specifyForm.value.type === 'missing') {
+          // 指定递交缺失
+          specifyData.missingDocumentId = selectedDocumentId.value as number | string;
+        } else if (specifyForm.value.type === 'folder') {
+          // 指定文件夹
+          specifyData.folderId = selectedFolderId.value;
+        }
+        
+        await specifyDocument(specifyData);
+        ElMessage.success('指定成功');
+        specifyDialog.visible = false;
+        await getDocumentList();
+        // 触发父组件刷新树(更新临时文件夹气泡数量)
+        emit('refreshTree');
+      } catch (error) {
+        console.error('指定失败:', error);
+        ElMessage.error('指定失败');
+      } finally {
+        specifyButtonLoading.value = false;
+      }
+    }
+  });
+};
+
 // 标识文档
 const handleMark = (row: DocumentVO) => {
   currentDocument.value = row;

+ 31 - 2
src/views/document/folder/document/FolderTree.vue

@@ -16,6 +16,7 @@
               <Document v-else />
             </el-icon>
             <span class="node-label" @click="handleFolderClick(data)">{{ node.label }}</span>
+            <el-badge v-if="data.id === 0 && tempDocumentCount > 0" :value="tempDocumentCount" type="danger" class="temp-badge" />
             <span v-if="data.id !== 0" class="node-actions">
               <span class="menu-trigger" @click="toggleMenu($event, data)">
                 <el-icon>
@@ -108,14 +109,18 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted, onUnmounted, nextTick, getCurrentInstance, watch } from 'vue';
+import { ref, reactive, onMounted, onUnmounted, nextTick, getCurrentInstance, watch, computed } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { listFolder, addFolder, delFolder, getFolder, updateFolder } from '@/api/document/folder';
 import { FolderListVO, FolderForm } from '@/api/document/folder/types';
+import { scanUpload } from '@/api/document/scan';
+import { ScanUploadForm } from '@/api/document/scan/types';
+import { countTempDocuments } from '@/api/document/document';
 import { Folder, Document, Location, OfficeBuilding, MoreFilled, ArrowRight } from '@element-plus/icons-vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import type { FormInstance } from 'element-plus';
 import type { ComponentInternalInstance } from 'vue';
+import fileUpload from '@/components/FileUpload/index.vue';
 
 interface Props {
   projectId?: number | string;
@@ -137,6 +142,8 @@ const loading = ref(false);
 const buttonLoading = ref(false);
 const treeData = ref<FolderListVO[]>([]);
 const folderFormRef = ref<FormInstance>();
+const scanFormRef = ref<FormInstance>();
+const tempDocumentCount = ref(0);
 
 // 对话框
 const dialog = reactive({
@@ -212,6 +219,9 @@ const getList = async () => {
     };
     
     treeData.value = [...folders, tempFolder];
+    
+    // 获取临时文件夹文档数量
+    await getTempDocumentCount();
   } catch (error) {
     ElMessage.error(t('document.document.message.getFolderListFailed'));
     console.error(error);
@@ -220,6 +230,19 @@ const getList = async () => {
   }
 };
 
+// 获取临时文件夹文档数量
+const getTempDocumentCount = async () => {
+  if (!props.projectId) return;
+  
+  try {
+    const res = await countTempDocuments(props.projectId);
+    tempDocumentCount.value = res.data || 0;
+  } catch (error) {
+    console.error('获取临时文件夹数量失败:', error);
+    tempDocumentCount.value = 0;
+  }
+};
+
 // 表单重置
 const reset = () => {
   form.value = { ...initFormData };
@@ -491,7 +514,8 @@ const handleScroll = () => {
 // 暴露方法给父组件
 defineExpose({
   getList,
-  treeData
+  treeData,
+  getTempDocumentCount
 });
 
 // 初始化
@@ -555,6 +579,11 @@ onUnmounted(() => {
     }
   }
 
+  .temp-badge {
+    margin-left: 8px;
+    margin-right: 8px;
+  }
+
   .node-actions {
     display: none;
     position: relative;

+ 59 - 28
src/views/document/folder/document/index.vue

@@ -16,7 +16,7 @@
         <!-- 文档列表组件 -->
         <div class="content-container">
           <DocumentList ref="documentListRef" :selected-folder="selectedFolder" :tree-data="treeData"
-            :project-id="projectId" />
+            :project-id="projectId" @refresh-tree="handleTreeRefresh" />
         </div>
       </div>
     </el-card>
@@ -64,8 +64,14 @@
         </el-form-item>
 
         <el-form-item v-if="documentForm.type === 0" :label="t('document.document.documentForm.actualFile')"
-          prop="ossId">
-          <fileUpload v-model="uploadedFileId" :limit="1" />
+          prop="actualDocument">
+          <fileUpload v-model="uploadedFileId" :limit="1" :file-type="['pdf']" :is-show-tip="false" />
+          <div style="color: #909399; font-size: 12px; margin-top: 5px;">仅支持上传 PDF 格式文件,大小不超过 5MB</div>
+        </el-form-item>
+
+        <el-form-item v-if="documentForm.type === 0 && uploadedFileId" label="生效日期" prop="effectiveDate">
+          <el-date-picker v-model="documentForm.effectiveDate" type="date" value-format="YYYY-MM-DD"
+            placeholder="请选择生效日期" style="width: 100%" />
         </el-form-item>
 
         <el-form-item v-if="documentForm.submitTime" :label="t('document.document.documentForm.submitTime')">
@@ -84,7 +90,7 @@
       <template #footer>
         <div class="dialog-footer">
           <el-button :loading="documentButtonLoading" type="primary" @click="submitDocumentForm">{{
-            t('document.document.button.submit')
+            t('document.document.button.confirm')
           }}</el-button>
           <el-button @click="cancelDocument">{{ t('document.document.button.cancel') }}</el-button>
         </div>
@@ -94,8 +100,9 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, getCurrentInstance, watch, computed, toRefs } from 'vue';
+import { ref, reactive, getCurrentInstance, watch, computed, toRefs, onMounted } from 'vue';
 import { useI18n } from 'vue-i18n';
+import { useRoute, useRouter } from 'vue-router';
 import { addDocument } from '@/api/document/document';
 import { DocumentForm } from '@/api/document/document/types';
 import { FolderListVO } from '@/api/document/folder/types';
@@ -111,21 +118,22 @@ import fileUpload from '@/components/FileUpload/index.vue';
 import FolderTree from './FolderTree.vue';
 import DocumentList from './DocumentList.vue';
 
-interface Props {
-  projectId?: number | string;
-}
-
-const props = defineProps<Props>();
-
-const emit = defineEmits<{
-  back: [];
-}>();
-
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { t } = useI18n();
+const route = useRoute();
+const router = useRouter();
 const userStore = useUserStore();
 const { plan_document_type } = toRefs<any>(proxy?.useDict('plan_document_type'));
 
+// 从路由参数获取项目ID,并转换为数字类型
+const projectId = computed(() => {
+  const id = route.query.projectId;
+  if (!id) return undefined;
+  const numId = typeof id === 'string' ? parseInt(id) : id;
+  console.log('Document Index - projectId:', numId, 'from route.query:', route.query.projectId);
+  return numId;
+});
+
 // 组件引用
 const folderTreeRef = ref<InstanceType<typeof FolderTree>>();
 const documentListRef = ref<InstanceType<typeof DocumentList>>();
@@ -148,6 +156,7 @@ const documentButtonLoading = ref(false);
 // 检查是否有计划文档添加权限
 const hasAddPlanPermission = computed(() => checkPermi(['document:document:addPlan']));
 
+// 文档表单初始数据
 // 文档表单初始数据
 const initDocumentFormData: DocumentForm = {
   id: undefined,
@@ -157,14 +166,15 @@ const initDocumentFormData: DocumentForm = {
   folderId: undefined,
   submitDeadline: undefined,
   planType: undefined,
-  ossId: undefined,
+  actualDocument: undefined,
   submitTime: undefined,
-  projectId: props.projectId,
+  projectId: projectId.value,
   note: '',
-  sendFlag: false
+  sendFlag: false,
+  effectiveDate: undefined
 };
 
-// 文件上传的ossId
+// 文件上传的actualDocument
 const uploadedFileId = ref<string>('');
 
 // 文档表单数据
@@ -179,12 +189,12 @@ let submitterSearchTimer: NodeJS.Timeout | null = null;
 const documentRules = {
   name: [{ required: true, message: t('document.document.documentRule.nameRequired'), trigger: 'blur' }],
   planSubmitter: [{ required: true, message: t('document.document.documentRule.planSubmitterRequired'), trigger: 'change' }],
-  ossId: [{ required: true, message: t('document.document.documentRule.fileRequired'), trigger: 'change' }]
+  actualDocument: [{ required: true, message: t('document.document.documentRule.fileRequired'), trigger: 'change' }]
 };
 
 // 返回项目列表
 const handleBack = () => {
-  emit('back');
+  router.push('/document/folder');
 };
 
 // 处理文件夹点击
@@ -213,6 +223,8 @@ const handleTreeRefresh = () => {
   if (folderTreeRef.value) {
     // 可以通过ref访问子组件的数据
     treeData.value = (folderTreeRef.value as any).treeData || [];
+    // 刷新临时文件夹数量
+    (folderTreeRef.value as any).getTempDocumentCount?.();
   }
 };
 
@@ -232,7 +244,7 @@ const resetDocumentForm = () => {
     documentForm.value.planSubmitter = undefined;
     documentForm.value.submitter = undefined;
   }
-  documentForm.value.projectId = props.projectId;
+  documentForm.value.projectId = projectId.value;
   submitterOptions.value = [];
   documentFormRef.value?.resetFields();
 };
@@ -269,7 +281,7 @@ const searchSubmitters = async (query: string) => {
       const queryParams: MemberNotInCenterQuery = {
         pageNum: 1,
         pageSize: 10,
-        projectId: props.projectId || 0,
+        projectId: projectId.value || 0,
         folderId: 0,
         name: query
       };
@@ -289,7 +301,7 @@ watch(uploadedFileId, (newVal) => {
   if (newVal) {
     const ids = newVal.split(',').filter((id) => id.trim());
     if (ids.length > 0) {
-      documentForm.value.ossId = parseInt(ids[0]);
+      documentForm.value.actualDocument = parseInt(ids[0]);
       const now = new Date();
       documentForm.value.submitTime = proxy?.parseTime(now, '{y}-{m}-{d} {h}:{i}:{s}');
 
@@ -297,7 +309,7 @@ watch(uploadedFileId, (newVal) => {
       documentForm.value.submitter = userStore.userId;
     }
   } else {
-    documentForm.value.ossId = undefined;
+    documentForm.value.actualDocument = undefined;
     documentForm.value.submitTime = undefined;
     documentForm.value.submitter = undefined;
   }
@@ -328,12 +340,13 @@ const submitDocumentForm = () => {
           folderId: documentForm.value.folderId || 0,
           submitDeadline: documentForm.value.submitDeadline || '',
           planType: documentForm.value.planType || '',
-          ossId: hasUploadedFile ? uploadedFileId.value : null,
+          actualDocument: hasUploadedFile ? uploadedFileId.value : null,
           submitTime: documentForm.value.submitTime || (hasUploadedFile ? new Date().toISOString() : ''),
-          projectId: documentForm.value.projectId || props.projectId,
+          projectId: documentForm.value.projectId || projectId.value,
           status: hasUploadedFile ? 1 : 0,
           note: documentForm.value.note || '',
-          sendFlag: documentForm.value.sendFlag !== undefined ? documentForm.value.sendFlag : false
+          sendFlag: documentForm.value.sendFlag !== undefined ? documentForm.value.sendFlag : false,
+          effectiveDate: documentForm.value.effectiveDate || ''
         };
 
         await addDocument(submitData);
@@ -350,6 +363,16 @@ const submitDocumentForm = () => {
   });
 };
 
+// 监听路由参数变化
+watch(() => route.query.projectId, (newProjectId) => {
+  console.log('Route query projectId changed:', newProjectId);
+  if (newProjectId) {
+    // projectId 是 computed,会自动更新
+    // 重新加载数据
+    handleTreeRefresh();
+  }
+});
+
 // 初始化时更新树数据
 watch(
   () => folderTreeRef.value,
@@ -358,6 +381,14 @@ watch(
   },
   { immediate: true }
 );
+
+// 组件挂载时输出调试信息
+onMounted(() => {
+  console.log('Document Index mounted');
+  console.log('Route:', route);
+  console.log('Route query:', route.query);
+  console.log('ProjectId:', projectId.value);
+});
 </script>
 
 <style scoped lang="scss">

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

@@ -1,28 +0,0 @@
-<template>
-  <div class="folder-container">
-    <component :is="currentComponent" v-bind="componentProps" />
-  </div>
-</template>
-
-<script setup lang="ts">
-import { ref, shallowRef, provide } from 'vue';
-import ProjectComponent from './project.vue';
-
-const currentComponent = shallowRef(ProjectComponent);
-const componentProps = ref<any>({});
-
-// 提供组件切换方法
-const switchComponent = (component: any, props: any = {}) => {
-  currentComponent.value = component;
-  componentProps.value = props;
-};
-
-// 向子组件提供切换方法
-provide('switchComponent', switchComponent);
-</script>
-
-<style scoped>
-.folder-container {
-  height: 100%;
-}
-</style>

+ 9 - 21
src/views/document/folder/project.vue

@@ -1,11 +1,9 @@
 <template>
   <div>
-    <!-- 项目列表视图 -->
-    <div v-if="!showDocumentView">
-      <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">
+    <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"
@@ -147,10 +145,6 @@
 
         <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
       </el-card>
-    </div>
-
-    <!-- 文档管理视图 -->
-    <document-view v-else :project-id="currentProjectId" @back="handleBackToList" />
   </div>
 </template>
 
@@ -159,7 +153,6 @@ import { listProject } from '@/api/document/folder';
 import { ProjectVO, ProjectQuery } from '@/api/document/folder/types';
 import { useI18n } from 'vue-i18n';
 import { parseI18nName } from '@/utils/i18n';
-import DocumentView from './document/index.vue';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { t } = useI18n();
@@ -169,8 +162,6 @@ const projectList = ref<ProjectVO[]>([]);
 const loading = ref(true);
 const showSearch = ref(true);
 const total = ref(0);
-const showDocumentView = ref(false);
-const currentProjectId = ref<number | string>();
 const dateRangeStartTime = ref<[DateModelType, DateModelType]>(['', '']);
 const dateRangeEndTime = ref<[DateModelType, DateModelType]>(['', '']);
 const dateRangeCreateTime = ref<[DateModelType, DateModelType]>(['', '']);
@@ -221,14 +212,11 @@ const resetQuery = () => {
 
 /** 进入项目 */
 const handleEnterProject = (row: ProjectVO) => {
-  currentProjectId.value = row.id;
-  showDocumentView.value = true;
-};
-
-/** 返回项目列表 */
-const handleBackToList = () => {
-  showDocumentView.value = false;
-  currentProjectId.value = undefined;
+  console.log('Entering project with ID:', row.id);
+  proxy?.$router.push({
+    path: '/document/folder/document',
+    query: { projectId: row.id }
+  });
 };
 
 onMounted(() => {

+ 3 - 3
src/views/home/taskCenter/audit/index.vue

@@ -83,7 +83,7 @@
               审核
             </el-button>
             <el-button
-              v-if="scope.row.ossId"
+              v-if="scope.row.actualDocument"
               type="success"
               icon="Download"
               style="padding: 0 5px; font-size: 10px; height: 24px"
@@ -323,12 +323,12 @@ const handleAuditConfirm = async () => {
  * 下载文件
  */
 const handleDownload = async (row: AuditTaskVO) => {
-  if (!row.ossId) {
+  if (!row.actualDocument) {
     ElMessage.warning('暂无文件可下载');
     return;
   }
 
-  await downloadDocumentFile(row.ossId, row.name);
+  await downloadDocumentFile(row.actualDocument, row.actualDocumentName || row.name);
 };
 
 onMounted(() => {

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

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

+ 20 - 7
src/views/home/taskCenter/submission/index.vue

@@ -27,8 +27,7 @@
         <!--        </el-form-item>-->
         <el-form-item label="状态">
           <el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 120px">
-            <el-option label="未递交" :value="0" />
-            <el-option label="待审核" :value="1" />
+            <el-option label="待递交" :value="0" />
             <el-option label="审核拒绝" :value="2" />
           </el-select>
         </el-form-item>
@@ -129,6 +128,15 @@
         <el-form-item label="文件" prop="ossId">
           <fileUpload v-model="submitForm.ossId" :limit="1" :action="'/common/resource/oss/upload'" accept=".pdf" />
         </el-form-item>
+        <el-form-item label="生效日期" prop="effectiveDate">
+          <el-date-picker
+            v-model="submitForm.effectiveDate"
+            type="date"
+            value-format="YYYY-MM-DD"
+            placeholder="请选择生效日期"
+            style="width: 100%"
+          />
+        </el-form-item>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
@@ -191,12 +199,14 @@ const submitFormRef = ref<FormInstance>();
 
 // 递交表单数据
 const submitForm = ref({
-  ossId: ''
+  ossId: '',
+  effectiveDate: ''
 });
 
 // 递交表单验证规则
 const submitRules = {
-  ossId: [{ required: true, message: '请上传文件', trigger: 'change' }]
+  ossId: [{ required: true, message: '请上传文件', trigger: 'change' }],
+  effectiveDate: [{ required: true, message: '请选择生效日期', trigger: 'change' }]
 };
 
 // 审核记录对话框
@@ -337,7 +347,8 @@ const resetQuery = () => {
 const handleTaskSubmit = (row: SubmissionTaskVO) => {
   currentTask.value = row;
   submitForm.value = {
-    ossId: ''
+    ossId: '',
+    effectiveDate: ''
   };
   submitDialog.visible = true;
   submitDialog.title = '递交文档';
@@ -361,7 +372,8 @@ const handleViewAuditLog = (row: SubmissionTaskVO) => {
 const cancelSubmit = () => {
   submitDialog.visible = false;
   submitForm.value = {
-    ossId: ''
+    ossId: '',
+    effectiveDate: ''
   };
   currentTask.value = null;
 };
@@ -376,7 +388,8 @@ const submitSubmitForm = () => {
       try {
         const submitData: SubmissionTaskSubmitForm = {
           documentId: currentTask.value.id,
-          ossId: Number(submitForm.value.ossId)
+          ossId: Number(submitForm.value.ossId),
+          effectiveDate: submitForm.value.effectiveDate
         };
         await submitDocument(submitData);
         ElMessage.success('递交成功');

+ 7 - 9
src/views/project/management/detail/components/header.vue

@@ -9,13 +9,14 @@
 </template>
 
 <script setup lang="ts">
-import { inject } from 'vue';
 import { useI18n } from 'vue-i18n';
+import { useRouter } from 'vue-router';
 
 const { t } = useI18n();
+const router = useRouter();
 
 // Props
-defineProps<{
+const props = defineProps<{
   projectId?: string | number | null;
   projectName?: string;
 }>();
@@ -25,15 +26,12 @@ const emit = defineEmits<{
   back: [];
 }>();
 
-// 接收切换组件的方法
-const switchComponent = inject<any>('switchComponent');
-const ListComponent = inject<any>('ListComponent');
-
 // 返回列表
 const handleBack = () => {
-  if (switchComponent && ListComponent) {
-    switchComponent(ListComponent);
-  }
+  router.push({
+    path: '/project',
+    query: props.projectId ? { projectId: props.projectId } : {}
+  });
   emit('back');
 };
 </script>

+ 9 - 7
src/views/project/management/detail/components/sidebar.vue

@@ -39,22 +39,24 @@
 <script setup lang="ts">
 import { Document, OfficeBuilding, User, UserFilled, Avatar } from '@element-plus/icons-vue';
 import { useI18n } from 'vue-i18n';
+import { useRouter, useRoute } from 'vue-router';
 
 const { t } = useI18n();
+const router = useRouter();
+const route = useRoute();
 
 // Props
 defineProps<{
   activeMenu?: string;
 }>();
 
-// Emit
-const emit = defineEmits<{
-  menuSelect: [index: string];
-}>();
-
-// 菜单选择事件
+// 菜单选择事件 - 使用路由跳转
 const handleMenuSelect = (index: string) => {
-  emit('menuSelect', index);
+  // 保留当前的 query 参数(项目ID)
+  router.push({
+    path: `/project/management/detail/${index}`,
+    query: route.query
+  });
 };
 </script>
 

+ 30 - 35
src/views/project/management/detail/index.vue

@@ -6,13 +6,13 @@
     <!-- Main 主体区域 -->
     <div class="detail-main">
       <!-- Sidebar 侧边栏 -->
-      <DetailSidebar :active-menu="activeMenu" @menu-select="handleMenuSelect" />
+      <DetailSidebar :active-menu="activeMenu" />
 
       <!-- Content 内容区域 -->
       <div class="detail-content">
         <el-card shadow="never" class="content-card">
-          <!-- 动态加载页面组件 -->
-          <component :is="currentPageComponent" />
+          <!-- 使用 router-view 渲染子路由 -->
+          <router-view />
         </el-card>
       </div>
     </div>
@@ -20,28 +20,32 @@
 </template>
 
 <script setup lang="ts">
-import { ref, inject, computed, onMounted, provide } from 'vue';
+import { ref, computed, onMounted, provide, watch } from 'vue';
 import { useI18n } from 'vue-i18n';
+import { useRoute, useRouter } from 'vue-router';
 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 route = useRoute();
+const router = useRouter();
+
+// 从路由参数获取项目ID
+const projectId = ref<string | number | null>(route.query.id as string || null);
+
+// 当前激活的菜单(根据路由路径计算)
+const activeMenu = computed(() => {
+  const path = route.path;
+  if (path.includes('/basicInfo')) return 'basicInfo';
+  if (path.includes('/centerInfo')) return 'centerInfo';
+  if (path.includes('/projectMember')) return 'projectMember';
+  if (path.includes('/centerMember')) return 'centerMember';
+  return 'basicInfo';
+});
 
 // 页面加载状态
 const loading = ref(false);
@@ -49,29 +53,11 @@ 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) {
@@ -91,8 +77,17 @@ const getProjectDetail = async () => {
   }
 };
 
-// 通过 provide 将项目数据提供给子组件
+// 监听路由query变化,更新projectId
+watch(() => route.query.id, (newId) => {
+  if (newId) {
+    projectId.value = newId as string;
+    getProjectDetail();
+  }
+});
+
+// 通过 provide 将项目数据和项目ID提供给子组件
 provide('projectData', projectData);
+provide('projectId', projectId);
 provide('loading', loading);
 
 // 组件挂载时获取数据

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

@@ -1,37 +0,0 @@
-<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>

+ 27 - 7
src/views/project/management/list.vue

@@ -211,7 +211,7 @@
           align="center"
           fixed="right"
           class-name="small-padding fixed-width"
-          width="360"
+          width="440"
         >
           <template #default="scope">
             <el-button
@@ -250,6 +250,15 @@
             >
               {{ t('project.management.tooltip.updateStatus') }}
             </el-button>
+            <el-button
+              type="info"
+              icon="Download"
+              style="padding: 0 5px; font-size: 10px; height: 24px; --el-button-icon-span-gap: 2px"
+              @click="handleExportReport(scope.row)"
+              v-hasPermi="['project:management:export']"
+            >
+              {{ t('project.management.button.exportReport') }}
+            </el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -457,9 +466,9 @@ const getStatusType = (status: number | undefined): 'success' | 'info' | 'warnin
   return typeMap[status] || 'info';
 };
 
-// 注入父组件提供的方法和组件
-const switchComponent = inject<any>('switchComponent');
-const DetailComponent = inject<any>('DetailComponent');
+// 移除组件切换相关的注入(已改用路由跳转)
+// const switchComponent = inject<any>('switchComponent');
+// const DetailComponent = inject<any>('DetailComponent');
 
 const managementList = ref<ManagementVO[]>([]);
 const buttonLoading = ref(false);
@@ -661,9 +670,10 @@ const handleExport = () => {
 
 /** 查看详情按钮操作 */
 const handleViewDetail = (row: ManagementVO) => {
-  if (switchComponent && DetailComponent) {
-    switchComponent(DetailComponent, { projectId: row.id });
-  }
+  proxy?.$router.push({
+    path: '/project/management/detail/basicInfo',
+    query: { id: row.id }
+  });
 };
 
 /** 更新状态按钮操作 */
@@ -694,6 +704,16 @@ const submitStatusForm = () => {
   });
 };
 
+/** 导出项目报告 */
+const handleExportReport = (row: ManagementVO) => {
+  const timestamp = proxy?.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}');
+  proxy?.download(
+    `project/management/export/${row.id}`,
+    {},
+    `${row.name}-${timestamp}.xlsx`
+  );
+};
+
 onMounted(() => {
   getList();
 });

+ 8 - 8
src/views/search/index.vue

@@ -126,9 +126,9 @@
         </el-table-column>
         <el-table-column prop="fileName" :label="t('search.table.fileName')" min-width="200" show-overflow-tooltip>
           <template #default="scope">
-            <div v-if="scope.row.fileName" class="file-name-cell">
-              <svg-icon :icon-class="getFileIconClass(scope.row.fileName)" class="file-icon" :size="18" />
-              <span class="file-name-text">{{ scope.row.fileName }}</span>
+            <div v-if="scope.row.actualDocumentName" class="file-name-cell">
+              <svg-icon :icon-class="getFileIconClass(scope.row.actualDocumentName)" class="file-icon" :size="18" />
+              <span class="file-name-text">{{ scope.row.actualDocumentName }}</span>
             </div>
             <span v-else>-</span>
           </template>
@@ -150,11 +150,11 @@
         <el-table-column :label="t('search.table.action')" width="120" align="center" fixed="right">
           <template #default="scope">
             <el-button
-              v-if="scope.row.ossId"
+              v-if="scope.row.actualDocument"
               type="primary"
               icon="Download"
               style="padding: 0 5px; font-size: 10px; height: 24px; --el-button-icon-span-gap: 2px"
-              @click="handleDownload(scope.row.ossId, scope.row.fileName)"
+              @click="handleDownload(scope.row.actualDocument, scope.row.actualDocumentName)"
             >
               {{ t('search.button.download') }}
             </el-button>
@@ -317,9 +317,9 @@ const resetQuery = () => {
 /**
  * 下载文件
  */
-const handleDownload = async (ossId: number, fileName: string) => {
-  if (!ossId) return;
-  await downloadDocumentFile(ossId, fileName);
+const handleDownload = async (actualDocument: number, actualDocumentName: string) => {
+  if (!actualDocument) return;
+  await downloadDocumentFile(actualDocument, actualDocumentName);
 };
 
 onMounted(() => {