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

- 重新修改从新增计划文档到归档整个流程
- 修复了一些bug

Huanyi 2 месяцев назад
Родитель
Сommit
f847502bd4
26 измененных файлов с 2015 добавлено и 1242 удалено
  1. 26 0
      src/api/document/document/index.ts
  2. 25 0
      src/api/document/document/types.ts
  3. 4 4
      src/api/document/folder/index.ts
  4. 12 0
      src/api/search/index.ts
  5. 6 2
      src/lang/modules/search/en_US.ts
  6. 6 2
      src/lang/modules/search/zh_CN.ts
  7. 18 0
      src/utils/ruoyi.ts
  8. 0 5
      src/views/dashboard/audit/index.vue
  9. 0 5
      src/views/dashboard/dispute/index.vue
  10. 0 5
      src/views/dashboard/overView/index.vue
  11. 0 5
      src/views/dashboard/submission/index.vue
  12. 197 590
      src/views/document/folder/document/DocumentList.vue
  13. 130 283
      src/views/document/folder/document/FolderTree.vue
  14. 416 0
      src/views/document/folder/document/components/AddDocumentDialog.vue
  15. 140 0
      src/views/document/folder/document/components/AuditDialog.vue
  16. 277 0
      src/views/document/folder/document/components/FolderFormDialog.vue
  17. 172 0
      src/views/document/folder/document/components/MarkDialog.vue
  18. 376 0
      src/views/document/folder/document/components/SpecifyDialog.vue
  19. 127 0
      src/views/document/folder/document/components/SubmitDialog.vue
  20. 4 0
      src/views/document/folder/document/components/index.ts
  21. 5 334
      src/views/document/folder/document/index.vue
  22. 2 1
      src/views/home/taskCenter/audit/index.vue
  23. 2 1
      src/views/home/taskCenter/filing/index.vue
  24. 6 1
      src/views/home/taskCenter/qc/index.vue
  25. 2 1
      src/views/home/taskCenter/submission/index.vue
  26. 62 3
      src/views/search/index.vue

+ 26 - 0
src/api/document/document/index.ts

@@ -195,3 +195,29 @@ export const specifyDocument = (data: DocumentSpecifyForm) => {
     data: data
   });
 };
+
+/**
+ * 根据名称和文件夹查询成员列表
+ * @param query 查询参数
+ */
+export const getMemberByNameAndFolder = (query: {
+  name: string;
+  folder: number;
+  project: number | string;
+  pageNum: number;
+  pageSize: number
+}): AxiosPromise<{
+  total: number;
+  rows: Array<{
+    id: number;
+    name: string;
+    dept: string;
+    phoneNumber: string;
+  }>;
+}> => {
+  return request({
+    url: '/document/document/getMemberByNameAndFolder',
+    method: 'get',
+    params: query
+  });
+};

+ 25 - 0
src/api/document/document/types.ts

@@ -237,6 +237,31 @@ export interface DocumentMarkForm {
    * 文档标识类型
    */
   type: string;
+
+  /**
+   * 语言
+   */
+  language?: string;
+
+  /**
+   * 版本
+   */
+  version?: string;
+
+  /**
+   * 版本日期
+   */
+  versionDate?: string;
+
+  /**
+   * 伦理递交日期
+   */
+  ethicsSubmissionDate?: string;
+
+  /**
+   * 伦理批准日期
+   */
+  ethicsApprovalDate?: string;
 }
 
 /**

+ 4 - 4
src/api/document/folder/index.ts

@@ -88,13 +88,13 @@ export const listFolderOnProject = (projectId: string | number): AxiosPromise<Fo
 };
 
 /**
- * 根据中心ID获取项目名称
+ * 根据中心ID获取项目编码
  * @param centerId 中心ID
  */
-export const getNameByCenterId = (centerId: number): AxiosPromise<{ projectName: string; centerId: number }> => {
+export const getNameByProjectId = (projectId: number | string): AxiosPromise<{ projectCode: string }> => {
   return request({
-    url: '/document/folder/getName',
+    url: '/project/management/getName',
     method: 'get',
-    params: { centerId }
+    params: { projectId }
   });
 };

+ 12 - 0
src/api/search/index.ts

@@ -10,3 +10,15 @@ export function listSearchDocuments(query: any) {
     params: query
   });
 }
+
+/**
+ * 导出搜索结果
+ */
+export function exportSearchDocuments(data: any) {
+  return request({
+    url: '/search/export',
+    method: 'post',
+    data: data,
+    responseType: 'blob'
+  });
+}

+ 6 - 2
src/lang/modules/search/en_US.ts

@@ -1,6 +1,7 @@
 // Search Module - English Translation
 export default {
   title: 'Document Search',
+  exportFileName: 'Document Tracker',
   search: {
     name: 'Document Name',
     namePlaceholder: 'Please enter document name',
@@ -20,7 +21,8 @@ export default {
   button: {
     search: 'Search',
     reset: 'Reset',
-    download: 'Download'
+    download: 'Download',
+    export: 'Export'
   },
   table: {
     id: 'No.',
@@ -54,6 +56,8 @@ export default {
     qualityControlReject: 'Quality Control Reject'
   },
   message: {
-    fetchFailed: 'Failed to get document list'
+    fetchFailed: 'Failed to get document list',
+    exportSuccess: 'Export successful',
+    exportFailed: 'Export failed'
   }
 };

+ 6 - 2
src/lang/modules/search/zh_CN.ts

@@ -1,6 +1,7 @@
 // 搜索模块 - 中文翻译
 export default {
   title: '文档搜索',
+  exportFileName: '文件追踪表',
   search: {
     name: '文档名称',
     namePlaceholder: '请输入文档名称',
@@ -20,7 +21,8 @@ export default {
   button: {
     search: '搜索',
     reset: '重置',
-    download: '下载'
+    download: '下载',
+    export: '导出'
   },
   table: {
     id: '序号',
@@ -54,6 +56,8 @@ export default {
     qualityControlReject: '质控拒绝'
   },
   message: {
-    fetchFailed: '获取文档列表失败'
+    fetchFailed: '获取文档列表失败',
+    exportSuccess: '导出成功',
+    exportFailed: '导出失败'
   }
 };

+ 18 - 0
src/utils/ruoyi.ts

@@ -231,6 +231,24 @@ export const blobValidate = (data: any) => {
   return data.type !== 'application/json';
 };
 
+/**
+ * 处理文档名称,去除其中的 "NA"
+ * @param name 原始文件名称
+ * @returns 处理后的文件名称
+ */
+export const formatDocumentName = (name: string | undefined): string => {
+  if (!name) return '';
+
+  // 通过 "-" 拆分
+  const parts = name.split('-');
+
+  // 过滤掉 "NA"
+  const filteredParts = parts.filter(part => part !== 'NA');
+
+  // 用 "-" 重新合并
+  return filteredParts.join('-');
+};
+
 export default {
   handleTree
 };

+ 0 - 5
src/views/dashboard/audit/index.vue

@@ -1,5 +0,0 @@
-<script setup lang="ts"></script>
-
-<template></template>
-
-<style scoped lang="scss"></style>

+ 0 - 5
src/views/dashboard/dispute/index.vue

@@ -1,5 +0,0 @@
-<script setup lang="ts"></script>
-
-<template></template>
-
-<style scoped lang="scss"></style>

+ 0 - 5
src/views/dashboard/overView/index.vue

@@ -1,5 +0,0 @@
-<script setup lang="ts"></script>
-
-<template></template>
-
-<style scoped lang="scss"></style>

+ 0 - 5
src/views/dashboard/submission/index.vue

@@ -1,5 +0,0 @@
-<script setup lang="ts"></script>
-
-<template></template>
-
-<style scoped lang="scss"></style>

+ 197 - 590
src/views/document/folder/document/DocumentList.vue

@@ -3,13 +3,18 @@
     <!-- 搜索栏 -->
     <el-form :model="documentQueryParams" :inline="true" class="search-form">
       <el-form-item :label="t('document.document.documentList.fileName')">
-        <el-input v-model="documentQueryParams.name"
-          :placeholder="t('document.document.documentList.fileNamePlaceholder')" clearable style="width: 240px"
-          @keyup.enter="handleDocumentQuery" />
+        <el-input
+          v-model="documentQueryParams.name"
+          :placeholder="t('document.document.documentList.fileNamePlaceholder')"
+          clearable
+          style="width: 240px"
+          @keyup.enter="handleDocumentQuery"
+        />
       </el-form-item>
       <el-form-item>
-        <el-button type="primary" icon="Search" @click="handleDocumentQuery">{{ t('document.document.button.search')
-          }}</el-button>
+        <el-button type="primary" icon="Search" @click="handleDocumentQuery">
+          {{ t('document.document.button.search') }}
+        </el-button>
         <el-button icon="Refresh" @click="handleDocumentReset">{{ t('document.document.button.reset') }}</el-button>
       </el-form-item>
     </el-form>
@@ -21,18 +26,38 @@
           {{ scope.row.id }}
         </template>
       </el-table-column>
-      <el-table-column prop="name" :label="t('document.document.documentList.name')" min-width="150"
-        show-overflow-tooltip />
-      <el-table-column prop="specification" :label="t('document.document.documentList.specification')" min-width="120"
-        show-overflow-tooltip>
+      <el-table-column
+        prop="name"
+        :label="t('document.document.documentList.name')"
+        min-width="150"
+        show-overflow-tooltip
+      >
         <template #default="scope">
-          <dict-tag v-if="scope.row.specification" :options="getSpecificationDict(scope.row.specificationType)"
-            :value="scope.row.specification" />
+          {{ formatDocumentName(scope.row.name) }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        prop="specification"
+        :label="t('document.document.documentList.specification')"
+        min-width="120"
+        show-overflow-tooltip
+      >
+        <template #default="scope">
+          <dict-tag
+            v-if="scope.row.specification"
+            :options="getSpecificationDict(scope.row.specificationType)"
+            :value="scope.row.specification"
+          />
           <span v-else>-</span>
         </template>
       </el-table-column>
-      <el-table-column prop="planType" :label="t('document.document.documentList.planDocumentType')" min-width="120"
-        align="center" show-overflow-tooltip>
+      <el-table-column
+        prop="planType"
+        :label="t('document.document.documentList.planDocumentType')"
+        min-width="120"
+        align="center"
+        show-overflow-tooltip
+      >
         <template #default="scope">
           <dict-tag v-if="scope.row.planType" :options="plan_document_type" :value="scope.row.planType" />
           <span v-else>-</span>
@@ -43,19 +68,37 @@
           <DocumentStatusTag :status="scope.row.status" />
         </template>
       </el-table-column>
-      <el-table-column prop="planSubmitterName" :label="t('document.document.documentList.planSubmitter')" width="120"
-        align="center" />
-      <el-table-column prop="submitterName" :label="t('document.document.documentList.submitter')" width="120"
-        align="center" />
-      <el-table-column prop="submitDeadline" :label="t('document.document.documentList.submitDeadline')" width="110"
-        align="center">
+      <el-table-column
+        prop="planSubmitterName"
+        :label="t('document.document.documentList.planSubmitter')"
+        width="120"
+        align="center"
+        show-overflow-tooltip
+      />
+      <el-table-column
+        prop="submitterName"
+        :label="t('document.document.documentList.submitter')"
+        width="120"
+        align="center"
+        show-overflow-tooltip
+      />
+      <el-table-column
+        prop="submitDeadline"
+        :label="t('document.document.documentList.submitDeadline')"
+        width="110"
+        align="center"
+      >
         <template #default="scope">
           <span v-if="scope.row.submitDeadline">{{ parseTime(scope.row.submitDeadline, '{y}-{m}-{d}') }}</span>
           <span v-else>-</span>
         </template>
       </el-table-column>
-      <el-table-column prop="submitTime" :label="t('document.document.documentList.submitTime')" width="160"
-        align="center">
+      <el-table-column
+        prop="submitTime"
+        :label="t('document.document.documentList.submitTime')"
+        width="160"
+        align="center"
+      >
         <template #default="scope">
           <span v-if="scope.row.submitTime">{{ parseTime(scope.row.submitTime) }}</span>
           <span v-else>-</span>
@@ -70,17 +113,29 @@
           <span v-else>-</span>
         </template>
       </el-table-column>
-      <el-table-column prop="note" :label="t('document.document.documentList.note')" min-width="150"
-        show-overflow-tooltip />
-      <el-table-column prop="createTime" :label="t('document.document.documentList.createTime')" width="160"
-        align="center">
+      <el-table-column
+        prop="note"
+        :label="t('document.document.documentList.note')"
+        min-width="150"
+        show-overflow-tooltip
+      />
+      <el-table-column
+        prop="createTime"
+        :label="t('document.document.documentList.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 prop="updateTime" :label="t('document.document.documentList.updateTime')" width="160"
-        align="center">
+      <el-table-column
+        prop="updateTime"
+        :label="t('document.document.documentList.updateTime')"
+        width="160"
+        align="center"
+      >
         <template #default="scope">
           <span v-if="scope.row.updateTime">{{ parseTime(scope.row.updateTime) }}</span>
           <span v-else>-</span>
@@ -91,55 +146,92 @@
         <template #default="scope">
           <template v-if="scope.row.folderId === 0">
             <!-- 临时文件夹只显示删除按钮 -->
-            <el-button v-hasPermi="['document:document:specify']" type="primary" icon="Position"
+            <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)">
+              @click="handleSpecify(scope.row)"
+            >
               {{ t('document.document.button.specify') }}
             </el-button>
-            <el-button v-hasPermi="['document:document:removeTemp']" type="danger" icon="Delete"
+            <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)">
+              @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"
-              v-hasPermi="['document:document:audit']" type="primary" :icon="Select"
+            <el-button
+              v-if="scope.row.actualDocument && 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)">
+              @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"
+              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)">
+              @click="handleSubmit(scope.row)"
+            >
               {{ t('document.document.button.submit') }}
             </el-button>
-            <el-button v-if="(scope.row.status === 0 || scope.row.status === 2)"
-              v-hasPermi="['document:document:confirmSubmit']" type="danger" icon="Delete"
+            <el-button
+              v-if="scope.row.status === 0 || scope.row.status === 2"
+              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)">
+              @click="handleConfirmSubmit(scope.row)"
+            >
               {{ t('document.document.button.confirmSubmit') }}
             </el-button>
-            <el-button v-hasPermi="['document:document:mark']" type="warning" icon="Flag"
+            <el-button
+              v-if="!scope.row.specification"
+              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)">
+              @click="handleMark(scope.row)"
+            >
               {{ t('document.document.button.mark') }}
             </el-button>
-            <el-button v-if="scope.row.actualDocument" type="info" icon="Download"
+            <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)">
+              @click="handleDownload(scope.row)"
+            >
               {{ t('document.document.button.download') }}
             </el-button>
-            <el-button v-hasPermi="['document:document:logAudit']" type="primary" icon="DocumentCopy"
+            <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)">
+              @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)">
+            <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>
@@ -148,186 +240,66 @@
     </el-table>
 
     <!-- 分页 -->
-    <pagination v-show="documentTotal > 0" v-model:page="documentQueryParams.pageNum"
-      v-model:limit="documentQueryParams.pageSize" :total="documentTotal" @pagination="getDocumentList" />
+    <pagination
+      v-show="documentTotal > 0"
+      v-model:page="documentQueryParams.pageNum"
+      v-model:limit="documentQueryParams.pageSize"
+      :total="documentTotal"
+      @pagination="getDocumentList"
+    />
 
     <!-- 标识文档对话框 -->
-    <el-dialog v-model="markDialog.visible" :title="markDialog.title" width="500px" append-to-body>
-      <el-form ref="markFormRef" :model="markForm" :rules="markRules" label-width="120px">
-        <el-form-item :label="t('document.document.markForm.specification')" prop="type">
-          <el-select v-model="markForm.type" :placeholder="t('document.document.markForm.specificationPlaceholder')"
-            clearable style="width: 100%">
-            <el-option v-for="dict in getSpecificationDict(currentDocument?.specificationType)" :key="dict.value"
-              :label="parseI18nName(dict.label)" :value="dict.value" />
-          </el-select>
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button :loading="markButtonLoading" type="primary" @click="submitMarkForm">{{
-            t('document.document.button.submit') }}</el-button>
-          <el-button @click="cancelMark">{{ t('document.document.button.cancel') }}</el-button>
-        </div>
-      </template>
-    </el-dialog>
+    <MarkDialog
+      v-model="markDialog.visible"
+      :document="currentDocument"
+      :specification-dict="getSpecificationDict(currentDocument?.specificationType)"
+      @success="handleDialogSuccess"
+    />
 
     <!-- 递交文档对话框 -->
-    <el-dialog v-model="submitDialog.visible" :title="submitDialog.title" width="500px" append-to-body>
-      <el-form ref="submitFormRef" :model="submitForm" :rules="submitRules" label-width="120px">
-        <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>
-        <div class="dialog-footer">
-          <el-button :loading="submitButtonLoading" type="primary" @click="submitSubmitForm">{{
-            t('document.document.button.submit') }}</el-button>
-          <el-button @click="cancelSubmit">{{ t('document.document.button.cancel') }}</el-button>
-        </div>
-      </template>
-    </el-dialog>
-
-    <!-- 文档审核记录对话框 -->
-    <AuditLogDialog v-model:visible="auditLogDialog.visible" :document-id="auditLogDialog.documentId"
-      :api-function="listDocumentAuditLog" i18n-prefix="document.document" />
+    <SubmitDialog v-model="submitDialog.visible" :document="currentDocument" @success="handleDialogSuccess" />
 
     <!-- 审核文档对话框 -->
-    <el-dialog v-model="auditDialog.visible" :title="t('document.document.dialog.auditDocument')" width="500px"
-      @close="handleAuditDialogClose">
-      <el-form ref="auditFormRef" :model="auditForm" :rules="auditRules" label-width="100px">
-        <el-form-item label="审核结果" prop="result">
-          <el-radio-group v-model="auditForm.result">
-            <el-radio :label="3">通过</el-radio>
-            <el-radio :label="2">驳回</el-radio>
-          </el-radio-group>
-        </el-form-item>
-        <el-form-item v-if="auditForm.result === 2" label="驳回理由" prop="rejectReason">
-          <el-input v-model="auditForm.rejectReason" type="textarea" :rows="4" placeholder="请输入驳回理由" maxlength="500"
-            show-word-limit />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="auditDialog.visible = false">取消</el-button>
-        <el-button type="primary" :loading="auditDialog.loading" @click="handleAuditConfirm">确定</el-button>
-      </template>
-    </el-dialog>
+    <AuditDialog v-model="auditDialog.visible" :document="currentDocument" @success="handleDialogSuccess" />
 
     <!-- 指定文档对话框 -->
-    <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>
+    <SpecifyDialog
+      v-model="specifyDialog.visible"
+      :document="currentDocument"
+      :tree-data="treeData"
+      :project-id="projectId"
+      @success="handleSpecifySuccess"
+    />
 
-        <!-- 分页 -->
-        <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>
+    <!-- 文档审核记录对话框 -->
+    <AuditLogDialog
+      v-model:visible="auditLogDialog.visible"
+      :document-id="auditLogDialog.documentId"
+      :api-function="listDocumentAuditLog"
+      i18n-prefix="document.document"
+    />
   </div>
   <el-empty v-else :description="t('document.document.empty.description')"> </el-empty>
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, nextTick, getCurrentInstance, computed, watch, toRefs } from 'vue';
+import { ref, reactive, getCurrentInstance, watch, toRefs } from 'vue';
 import { useI18n } from 'vue-i18n';
-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 { listDocument, confirmSubmit, filingDocument, downloadDocumentFile, removeTempDocument } from '@/api/document/document';
+import { DocumentQuery, DocumentVO } 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';
-import type { FormInstance } from 'element-plus';
 import type { ComponentInternalInstance } from 'vue';
 import { useUserStore } from '@/store/modules/user';
-import { parseTime } from '@/utils/ruoyi';
-import { parseI18nName } from '@/utils/i18n';
-import fileUpload from '@/components/FileUpload/index.vue';
+import { parseTime, formatDocumentName } from '@/utils/ruoyi';
 import AuditLogDialog from '@/components/AuditLogDialog/index.vue';
-import { listDocumentAuditLog, auditDocument } from '@/api/document/document';
+import { listDocumentAuditLog } from '@/api/document/document';
 import DocumentStatusTag from '@/components/DocumentStatusTag/index.vue';
-import FolderSelector from '@/components/FolderSelector/index.vue';
+import MarkDialog from './components/MarkDialog.vue';
+import SubmitDialog from './components/SubmitDialog.vue';
+import AuditDialog from './components/AuditDialog.vue';
+import SpecifyDialog from './components/SpecifyDialog.vue';
 
 interface Props {
   selectedFolder: FolderListVO | null;
@@ -361,151 +333,35 @@ const documentQueryParams = reactive<DocumentQuery>({
   folderId: undefined
 });
 
+// 当前操作的文档
+const currentDocument = ref<DocumentVO | null>(null);
+
 // 标识文档对话框
 const markDialog = reactive({
-  visible: false,
-  title: ''
+  visible: false
 });
 
-// 标识表单ref
-const markFormRef = ref<FormInstance>();
-const markButtonLoading = ref(false);
-
-// 标识表单数据
-const markForm = ref<DocumentMarkForm>({
-  id: 0,
-  type: ''
-});
-
-// 标识表单验证规则
-const markRules = {
-  type: [{ required: true, message: t('document.document.markRule.typeRequired'), trigger: 'change' }]
-};
-
 // 递交文档对话框
 const submitDialog = reactive({
-  visible: false,
-  title: ''
+  visible: false
 });
 
-// 递交表单ref
-const submitFormRef = ref<FormInstance>();
-const submitButtonLoading = ref(false);
-
-// 递交表单数据
-interface SubmitForm {
-  actualDocument: string;
-  effectiveDate?: string;
-}
-
-const submitForm = ref<SubmitForm>({
-  actualDocument: '',
-  effectiveDate: undefined
+// 审核对话框
+const auditDialog = reactive({
+  visible: false
 });
 
-// 递交表单验证规则
-const submitRules = reactive({
-  actualDocument: [
-    {
-      required: true,
-      message: t('document.document.submitRule.fileRequired'),
-      trigger: 'blur'
-    }
-  ]
+// 指定文档对话框
+const specifyDialog = reactive({
+  visible: false
 });
 
-// 当前操作的文档
-const currentDocument = ref<DocumentVO | null>(null);
-
 // 审核记录对话框
 const auditLogDialog = reactive({
   visible: false,
   documentId: ''
 });
 
-// 审核对话框状态
-const auditDialog = reactive({
-  visible: false,
-  document: null as DocumentVO | null,
-  loading: false
-});
-
-// 审核表单数据
-const auditForm = ref({
-  documentId: 0,
-  result: 3, // 默认通过
-  rejectReason: ''
-});
-
-// 审核表单引用
-const auditFormRef = ref<ElFormInstance>();
-
-// 审核表单验证规则
-const auditRules = reactive({
-  result: [
-    {
-      required: true,
-      message: '请选择审核结果',
-      trigger: 'change'
-    }
-  ],
-  rejectReason: [
-    {
-      required: true,
-      message: '请输入驳回理由',
-      trigger: 'blur'
-    }
-  ]
-});
-
-// 指定文档对话框
-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: 项目文件)
@@ -516,7 +372,6 @@ const getSpecificationDict = (specificationType: number | undefined) => {
   } else if (specificationType === 1) {
     return project_file_specification?.value || [];
   }
-  // 默认返回空数组
   return [];
 };
 
@@ -560,57 +415,8 @@ const handleViewAuditLog = (row: DocumentVO) => {
 
 // 审核按钮点击事件
 const handleAuditClick = (row: DocumentVO) => {
-  auditDialog.document = row;
-  auditForm.value = {
-    documentId: row.id as number,
-    result: 3, // 默认通过
-    rejectReason: ''
-  };
+  currentDocument.value = row;
   auditDialog.visible = true;
-  nextTick(() => {
-    auditFormRef.value?.clearValidate();
-  });
-};
-
-// 审核对话框关闭事件
-const handleAuditDialogClose = () => {
-  auditFormRef.value?.resetFields();
-  auditForm.value = {
-    documentId: 0,
-    result: 3,
-    rejectReason: ''
-  };
-};
-
-// 处理审核确认
-const handleAuditConfirm = async () => {
-  // 如果选择驳回,需要验证驳回理由
-  if (auditForm.value.result === 2) {
-    try {
-      await auditFormRef.value?.validate();
-    } catch (error) {
-      return;
-    }
-  }
-
-  auditDialog.loading = true;
-  try {
-    const submitData = {
-      documentId: auditForm.value.documentId,
-      result: auditForm.value.result,
-      rejectReason: auditForm.value.result === 2 ? auditForm.value.rejectReason : undefined
-    };
-
-    await auditDocument(submitData);
-    ElMessage.success(t('document.document.message.auditSuccess'));
-    auditDialog.visible = false;
-    await getDocumentList();
-  } catch (error) {
-    console.error('审核失败:', error);
-    ElMessage.error(t('document.document.message.auditFailed'));
-  } finally {
-    auditDialog.loading = false;
-  }
 };
 
 // 下载文档
@@ -626,49 +432,7 @@ const handleDownload = async (row: DocumentVO) => {
 // 递交文档
 const handleSubmit = (row: DocumentVO) => {
   currentDocument.value = row;
-  submitForm.value = {
-    actualDocument: '',
-    effectiveDate: undefined
-  };
   submitDialog.visible = true;
-  submitDialog.title = t('document.document.dialog.submitDocument');
-  nextTick(() => {
-    submitFormRef.value?.clearValidate();
-  });
-};
-
-// 取消递交
-const cancelSubmit = () => {
-  submitDialog.visible = false;
-  submitForm.value = {
-    actualDocument: '',
-    effectiveDate: undefined
-  };
-  currentDocument.value = null;
-};
-
-// 提交递交表单
-const submitSubmitForm = () => {
-  submitFormRef.value?.validate(async (valid: boolean) => {
-    if (valid && currentDocument.value) {
-      submitButtonLoading.value = true;
-      try {
-        const submitData: DocumentSubmitForm = {
-          documentId: currentDocument.value.id,
-          ossId: submitForm.value.actualDocument,
-          effectiveDate: submitForm.value.effectiveDate
-        };
-        await submitDocument(submitData);
-        proxy?.$modal.msgSuccess(t('document.document.message.submitSuccess'));
-        submitDialog.visible = false;
-        await getDocumentList();
-      } catch (error) {
-        console.error(t('document.document.message.submitFailed'), error);
-      } finally {
-        submitButtonLoading.value = false;
-      }
-    }
-  });
 };
 
 // 确认递交文档
@@ -713,7 +477,6 @@ const handleDeleteTemp = async (row: DocumentVO) => {
     await removeTempDocument(row.id);
     ElMessage.success('删除成功');
     await getDocumentList();
-    // 触发父组件刷新树(更新临时文件夹气泡数量)
     emit('refreshTree');
   } catch (error) {
     if (error !== 'cancel') {
@@ -726,180 +489,24 @@ const handleDeleteTemp = async (row: DocumentVO) => {
 // 指定文档
 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;
-  markForm.value = {
-    id: row.id,
-    type: ''
-  };
   markDialog.visible = true;
-  markDialog.title = t('document.document.dialog.markDocument');
-  nextTick(() => {
-    markFormRef.value?.clearValidate();
-  });
 };
 
-// 取消标识
-const cancelMark = () => {
-  markDialog.visible = false;
-  markForm.value = {
-    id: 0,
-    type: ''
-  };
-  currentDocument.value = null;
+// 对话框成功回调
+const handleDialogSuccess = async () => {
+  await getDocumentList();
 };
 
-// 提交标识表单
-const submitMarkForm = () => {
-  markFormRef.value?.validate(async (valid: boolean) => {
-    if (valid) {
-      markButtonLoading.value = true;
-      try {
-        await markDocument(markForm.value);
-        proxy?.$modal.msgSuccess(t('document.document.message.markSuccess'));
-        markDialog.visible = false;
-        await getDocumentList();
-      } catch (error) {
-        console.error(t('document.document.message.markFailed'), error);
-      } finally {
-        markButtonLoading.value = false;
-      }
-    }
-  });
+// 指定成功回调
+const handleSpecifySuccess = async () => {
+  await getDocumentList();
+  emit('refreshTree');
 };
 
 // 文件类型判断函数

+ 130 - 283
src/views/document/folder/document/FolderTree.vue

@@ -6,8 +6,7 @@
       </el-button>
     </div>
     <el-scrollbar class="tree-scrollbar">
-      <el-tree ref="treeRef" v-loading="loading" :data="treeData" :props="treeProps" node-key="id"
-        :expand-on-click-node="false">
+      <el-tree ref="treeRef" v-loading="loading" :data="treeData" :props="treeProps" node-key="id" :expand-on-click-node="false">
         <template #default="{ node, data }">
           <span class="custom-tree-node">
             <el-icon>
@@ -17,8 +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" />
+            <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>
@@ -42,8 +40,7 @@
       <li class="menu-item" v-hasPermi="['document:folder:edit']" @click="handleMenuItemClick('edit', currentMenuData)">
         <span>{{ t('document.document.menu.edit') }}</span>
       </li>
-      <li class="menu-item" v-hasPermi="['document:folder:remove']"
-        @click="handleMenuItemClick('delete', currentMenuData)">
+      <li class="menu-item" v-hasPermi="['document:folder:remove']" @click="handleMenuItemClick('delete', currentMenuData)">
         <span>{{ t('document.document.menu.delete') }}</span>
       </li>
     </ul>
@@ -51,86 +48,50 @@
     <!-- 二级菜单 -->
     <ul class="secondary-menu" v-if="showSecondaryMenu" :style="secondaryMenuStyle">
       <!-- 国家或中心:显示中心和文件夹 -->
-      <template v-if="currentMenuData && (currentMenuData.type === 1 || currentMenuData.type === 2)"
-        v-hasPermi="['document:folder:add']">
-        <li class="menu-item" @click="handleMenuItemClick('add:2', currentMenuData)"
-          v-hasPermi="['document:folder:add']">
+      <template v-if="currentMenuData && (currentMenuData.type === 1 || currentMenuData.type === 2)" v-hasPermi="['document:folder:add']">
+        <li class="menu-item" @click="handleMenuItemClick('add:2', currentMenuData)" v-hasPermi="['document:folder:add']">
           <span>{{ t('document.document.menu.center') }}</span>
         </li>
-        <li class="menu-item" @click="handleMenuItemClick('add:0', currentMenuData)"
-          v-hasPermi="['document:folder:add']">
+        <li class="menu-item" @click="handleMenuItemClick('add:0', currentMenuData)" v-hasPermi="['document:folder:add']">
           <span>{{ t('document.document.menu.folder') }}</span>
         </li>
       </template>
       <!-- 文件夹:只显示文件夹和文档 -->
       <template v-else-if="currentMenuData && currentMenuData.type === 0">
-        <li class="menu-item" @click="handleMenuItemClick('add:0', currentMenuData)"
-          v-hasPermi="['document:folder:add']">
+        <li class="menu-item" @click="handleMenuItemClick('add:0', currentMenuData)" v-hasPermi="['document:folder:add']">
           <span>{{ t('document.document.menu.folder') }}</span>
         </li>
-        <li class="menu-item" v-hasPermi="['document:document:add']"
-          @click="handleMenuItemClick('add:document', currentMenuData)">
+        <li class="menu-item" v-hasPermi="['document:document:add']" @click="handleMenuItemClick('add:document', currentMenuData)">
           <span>{{ t('document.document.menu.document') }}</span>
         </li>
       </template>
     </ul>
 
-    <!-- 添加文件夹对话框 -->
-    <el-dialog v-model="dialog.visible" :title="dialog.title" width="600px" append-to-body>
-      <el-form ref="folderFormRef" :model="form" :rules="rules" label-width="140px">
-        <el-form-item v-if="!dialog.isEdit && form.parentId === undefined" :label="t('document.document.form.type')"
-          prop="type">
-          <el-select v-model="form.type" :placeholder="t('document.document.form.typePlaceholder')" style="width: 100%">
-            <el-option :label="t('document.document.type.country')" :value="1" />
-            <el-option :label="t('document.document.type.center')" :value="2" />
-            <el-option :label="t('document.document.type.folder')" :value="0" />
-          </el-select>
-        </el-form-item>
-        <el-form-item :label="t('document.document.form.name')" prop="name">
-          <el-input v-model="form.name" :placeholder="t('document.document.form.namePlaceholder')" clearable />
-        </el-form-item>
-        <el-form-item :label="t('document.document.form.restrictionLevel')" prop="restrictionLevel">
-          <el-radio-group v-model="isRestricted" @change="handleRestrictionChange">
-            <el-radio :label="false">{{ t('document.document.form.noRestriction') }}</el-radio>
-            <el-radio :label="true">{{ t('document.document.form.restricted') }}</el-radio>
-          </el-radio-group>
-          <el-input-number v-if="isRestricted" v-model="restrictionLevelValue" :min="0" :max="10000"
-            style="width: 100%; margin-top: 10px"
-            :placeholder="t('document.document.form.restrictionLevelPlaceholder')" />
-        </el-form-item>
-        <el-form-item :label="t('document.document.form.note')" prop="note">
-          <el-input v-model="form.note" type="textarea" :rows="4"
-            :placeholder="t('document.document.form.notePlaceholder')" />
-        </el-form-item>
-        <el-form-item :label="t('document.document.form.keywords')" prop="keywords">
-          <el-select v-loading="keywordLoading" v-model="form.keywords" multiple
-            :placeholder="t('document.document.form.keywordsPlaceholder')" style="width: 100%">
-            <el-option v-for="keyword in keywordList" :key="keyword.id" :label="keyword.content" :value="keyword.id" />
-          </el-select>
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button :loading="buttonLoading" type="primary" @click="submitFolderForm">{{
-            t('document.document.button.confirm') }}</el-button>
-          <el-button @click="cancel">{{ t('document.document.button.cancel') }}</el-button>
-        </div>
-      </template>
-    </el-dialog>
+    <!-- 文件夹表单对话框 -->
+    <FolderFormDialog ref="folderFormDialogRef" @success="handleFolderFormSuccess" />
+
+    <!-- 新增文档对话框 -->
+    <AddDocumentDialog
+      ref="addDocumentDialogRef"
+      :project-id="projectId"
+      :folder-id="currentDocumentFolderId"
+      :document-info="currentDocumentInfo"
+      @success="handleDocumentSuccess"
+    />
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted, onUnmounted, nextTick, getCurrentInstance, watch, computed } from 'vue';
+import { ref, reactive, onMounted, onUnmounted, nextTick, getCurrentInstance } from 'vue';
 import { useI18n } from 'vue-i18n';
-import { listFolder, addFolder, delFolder, getFolder, updateFolder, getNameByCenterId } from '@/api/document/folder';
-import { FolderListVO, FolderForm } from '@/api/document/folder/types';
+import { listFolder, delFolder, getNameByProjectId } from '@/api/document/folder';
+import { FolderListVO } from '@/api/document/folder/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 request from '@/utils/request';
+import AddDocumentDialog from './components/AddDocumentDialog.vue';
+import FolderFormDialog from './components/FolderFormDialog.vue';
 
 interface Props {
   projectId?: number | string;
@@ -140,84 +101,25 @@ const props = defineProps<Props>();
 
 const emit = defineEmits<{
   folderClick: [folder: FolderListVO];
-  addDocument: [folder: FolderListVO, projectInfo?: { projectName: string; centerId: number }];
   refresh: [];
 }>();
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { t } = useI18n();
 
-// 标签类型定义
-interface KeywordVO {
-  id: number;
-  content: string;
-  note?: string;
-}
+// 对话框引用
+const folderFormDialogRef = ref<InstanceType<typeof FolderFormDialog>>();
+const addDocumentDialogRef = ref<InstanceType<typeof AddDocumentDialog>>();
+
+// 新增文档对话框相关
+const currentDocumentFolderId = ref<number>();
+const currentDocumentInfo = ref<{ projectCode: string; countryName: string; centerId: number | string }>();
 
 // 数据定义
 const loading = ref(false);
-const buttonLoading = ref(false);
 const treeData = ref<FolderListVO[]>([]);
 const treeRef = ref();
-const folderFormRef = ref<FormInstance>();
-const scanFormRef = ref<FormInstance>();
 const tempDocumentCount = ref(0);
-const keywordList = ref<KeywordVO[]>([]);
-const keywordLoading = ref(false);
-
-// 获取标签列表
-const getKeywordList = async () => {
-  keywordLoading.value = true;
-  try {
-    const res = await request({
-      url: '/setting/keyword/listOnFolder',
-      method: 'get'
-    });
-    keywordList.value = res.data || [];
-  } catch (error) {
-    ElMessage.error('获取标签列表失败');
-    console.error('获取标签列表失败:', error);
-    keywordList.value = [];
-  } finally {
-    keywordLoading.value = false;
-  }
-};
-
-// 对话框
-const dialog = reactive({
-  visible: false,
-  title: '',
-  isEdit: false
-});
-
-// 当前操作的节点
-const currentNode = ref<FolderListVO | null>(null);
-
-// 限制层级相关状态
-const isRestricted = ref(false);
-const restrictionLevelValue = ref(0);
-
-// 表单初始数据
-const initFormData: FolderForm & { keywords?: number[] } = {
-  id: undefined,
-  projectId: undefined,
-  parentId: undefined,
-  type: 0,
-  name: '',
-  status: 0,
-  note: '',
-  restrictionLevel: -1,
-  keywords: []
-};
-
-// 表单数据
-const form = ref<FolderForm & { keywords?: number[] }>({ ...initFormData });
-
-// 表单验证规则
-const rules = {
-  name: [{ required: true, message: t('document.document.rule.nameRequired'), trigger: 'blur' }],
-  type: [{ required: true, message: t('document.document.rule.typeRequired'), trigger: 'change' }]
-};
 
 // 树形组件配置
 const treeProps = {
@@ -242,9 +144,10 @@ const getList = async () => {
   loading.value = true;
   try {
     // 保存当前展开的节点
-    const expandedKeys = treeRef.value?.store?.nodesMap ? 
-      Object.keys(treeRef.value.store.nodesMap).filter(key => treeRef.value.store.nodesMap[key].expanded) : [];
-    
+    const expandedKeys = treeRef.value?.store?.nodesMap
+      ? Object.keys(treeRef.value.store.nodesMap).filter((key) => treeRef.value.store.nodesMap[key].expanded)
+      : [];
+
     const res = await listFolder({ projectId: props.projectId } as any);
     const folders = res.data || [];
 
@@ -266,7 +169,7 @@ const getList = async () => {
     // 恢复展开状态
     nextTick(() => {
       if (expandedKeys.length > 0 && treeRef.value) {
-        expandedKeys.forEach(key => {
+        expandedKeys.forEach((key) => {
           const node = treeRef.value.store.nodesMap[key];
           if (node) {
             node.expanded = true;
@@ -298,145 +201,33 @@ const getTempDocumentCount = async () => {
   }
 };
 
-// 表单重置
-const reset = () => {
-  form.value = { ...initFormData };
-  isRestricted.value = false;
-  restrictionLevelValue.value = 0;
-  folderFormRef.value?.resetFields();
-};
-
-// 处理限制状态变化
-const handleRestrictionChange = (value: boolean) => {
-  if (value) {
-    form.value.restrictionLevel = restrictionLevelValue.value;
-  } else {
-    form.value.restrictionLevel = -1;
-  }
-};
-
-// 监听restrictionLevelValue变化
-watch(restrictionLevelValue, (newValue) => {
-  if (isRestricted.value) {
-    form.value.restrictionLevel = newValue;
-  }
-});
-
-// 取消按钮
-const cancel = () => {
-  reset();
-  dialog.visible = false;
-};
-
 // 新建文件夹(顶级)
 const handleAddFolder = async () => {
-  reset();
-  currentNode.value = null;
-  form.value.projectId = props.projectId;
-  form.value.parentId = undefined;
-  form.value.type = 1; // 默认选择国家类型
-  // 获取标签列表
-  await getKeywordList();
-  dialog.visible = true;
-  dialog.title = t('document.document.dialog.addFolder');
-  dialog.isEdit = false;
+  if (!props.projectId) {
+    ElMessage.warning(t('document.document.message.projectIdNotExist'));
+    return;
+  }
+  folderFormDialogRef.value?.open(props.projectId);
 };
 
 // 新增子节点(指定类型)
 const handleAddChildWithType = async (data: FolderListVO, type: number) => {
-  reset();
-  currentNode.value = data;
-  form.value.projectId = props.projectId;
-  form.value.parentId = data.id;
-  form.value.type = type;
-  // 获取标签列表
-  await getKeywordList();
-  dialog.visible = true;
-  const typeLabel =
-    type === 0 ? t('document.document.type.folder') : type === 1 ? t('document.document.type.country') : t('document.document.type.center');
-  dialog.title =
-    type === 0
-      ? t('document.document.dialog.addFolder')
-      : type === 1
-        ? t('document.document.dialog.addCountry')
-        : t('document.document.dialog.addCenter');
-  dialog.isEdit = false;
-};
-
-// 提交表单
-const submitFolderForm = () => {
-  folderFormRef.value?.validate(async (valid: boolean) => {
-    if (valid) {
-      if (dialog.isEdit) {
-        try {
-          const confirmMessage = `
-            <div style="text-align: left;">
-              <p><strong>${t('document.document.confirm.nameLabel')}</strong>${form.value.name}</p>
-              <p><strong>${t('document.document.confirm.restrictionLevelLabel')}</strong>${form.value.restrictionLevel !== -1 ? form.value.restrictionLevel : t('document.document.confirm.noLimit')}</p>
-              <p><strong>${t('document.document.confirm.noteLabel')}</strong>${form.value.note || t('document.document.confirm.noNote')}</p>
-            </div>
-          `;
-          await ElMessageBox.confirm(confirmMessage, t('document.document.dialog.confirmEdit'), {
-            confirmButtonText: t('document.document.message.confirmButton'),
-            cancelButtonText: t('document.document.message.cancelButton'),
-            type: 'warning',
-            dangerouslyUseHTMLString: true
-          });
-        } catch {
-          return;
-        }
-      }
-
-      buttonLoading.value = true;
-      try {
-        if (dialog.isEdit) {
-          await updateFolder(form.value);
-          proxy?.$modal.msgSuccess(t('document.document.message.editSuccess'));
-        } else {
-          await addFolder(form.value);
-          proxy?.$modal.msgSuccess(t('document.document.message.addSuccess'));
-        }
-        dialog.visible = false;
-        await getList();
-        emit('refresh');
-      } catch (error) {
-        console.error(dialog.isEdit ? t('document.document.message.editFailed') : t('document.document.message.addFailed'), error);
-      } finally {
-        buttonLoading.value = false;
-      }
-    }
-  });
+  if (!props.projectId) {
+    ElMessage.warning(t('document.document.message.projectIdNotExist'));
+    return;
+  }
+  folderFormDialogRef.value?.open(props.projectId, data.id, type);
 };
 
 // 编辑
 const handleEdit = async (data: FolderListVO) => {
-  reset();
-  loading.value = true;
-  try {
-    const res = await getFolder(data.id);
-    Object.assign(form.value, res.data);
-
-    if (form.value.restrictionLevel === -1) {
-      isRestricted.value = false;
-      restrictionLevelValue.value = 0;
-    } else {
-      isRestricted.value = true;
-      restrictionLevelValue.value = form.value.restrictionLevel;
-    }
-
-    // 获取标签列表
-    await getKeywordList();
+  folderFormDialogRef.value?.openEdit(data.id);
+};
 
-    currentNode.value = null;
-    dialog.visible = true;
-    dialog.title = t('document.document.dialog.editFolder');
-    dialog.isEdit = true;
-  } catch (error) {
-    ElMessage.error(t('document.document.message.getFolderInfoFailed'));
-    console.error(error);
-  } finally {
-    loading.value = false;
-  }
+// 处理文件夹表单成功
+const handleFolderFormSuccess = async () => {
+  await getList();
+  emit('refresh');
 };
 
 // 删除
@@ -468,7 +259,7 @@ const handleDelete = async (data: FolderListVO) => {
 };
 
 // 切换菜单显示
-const toggleMenu = (event: MouseEvent, data: FolderListVO) => {
+const toggleMenu = async (event: MouseEvent, data: FolderListVO) => {
   if (event.type !== 'click') return;
 
   event.stopPropagation();
@@ -480,6 +271,9 @@ const toggleMenu = (event: MouseEvent, data: FolderListVO) => {
   if (activeMenu.value === data.id) {
     closeAllMenus();
   } else {
+    // 在打开菜单时就获取文档信息
+    await prepareDocumentInfo(data);
+
     primaryMenuStyle.value = {
       left: `${rect.left}px`,
       top: `${rect.bottom + 2}px`
@@ -491,6 +285,50 @@ const toggleMenu = (event: MouseEvent, data: FolderListVO) => {
   }
 };
 
+// 预先获取文档信息
+const prepareDocumentInfo = async (data: FolderListVO) => {
+  // 查找该文件夹所属的中心ID和国家名
+  const centerId = findCenterId(data);
+  const countryName = findCountryName(data);
+
+  console.log('预获取文档信息 - 所属中心ID:', centerId);
+  console.log('预获取文档信息 - 所属国家名:', countryName);
+  console.log('预获取文档信息 - 当前文件夹信息:', data);
+
+  // 设置当前文件夹ID
+  currentDocumentFolderId.value = data.id;
+
+  const res = await getNameByProjectId(props.projectId);
+  console.log('获取到的项目编码:', res.data);
+  // 如果找到中心ID,调用接口获取项目编码
+  if (centerId !== undefined) {
+    try {
+      // 设置文档信息
+      currentDocumentInfo.value = {
+        projectCode: res.data.projectCode,
+        countryName: countryName || 'NA',
+        centerId: centerId || 'NA'
+      };
+    } catch (error) {
+      console.error('获取项目编码失败:', error);
+      ElMessage.error('获取项目编码失败');
+      // 即使失败也设置默认值
+      currentDocumentInfo.value = {
+        projectCode: '',
+        countryName: countryName || 'NA',
+        centerId: centerId || 'NA'
+      };
+    }
+  } else {
+    // 没有中心ID,使用默认值
+    currentDocumentInfo.value = {
+      projectCode: res.data.projectCode,
+      countryName: countryName || 'NA',
+      centerId: 'NA'
+    };
+  }
+};
+
 // 切换二级菜单显示
 const toggleSubmenu = (event: MouseEvent) => {
   if (event.type !== 'click') return;
@@ -528,25 +366,8 @@ const handleCommand = async (command: string, data: FolderListVO) => {
   if (command.startsWith('add:')) {
     const cmdPart = command.split(':')[1];
     if (cmdPart === 'document') {
-      // 查找该文件夹所属的中心ID
-      const centerId = findCenterId(data);
-      console.log('新增文档 - 所属中心ID:', centerId);
-      console.log('新增文档 - 当前文件夹信息:', data);
-      
-      // 如果找到中心ID,调用接口获取项目名称
-      if (centerId !== undefined) {
-        try {
-          const res = await getNameByCenterId(centerId);
-          console.log('获取到的项目信息:', res.data);
-          emit('addDocument', data, res.data);
-        } catch (error) {
-          console.error('获取项目名称失败:', error);
-          ElMessage.error('获取项目名称失败');
-          emit('addDocument', data);
-        }
-      } else {
-        emit('addDocument', data);
-      }
+      // 数据已经在toggleMenu时获取,直接打开对话框
+      addDocumentDialogRef.value?.open();
     } else {
       const type = parseInt(cmdPart);
       handleAddChildWithType(data, type);
@@ -564,7 +385,7 @@ const findCenterId = (folder: FolderListVO): number | undefined => {
   if (folder.type === 2) {
     return folder.id;
   }
-  
+
   // 如果有父节点ID,递归查找父节点
   if (folder.parentId !== undefined && folder.parentId !== null) {
     const parentFolder = findFolderById(treeData.value, folder.parentId);
@@ -572,7 +393,25 @@ const findCenterId = (folder: FolderListVO): number | undefined => {
       return findCenterId(parentFolder);
     }
   }
-  
+
+  return undefined;
+};
+
+// 查找文件夹所属的国家名
+const findCountryName = (folder: FolderListVO): string | undefined => {
+  // 如果当前节点就是国家,直接返回其名称
+  if (folder.type === 1) {
+    return folder.name;
+  }
+
+  // 如果有父节点ID,递归查找父节点
+  if (folder.parentId !== undefined && folder.parentId !== null) {
+    const parentFolder = findFolderById(treeData.value, folder.parentId);
+    if (parentFolder) {
+      return findCountryName(parentFolder);
+    }
+  }
+
   return undefined;
 };
 
@@ -597,6 +436,14 @@ const handleFolderClick = (data: FolderListVO) => {
   emit('folderClick', data);
 };
 
+// 处理文档添加成功
+const handleDocumentSuccess = () => {
+  // 刷新树数据
+  getList();
+  // 通知父组件刷新文档列表
+  emit('refresh');
+};
+
 // 关闭所有菜单
 const closeAllMenus = () => {
   showSecondaryMenu.value = false;

+ 416 - 0
src/views/document/folder/document/components/AddDocumentDialog.vue

@@ -0,0 +1,416 @@
+<template>
+  <el-dialog v-model="visible" :title="t('document.document.dialog.addDocument')" width="700px" append-to-body @close="handleClose">
+    <el-form ref="formRef" :model="form" :rules="rules" label-width="140px">
+      <el-form-item
+        :label="form.type === 1 ? t('document.document.documentForm.planName') : t('document.document.documentForm.name')"
+        prop="name">
+        <div style="display: flex; align-items: center; gap: 8px; width: 100%;">
+          <span v-if="namePrefix" style="white-space: nowrap; color: #606266;">{{ namePrefix }}</span>
+          <el-input v-model="nameInput" :placeholder="t('document.document.documentForm.namePlaceholder')"
+            clearable style="flex: 1;" @input="handleNameInputChange" />
+          <span v-if="namePrefix" style="white-space: nowrap; color: #606266;">-</span>
+          <el-date-picker v-model="form.documentDate" type="date" value-format="YYYYMMDD" format="YYYYMMDD"
+            placeholder="请选择日期" style="width: 180px;" @change="handleDateChange" />
+        </div>
+      </el-form-item>
+
+      <el-form-item :label="t('document.document.documentForm.type')" prop="type" v-if="hasAddPlanPermission">
+        <el-radio-group v-model="form.type" @change="handleTypeChange">
+          <el-radio :label="0">{{ t('document.document.documentForm.normalDocument') }}</el-radio>
+          <el-radio :label="1">{{ t('document.document.documentForm.planDocument') }}</el-radio>
+        </el-radio-group>
+      </el-form-item>
+
+      <el-form-item v-if="form.type === 1" :label="t('document.document.documentForm.planSubmitter')"
+        prop="planSubmitter">
+        <el-select v-model="form.planSubmitter" filterable remote reserve-keyword
+          :placeholder="t('document.document.documentForm.planSubmitterPlaceholder')"
+          :remote-method="searchSubmitters" :loading="submitterSearchLoading" style="width: 100%">
+          <el-option v-for="submitter in submitterOptions" :key="submitter.id"
+            :label="`${submitter.name} / ${submitter.dept} --- ${submitter.phoneNumber}`" :value="submitter.id" />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item v-if="form.type === 1" :label="t('document.document.documentForm.submitDeadline')"
+        prop="submitDeadline">
+        <el-date-picker v-model="form.submitDeadline" type="date" value-format="YYYY-MM-DD"
+          :placeholder="t('document.document.documentForm.submitDeadlinePlaceholder')" style="width: 100%" />
+      </el-form-item>
+
+      <el-form-item v-if="form.type === 1" :label="t('document.document.documentForm.planType')"
+        prop="planType">
+        <el-select v-model="form.planType"
+          :placeholder="t('document.document.documentForm.planTypePlaceholder')" clearable style="width: 100%">
+          <el-option v-for="dict in plan_document_type" :key="dict.value" :label="parseI18nName(dict.label)"
+            :value="dict.value" />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item v-if="form.type === 0" :label="t('document.document.documentForm.actualFile')"
+        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="form.type === 0 && uploadedFileId" label="生效日期" prop="effectiveDate">
+        <el-date-picker v-model="form.effectiveDate" type="date" value-format="YYYY-MM-DD"
+          placeholder="请选择生效日期" style="width: 100%" />
+      </el-form-item>
+
+      <el-form-item v-if="form.submitTime" :label="t('document.document.documentForm.submitTime')">
+        <el-input v-model="form.submitTime" disabled />
+      </el-form-item>
+
+      <el-form-item :label="t('document.document.documentForm.sendFlag')" prop="sendFlag">
+        <el-switch v-model="form.sendFlag" :active-value="true" :inactive-value="false" />
+      </el-form-item>
+
+      <el-form-item :label="t('document.document.documentForm.note')" prop="note">
+        <el-input v-model="form.note" type="textarea" :rows="4"
+          :placeholder="t('document.document.documentForm.notePlaceholder')" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button :loading="buttonLoading" type="primary" @click="submitForm">{{
+          t('document.document.button.confirm')
+        }}</el-button>
+        <el-button @click="handleClose">{{ t('document.document.button.cancel') }}</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, watch, computed, getCurrentInstance, type ComponentInternalInstance } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { addDocument, getMemberByNameAndFolder } from '@/api/document/document';
+import { DocumentForm } from '@/api/document/document/types';
+import { ElMessage } from 'element-plus';
+import type { FormInstance } from 'element-plus';
+import { useUserStore } from '@/store/modules/user';
+import { checkPermi } from '@/utils/permission';
+import { parseI18nName } from '@/utils/i18n';
+import fileUpload from '@/components/FileUpload/index.vue';
+import { getNameByProjectId } from '@/api/document/folder';
+
+interface Props {
+  projectId?: number | string;
+  folderId?: number;
+  documentInfo?: {
+    projectCode: string;
+    countryName: string;
+    centerId: number | string;
+  };
+}
+
+const props = defineProps<Props>();
+
+const emit = defineEmits<{
+  success: [];
+  close: [];
+}>();
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { t } = useI18n();
+const userStore = useUserStore();
+const { plan_document_type } = (proxy?.useDict('plan_document_type') as any);
+
+const visible = ref(false);
+const buttonLoading = ref(false);
+const formRef = ref<FormInstance>();
+
+// 检查是否有计划文档添加权限
+const hasAddPlanPermission = computed(() => checkPermi(['document:document:addPlan']));
+
+// 名称前缀(用于显示,隐藏NA)
+const namePrefix = ref<string>('');
+// 完整前缀(用于提交,保留NA)
+const fullNamePrefix = ref<string>('');
+const nameInput = ref<string>('');
+
+// 文档信息(用于构建完整名称)
+const documentInfoData = ref<{
+  projectCode: string;
+  countryName: string;
+  centerId: string;
+}>();
+
+// 文档表单初始数据
+const initFormData: DocumentForm & { documentDate?: string } = {
+  id: undefined,
+  name: '',
+  type: 0,
+  planSubmitter: undefined,
+  folderId: undefined,
+  submitDeadline: undefined,
+  planType: undefined,
+  actualDocument: undefined,
+  submitTime: undefined,
+  projectId: undefined,
+  note: '',
+  sendFlag: false,
+  effectiveDate: undefined,
+  documentDate: undefined
+};
+
+const form = ref<DocumentForm & { documentDate?: string }>({ ...initFormData });
+
+// 文件上传
+const uploadedFileId = ref<string>('');
+
+// 递交人搜索相关
+const submitterSearchLoading = ref(false);
+const submitterOptions = ref<Array<{
+  id: number;
+  name: string;
+  dept: string;
+  phoneNumber: string;
+}>>([]);
+let submitterSearchTimer: NodeJS.Timeout | null = null;
+
+// 表单验证规则
+const rules = {
+  name: [{ required: true, message: t('document.document.documentRule.nameRequired'), trigger: 'blur' }],
+  planSubmitter: [{ required: true, message: t('document.document.documentRule.planSubmitterRequired'), trigger: 'change' }],
+  actualDocument: [{ required: true, message: t('document.document.documentRule.fileRequired'), trigger: 'change' }]
+};
+
+// 处理名称输入变化
+const handleNameInputChange = (value: string) => {
+  nameInput.value = value;
+  updateDocumentName();
+};
+
+// 处理日期变化
+const handleDateChange = (value: string) => {
+  updateDocumentName();
+};
+
+// 更新文档名称
+const updateDocumentName = () => {
+  if (!documentInfoData.value) {
+    form.value.name = nameInput.value;
+    return;
+  }
+
+  // 构建完整名称(用于提交,包含NA)
+  // 格式:{projectCode}-{countryName/NA}-{centerId/NA}-{用户输入}-{日期}
+  let fullName = `${documentInfoData.value.projectCode}-${documentInfoData.value.countryName}-${documentInfoData.value.centerId}`;
+
+  if (nameInput.value) {
+    fullName += `-${nameInput.value}`;
+  }
+
+  if (form.value.documentDate) {
+    fullName += `-${form.value.documentDate}`;
+  }
+
+  form.value.name = fullName;
+};
+
+// 处理文档类型变化
+const handleTypeChange = (value: number) => {
+  if (value === 0) {
+    form.value.planSubmitter = undefined;
+    form.value.submitter = userStore.userId;
+    form.value.submitDeadline = undefined;
+    form.value.planType = undefined;
+  } else {
+    form.value.planSubmitter = undefined;
+    form.value.submitter = undefined;
+  }
+};
+
+// 搜索递交人
+const searchSubmitters = async (query: string) => {
+  if (!query || query.trim() === '') {
+    submitterOptions.value = [];
+    return;
+  }
+
+  // 检查是否有 folderId
+  if (!props.folderId) {
+    ElMessage.warning('未指定文件夹,无法查询递交人');
+    return;
+  }
+
+  // 检查是否有 projectId
+  if (!props.projectId) {
+    ElMessage.warning('未指定项目,无法查询递交人');
+    return;
+  }
+
+  if (submitterSearchTimer) {
+    clearTimeout(submitterSearchTimer);
+  }
+
+  submitterSearchTimer = setTimeout(async () => {
+    submitterSearchLoading.value = true;
+    try {
+      const res = await getMemberByNameAndFolder({
+        name: query,
+        folder: props.folderId!,
+        project: props.projectId!,
+        pageNum: 1,
+        pageSize: 10
+      });
+      submitterOptions.value = res.rows || [];
+    } catch (error) {
+      console.error('Failed to search submitters:', error);
+      ElMessage.error(t('document.document.message.searchSubmitterFailed'));
+    } finally {
+      submitterSearchLoading.value = false;
+    }
+  }, 300);
+};
+
+// 监听文件上传变化
+watch(uploadedFileId, (newVal) => {
+  if (newVal) {
+    const ids = newVal.split(',').filter((id) => id.trim());
+    if (ids.length > 0) {
+      form.value.actualDocument = parseInt(ids[0]);
+      const now = new Date();
+      form.value.submitTime = proxy?.parseTime(now, '{y}-{m}-{d} {h}:{i}:{s}');
+      form.value.submitter = userStore.userId;
+    }
+  } else {
+    form.value.actualDocument = undefined;
+    form.value.submitTime = undefined;
+    form.value.submitter = undefined;
+  }
+});
+
+// 重置表单
+const resetForm = () => {
+  form.value = { ...initFormData };
+  uploadedFileId.value = '';
+  namePrefix.value = '';
+  fullNamePrefix.value = '';
+  nameInput.value = '';
+  documentInfoData.value = undefined;
+  submitterOptions.value = [];
+  formRef.value?.resetFields();
+
+  if (!hasAddPlanPermission.value) {
+    form.value.type = 0;
+  }
+
+  if (form.value.type === 0) {
+    form.value.planSubmitter = undefined;
+    form.value.submitter = userStore.userId;
+  } else {
+    form.value.planSubmitter = undefined;
+    form.value.submitter = undefined;
+  }
+
+  form.value.projectId = props.projectId;
+  form.value.folderId = props.folderId;
+};
+
+// 打开对话框
+const open = async () => {
+  resetForm();
+
+  console.log('AddDocumentDialog - 接收到的documentInfo:', props.documentInfo);
+
+  // 设置名称前缀
+  if (props.documentInfo) {
+    let projectCode = '';
+    const countryName = props.documentInfo.countryName;
+    const centerId = props.documentInfo.centerId;
+
+    // 如果有centerId且不是'NA',调用接口获取projectCode
+    try {
+      const res = await getNameByProjectId(props.projectId);
+      projectCode = res.data.projectCode;
+      console.log('AddDocumentDialog - 获取到的projectCode:', projectCode);
+    } catch (error) {
+      console.error('AddDocumentDialog - 获取projectCode失败:', error);
+      ElMessage.error('获取项目编码失败');
+    }
+
+    // 保存文档信息(用于构建完整名称)
+    documentInfoData.value = {
+      projectCode: projectCode,
+      countryName: countryName,
+      centerId: String(centerId)
+    };
+
+    // 构建显示前缀(隐藏NA)
+    // 格式:{projectCode}-{countryName}-{centerId}-
+    // 如果countryName或centerId为NA,则不显示该部分
+    let displayPrefix = projectCode;
+
+    if (countryName && countryName !== 'NA') {
+      displayPrefix += `-${countryName}`;
+    }
+
+    if (centerId && centerId !== 'NA') {
+      displayPrefix += `-${centerId}`;
+    }
+
+    displayPrefix += '-';
+
+    namePrefix.value = displayPrefix;
+    console.log('AddDocumentDialog - 显示前缀:', namePrefix.value);
+    console.log('AddDocumentDialog - 文档信息数据:', documentInfoData.value);
+
+    updateDocumentName();
+  }
+
+  visible.value = true;
+};
+
+// 关闭对话框
+const handleClose = () => {
+  visible.value = false;
+  resetForm();
+  emit('close');
+};
+
+// 提交表单
+const submitForm = () => {
+  formRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      buttonLoading.value = true;
+      try {
+        const hasUploadedFile = !!uploadedFileId.value;
+
+        const submitData: DocumentForm = {
+          id: form.value.id || 0,
+          name: form.value.name || '',
+          type: form.value.type !== undefined ? form.value.type : 0,
+          planSubmitter: form.value.type === 0 ? undefined : form.value.planSubmitter,
+          submitter: form.value.type === 0 ? userStore.userId : form.value.submitter,
+          folderId: form.value.folderId || 0,
+          submitDeadline: form.value.submitDeadline || '',
+          planType: form.value.planType || '',
+          actualDocument: hasUploadedFile ? uploadedFileId.value : null,
+          submitTime: form.value.submitTime || (hasUploadedFile ? new Date().toISOString() : ''),
+          projectId: form.value.projectId || props.projectId,
+          status: hasUploadedFile ? 1 : 0,
+          note: form.value.note || '',
+          sendFlag: form.value.sendFlag !== undefined ? form.value.sendFlag : false,
+          effectiveDate: form.value.effectiveDate || ''
+        };
+
+        await addDocument(submitData);
+        proxy?.$modal.msgSuccess(t('document.document.message.addDocumentSuccess'));
+        visible.value = false;
+        emit('success');
+      } catch (error) {
+        console.error(t('document.document.message.addDocumentFailed'), error);
+      } finally {
+        buttonLoading.value = false;
+      }
+    }
+  });
+};
+
+// 暴露方法给父组件
+defineExpose({
+  open
+});
+</script>

+ 140 - 0
src/views/document/folder/document/components/AuditDialog.vue

@@ -0,0 +1,140 @@
+<template>
+  <el-dialog v-model="visible" :title="title" width="500px" @close="handleClose">
+    <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
+      <el-form-item label="审核结果" prop="result">
+        <el-radio-group v-model="form.result">
+          <el-radio :label="3">通过</el-radio>
+          <el-radio :label="2">驳回</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item v-if="form.result === 2" label="驳回理由" prop="rejectReason">
+        <el-input
+          v-model="form.rejectReason"
+          type="textarea"
+          :rows="4"
+          placeholder="请输入驳回理由"
+          maxlength="500"
+          show-word-limit
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="handleClose">取消</el-button>
+      <el-button type="primary" :loading="loading" @click="handleSubmit">确定</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed, nextTick, watch } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { ElMessage } from 'element-plus';
+import type { FormInstance } from 'element-plus';
+import { auditDocument } from '@/api/document/document';
+import { DocumentVO } from '@/api/document/document/types';
+
+interface Props {
+  modelValue: boolean;
+  document: DocumentVO | null;
+}
+
+const props = defineProps<Props>();
+const emit = defineEmits<{
+  'update:modelValue': [value: boolean];
+  success: [];
+}>();
+
+const { t } = useI18n();
+const formRef = ref<FormInstance>();
+const loading = ref(false);
+
+const visible = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+});
+
+const title = computed(() => t('document.document.dialog.auditDocument'));
+
+const form = ref({
+  documentId: 0,
+  result: 3,
+  rejectReason: ''
+});
+
+const rules = reactive({
+  result: [
+    {
+      required: true,
+      message: '请选择审核结果',
+      trigger: 'change'
+    }
+  ],
+  rejectReason: [
+    {
+      required: true,
+      message: '请输入驳回理由',
+      trigger: 'blur'
+    }
+  ]
+});
+
+// 监听 document 变化,初始化表单
+watch(
+  () => props.document,
+  (newDoc) => {
+    if (newDoc) {
+      form.value = {
+        documentId: newDoc.id as number,
+        result: 3,
+        rejectReason: ''
+      };
+      nextTick(() => {
+        formRef.value?.clearValidate();
+      });
+    }
+  },
+  { immediate: true }
+);
+
+const handleClose = () => {
+  visible.value = false;
+  formRef.value?.resetFields();
+  form.value = {
+    documentId: 0,
+    result: 3,
+    rejectReason: ''
+  };
+};
+
+const handleSubmit = async () => {
+  if (!formRef.value) return;
+
+  // 如果选择驳回,需要验证驳回理由
+  if (form.value.result === 2) {
+    try {
+      await formRef.value.validate();
+    } catch (error) {
+      return;
+    }
+  }
+
+  loading.value = true;
+  try {
+    const submitData = {
+      documentId: form.value.documentId,
+      result: form.value.result,
+      rejectReason: form.value.result === 2 ? form.value.rejectReason : undefined
+    };
+
+    await auditDocument(submitData);
+    ElMessage.success(t('document.document.message.auditSuccess'));
+    handleClose();
+    emit('success');
+  } catch (error) {
+    console.error('审核失败:', error);
+    ElMessage.error(t('document.document.message.auditFailed'));
+  } finally {
+    loading.value = false;
+  }
+};
+</script>

+ 277 - 0
src/views/document/folder/document/components/FolderFormDialog.vue

@@ -0,0 +1,277 @@
+<template>
+  <el-dialog v-model="visible" :title="title" width="600px" append-to-body @close="handleClose">
+    <el-form ref="formRef" :model="form" :rules="rules" label-width="140px">
+      <el-form-item v-if="!isEdit && form.parentId === undefined" :label="t('document.document.form.type')" prop="type">
+        <el-select v-model="form.type" :placeholder="t('document.document.form.typePlaceholder')" style="width: 100%">
+          <el-option :label="t('document.document.type.country')" :value="1" />
+          <el-option :label="t('document.document.type.center')" :value="2" />
+          <el-option :label="t('document.document.type.folder')" :value="0" />
+        </el-select>
+      </el-form-item>
+      <el-form-item :label="t('document.document.form.name')" prop="name">
+        <el-input v-model="form.name" :placeholder="t('document.document.form.namePlaceholder')" clearable />
+      </el-form-item>
+      <el-form-item :label="t('document.document.form.restrictionLevel')" prop="restrictionLevel">
+        <el-radio-group v-model="isRestricted" @change="handleRestrictionChange">
+          <el-radio :label="false">{{ t('document.document.form.noRestriction') }}</el-radio>
+          <el-radio :label="true">{{ t('document.document.form.restricted') }}</el-radio>
+        </el-radio-group>
+        <el-input-number
+          v-if="isRestricted"
+          v-model="restrictionLevelValue"
+          :min="0"
+          :max="10000"
+          style="width: 100%; margin-top: 10px"
+          :placeholder="t('document.document.form.restrictionLevelPlaceholder')"
+        />
+      </el-form-item>
+      <el-form-item :label="t('document.document.form.note')" prop="note">
+        <el-input v-model="form.note" type="textarea" :rows="4" :placeholder="t('document.document.form.notePlaceholder')" />
+      </el-form-item>
+      <el-form-item v-if="form.type === 0" :label="t('document.document.form.keywords')" prop="keywords">
+        <el-select
+          v-loading="keywordLoading"
+          v-model="form.keywords"
+          multiple
+          :placeholder="t('document.document.form.keywordsPlaceholder')"
+          style="width: 100%"
+        >
+          <el-option v-for="keyword in keywordList" :key="keyword.id" :label="keyword.content" :value="keyword.id" />
+        </el-select>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button :loading="buttonLoading" type="primary" @click="submitForm">{{ t('document.document.button.confirm') }}</el-button>
+        <el-button @click="handleClose">{{ t('document.document.button.cancel') }}</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, watch, getCurrentInstance } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { addFolder, getFolder, updateFolder } from '@/api/document/folder';
+import { FolderForm } from '@/api/document/folder/types';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import type { FormInstance } from 'element-plus';
+import type { ComponentInternalInstance } from 'vue';
+import request from '@/utils/request';
+
+interface KeywordVO {
+  id: number;
+  content: string;
+  note?: string;
+}
+
+const emit = defineEmits<{
+  success: [];
+}>();
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { t } = useI18n();
+
+const visible = ref(false);
+const title = ref('');
+const isEdit = ref(false);
+const buttonLoading = ref(false);
+const keywordLoading = ref(false);
+const formRef = ref<FormInstance>();
+
+const keywordList = ref<KeywordVO[]>([]);
+const isRestricted = ref(false);
+const restrictionLevelValue = ref(0);
+
+const initFormData: FolderForm & { keywords?: number[] } = {
+  id: undefined,
+  projectId: undefined,
+  parentId: undefined,
+  type: 0,
+  name: '',
+  status: 0,
+  note: '',
+  restrictionLevel: -1,
+  keywords: []
+};
+
+const form = ref<FolderForm & { keywords?: number[] }>({ ...initFormData });
+
+const rules = {
+  name: [{ required: true, message: t('document.document.rule.nameRequired'), trigger: 'blur' }],
+  type: [{ required: true, message: t('document.document.rule.typeRequired'), trigger: 'change' }]
+};
+
+// 获取标签列表
+const getKeywordList = async () => {
+  keywordLoading.value = true;
+  try {
+    const res = await request({
+      url: '/setting/keyword/listOnFolder',
+      method: 'get'
+    });
+    keywordList.value = res.data || [];
+  } catch (error) {
+    ElMessage.error('获取标签列表失败');
+    console.error('获取标签列表失败:', error);
+    keywordList.value = [];
+  } finally {
+    keywordLoading.value = false;
+  }
+};
+
+// 处理限制状态变化
+const handleRestrictionChange = (value: boolean) => {
+  if (value) {
+    form.value.restrictionLevel = restrictionLevelValue.value;
+  } else {
+    form.value.restrictionLevel = -1;
+  }
+};
+
+// 监听restrictionLevelValue变化
+watch(restrictionLevelValue, (newValue) => {
+  if (isRestricted.value) {
+    form.value.restrictionLevel = newValue;
+  }
+});
+
+// 监听类型变化,动态加载标签列表
+watch(() => form.value.type, async (newType) => {
+  if (newType === 0 && keywordList.value.length === 0) {
+    // 类型切换为文件夹且标签列表为空时,加载标签列表
+    await getKeywordList();
+  }
+});
+
+// 重置表单
+const reset = () => {
+  form.value = { ...initFormData };
+  isRestricted.value = false;
+  restrictionLevelValue.value = 0;
+  formRef.value?.resetFields();
+};
+
+// 打开对话框 - 新增
+const open = async (projectId: number | string, parentId?: number, type?: number) => {
+  reset();
+  form.value.projectId = projectId;
+  form.value.parentId = parentId;
+  if (type !== undefined) {
+    form.value.type = type;
+  } else {
+    form.value.type = 1; // 默认选择国家类型
+  }
+  
+  // 只有类型为文件夹时才获取标签列表
+  if (form.value.type === 0) {
+    await getKeywordList();
+  }
+  
+  const typeLabel =
+    type === 0 ? t('document.document.type.folder') : type === 1 ? t('document.document.type.country') : t('document.document.type.center');
+  title.value =
+    type === 0
+      ? t('document.document.dialog.addFolder')
+      : type === 1
+        ? t('document.document.dialog.addCountry')
+        : type === 2
+          ? t('document.document.dialog.addCenter')
+          : t('document.document.dialog.addFolder');
+  
+  isEdit.value = false;
+  visible.value = true;
+};
+
+// 打开对话框 - 编辑
+const openEdit = async (folderId: number) => {
+  reset();
+  try {
+    const res = await getFolder(folderId);
+    Object.assign(form.value, res.data);
+
+    if (form.value.restrictionLevel === -1) {
+      isRestricted.value = false;
+      restrictionLevelValue.value = 0;
+    } else {
+      isRestricted.value = true;
+      restrictionLevelValue.value = form.value.restrictionLevel;
+    }
+
+    // 只有类型为文件夹时才获取标签列表
+    if (form.value.type === 0) {
+      await getKeywordList();
+    }
+
+    title.value = t('document.document.dialog.editFolder');
+    isEdit.value = true;
+    visible.value = true;
+  } catch (error) {
+    ElMessage.error(t('document.document.message.getFolderInfoFailed'));
+    console.error(error);
+  }
+};
+
+// 提交表单
+const submitForm = () => {
+  formRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      if (isEdit.value) {
+        try {
+          const confirmMessage = `
+            <div style="text-align: left;">
+              <p><strong>${t('document.document.confirm.nameLabel')}</strong>${form.value.name}</p>
+              <p><strong>${t('document.document.confirm.restrictionLevelLabel')}</strong>${form.value.restrictionLevel !== -1 ? form.value.restrictionLevel : t('document.document.confirm.noLimit')}</p>
+              <p><strong>${t('document.document.confirm.noteLabel')}</strong>${form.value.note || t('document.document.confirm.noNote')}</p>
+            </div>
+          `;
+          await ElMessageBox.confirm(confirmMessage, t('document.document.dialog.confirmEdit'), {
+            confirmButtonText: t('document.document.message.confirmButton'),
+            cancelButtonText: t('document.document.message.cancelButton'),
+            type: 'warning',
+            dangerouslyUseHTMLString: true
+          });
+        } catch {
+          return;
+        }
+      }
+
+      buttonLoading.value = true;
+      try {
+        if (isEdit.value) {
+          await updateFolder(form.value);
+          proxy?.$modal.msgSuccess(t('document.document.message.editSuccess'));
+        } else {
+          await addFolder(form.value);
+          proxy?.$modal.msgSuccess(t('document.document.message.addSuccess'));
+        }
+        visible.value = false;
+        emit('success');
+      } catch (error) {
+        console.error(isEdit.value ? t('document.document.message.editFailed') : t('document.document.message.addFailed'), error);
+      } finally {
+        buttonLoading.value = false;
+      }
+    }
+  });
+};
+
+// 关闭对话框
+const handleClose = () => {
+  reset();
+  visible.value = false;
+};
+
+defineExpose({
+  open,
+  openEdit
+});
+</script>
+
+<style scoped lang="scss">
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+</style>

+ 172 - 0
src/views/document/folder/document/components/MarkDialog.vue

@@ -0,0 +1,172 @@
+<template>
+  <el-dialog v-model="visible" :title="title" width="500px" append-to-body @close="handleClose">
+    <el-form ref="formRef" :model="form" :rules="rules" label-width="140px">
+      <el-form-item :label="t('document.document.markForm.specification')" prop="type">
+        <el-select
+          v-model="form.type"
+          :placeholder="t('document.document.markForm.specificationPlaceholder')"
+          clearable
+          style="width: 100%"
+        >
+          <el-option
+            v-for="dict in specificationDict"
+            :key="dict.value"
+            :label="parseI18nName(dict.label)"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="语言" prop="language">
+        <el-input
+          v-model="form.language"
+          placeholder="请输入语言"
+          clearable
+        />
+      </el-form-item>
+      <el-form-item label="版本" prop="version">
+        <el-input
+          v-model="form.version"
+          placeholder="请输入版本"
+          clearable
+        />
+      </el-form-item>
+      <el-form-item label="版本日期" prop="versionDate">
+        <el-date-picker
+          v-model="form.versionDate"
+          type="date"
+          placeholder="请选择版本日期"
+          style="width: 100%"
+          value-format="YYYY-MM-DD"
+        />
+      </el-form-item>
+      <el-form-item label="伦理递交日期" prop="ethicsSubmissionDate">
+        <el-date-picker
+          v-model="form.ethicsSubmissionDate"
+          type="date"
+          placeholder="请选择伦理递交日期"
+          style="width: 100%"
+          value-format="YYYY-MM-DD"
+        />
+      </el-form-item>
+      <el-form-item label="伦理批准日期" prop="ethicsApprovalDate">
+        <el-date-picker
+          v-model="form.ethicsApprovalDate"
+          type="date"
+          placeholder="请选择伦理批准日期"
+          style="width: 100%"
+          value-format="YYYY-MM-DD"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button :loading="loading" type="primary" @click="handleSubmit">
+          {{ t('document.document.button.submit') }}
+        </el-button>
+        <el-button @click="handleClose">{{ t('document.document.button.cancel') }}</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed, nextTick, watch } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { ElMessage } from 'element-plus';
+import type { FormInstance } from 'element-plus';
+import { markDocument } from '@/api/document/document';
+import { DocumentVO, DocumentMarkForm } from '@/api/document/document/types';
+import { parseI18nName } from '@/utils/i18n';
+
+interface Props {
+  modelValue: boolean;
+  document: DocumentVO | null;
+  specificationDict: any[];
+}
+
+const props = defineProps<Props>();
+const emit = defineEmits<{
+  'update:modelValue': [value: boolean];
+  success: [];
+}>();
+
+const { t } = useI18n();
+const formRef = ref<FormInstance>();
+const loading = ref(false);
+
+const visible = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+});
+
+const title = computed(() => t('document.document.dialog.markDocument'));
+
+const form = ref<DocumentMarkForm>({
+  id: 0,
+  type: '',
+  language: '',
+  version: '',
+  versionDate: '',
+  ethicsSubmissionDate: '',
+  ethicsApprovalDate: ''
+});
+
+const rules = reactive({
+  type: [{ required: true, message: t('document.document.markRule.typeRequired'), trigger: 'change' }]
+});
+
+// 监听 document 变化,初始化表单
+watch(
+  () => props.document,
+  (newDoc) => {
+    if (newDoc) {
+      form.value = {
+        id: newDoc.id,
+        type: '',
+        language: '',
+        version: '',
+        versionDate: '',
+        ethicsSubmissionDate: '',
+        ethicsApprovalDate: ''
+      };
+      nextTick(() => {
+        formRef.value?.clearValidate();
+      });
+    }
+  },
+  { immediate: true }
+);
+
+const handleClose = () => {
+  visible.value = false;
+  form.value = {
+    id: 0,
+    type: '',
+    language: '',
+    version: '',
+    versionDate: '',
+    ethicsSubmissionDate: '',
+    ethicsApprovalDate: ''
+  };
+};
+
+const handleSubmit = async () => {
+  if (!formRef.value) return;
+
+  try {
+    await formRef.value.validate();
+    loading.value = true;
+
+    await markDocument(form.value);
+    ElMessage.success(t('document.document.message.markSuccess'));
+    handleClose();
+    emit('success');
+  } catch (error) {
+    if (error !== false) {
+      console.error(t('document.document.message.markFailed'), error);
+    }
+  } finally {
+    loading.value = false;
+  }
+};
+</script>

+ 376 - 0
src/views/document/folder/document/components/SpecifyDialog.vue

@@ -0,0 +1,376 @@
+<template>
+  <el-dialog v-model="visible" :title="title" width="900px" append-to-body @close="handleClose">
+    <el-form ref="formRef" :model="form" :rules="rules" label-width="140px">
+      <el-form-item :label="t('document.document.specifyForm.type')" prop="type">
+        <el-radio-group v-model="form.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-item label="语言" prop="language">
+        <el-input v-model="form.language" placeholder="请输入语言" clearable style="width: 100%" />
+      </el-form-item>
+      <el-form-item label="版本" prop="version">
+        <el-input v-model="form.version" placeholder="请输入版本" clearable style="width: 100%" />
+      </el-form-item>
+      <el-form-item label="版本日期" prop="versionDate">
+        <el-date-picker
+          v-model="form.versionDate"
+          type="date"
+          value-format="YYYY-MM-DD"
+          placeholder="请选择版本日期"
+          style="width: 100%"
+        />
+      </el-form-item>
+      <el-form-item label="伦理递交日期" prop="ethicsSubmissionDate">
+        <el-date-picker
+          v-model="form.ethicsSubmissionDate"
+          type="date"
+          value-format="YYYY-MM-DD"
+          placeholder="请选择伦理递交日期"
+          style="width: 100%"
+        />
+      </el-form-item>
+      <el-form-item label="伦理批准日期" prop="ethicsApprovalDate">
+        <el-date-picker
+          v-model="form.ethicsApprovalDate"
+          type="date"
+          value-format="YYYY-MM-DD"
+          placeholder="请选择伦理批准日期"
+          style="width: 100%"
+        />
+      </el-form-item>
+    </el-form>
+
+    <!-- 指定递交缺失时显示文档列表 -->
+    <div v-if="form.type === 'missing'" style="margin-top: 20px">
+      <!-- 搜索栏 -->
+      <el-form :inline="true" style="margin-bottom: 10px">
+        <el-form-item>
+          <el-input
+            v-model="searchName"
+            :placeholder="t('document.document.specifyForm.searchPlaceholder')"
+            clearable
+            style="width: 240px"
+            @keyup.enter="handleSearch"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="Search" @click="handleSearch">
+            {{ t('document.document.button.search') }}
+          </el-button>
+          <el-button icon="Refresh" @click="handleReset">{{ t('document.document.button.reset') }}</el-button>
+        </el-form-item>
+      </el-form>
+
+      <!-- 文档列表 -->
+      <el-table v-loading="documentLoading" :data="documentList" 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="documentTotal > 0"
+        v-model:page="queryParams.pageNum"
+        v-model:limit="queryParams.pageSize"
+        :total="documentTotal"
+        @pagination="getDocumentList"
+      />
+    </div>
+
+    <!-- 指定文件夹时显示文件夹树 -->
+    <div v-else-if="form.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="loading" type="primary" @click="handleSubmit">
+          {{ t('document.document.button.submit') }}
+        </el-button>
+        <el-button @click="handleClose">{{ t('document.document.button.cancel') }}</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed, nextTick, watch } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { ElMessage } from 'element-plus';
+import type { FormInstance } from 'element-plus';
+import { listDocumentOnSpecify, specifyDocument } from '@/api/document/document';
+import { DocumentVO, DocumentSpecifyVO, DocumentSpecifyForm } from '@/api/document/document/types';
+import { FolderListVO } from '@/api/document/folder/types';
+import { parseTime } from '@/utils/ruoyi';
+import DocumentStatusTag from '@/components/DocumentStatusTag/index.vue';
+import FolderSelector from '@/components/FolderSelector/index.vue';
+
+interface Props {
+  modelValue: boolean;
+  document: DocumentVO | null;
+  treeData: FolderListVO[];
+  projectId?: number | string;
+}
+
+const props = defineProps<Props>();
+const emit = defineEmits<{
+  'update:modelValue': [value: boolean];
+  success: [];
+}>();
+
+const { t } = useI18n();
+const formRef = ref<FormInstance>();
+const folderSelectorRef = ref();
+const loading = ref(false);
+
+const visible = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+});
+
+const title = computed(() => t('document.document.dialog.specifyDocument'));
+
+const form = ref({
+  type: 'missing',
+  folderId: undefined as number | undefined,
+  language: '',
+  version: '',
+  versionDate: '',
+  ethicsSubmissionDate: '',
+  ethicsApprovalDate: ''
+});
+
+const rules = reactive({
+  type: [
+    {
+      required: true,
+      message: t('document.document.specifyRule.typeRequired'),
+      trigger: 'change'
+    }
+  ]
+});
+
+// 文档列表相关
+const documentList = ref<DocumentSpecifyVO[]>([]);
+const documentLoading = ref(false);
+const documentTotal = ref(0);
+const searchName = ref('');
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  projectId: props.projectId,
+  name: ''
+});
+
+const selectedDocumentId = ref<string | number | null>(null);
+const selectedFolderId = ref<number | string | undefined>(undefined);
+
+// 获取可指定的文档列表
+const getDocumentList = async () => {
+  if (!props.projectId) return;
+
+  documentLoading.value = true;
+  try {
+    const res = await listDocumentOnSpecify(queryParams);
+    documentList.value = res.rows || [];
+    documentTotal.value = res.total || 0;
+  } catch (error) {
+    console.error('获取可指定文档列表失败:', error);
+    ElMessage.error('获取可指定文档列表失败');
+  } finally {
+    documentLoading.value = false;
+  }
+};
+
+// 搜索
+const handleSearch = () => {
+  queryParams.name = searchName.value;
+  queryParams.pageNum = 1;
+  getDocumentList();
+};
+
+// 重置
+const handleReset = () => {
+  searchName.value = '';
+  queryParams.name = '';
+  queryParams.pageNum = 1;
+  getDocumentList();
+};
+
+// 选择文档
+const handleSelectDocument = (id: string | number) => {
+  selectedDocumentId.value = id;
+};
+
+// 文件夹选择变化
+const handleFolderChange = (folderId: number | string | undefined) => {
+  selectedFolderId.value = folderId;
+};
+
+// 监听类型变化
+watch(
+  () => form.value.type,
+  (newType) => {
+    selectedDocumentId.value = null;
+    selectedFolderId.value = undefined;
+    if (newType === 'missing' && visible.value) {
+      getDocumentList();
+    } else if (newType === 'folder' && folderSelectorRef.value) {
+      folderSelectorRef.value.clearSelection();
+    }
+  }
+);
+
+// 监听 document 变化,初始化表单
+watch(
+  () => props.document,
+  (newDoc) => {
+    if (newDoc) {
+      form.value = {
+        type: 'missing',
+        folderId: undefined,
+        language: '',
+        version: '',
+        versionDate: '',
+        ethicsSubmissionDate: '',
+        ethicsApprovalDate: ''
+      };
+      selectedDocumentId.value = null;
+      selectedFolderId.value = undefined;
+      searchName.value = '';
+      queryParams.pageNum = 1;
+      queryParams.name = '';
+      queryParams.projectId = props.projectId;
+
+      nextTick(() => {
+        formRef.value?.clearValidate();
+        if (form.value.type === 'missing') {
+          getDocumentList();
+        }
+      });
+    }
+  },
+  { immediate: true }
+);
+
+const handleClose = () => {
+  visible.value = false;
+  form.value = {
+    type: 'missing',
+    folderId: undefined,
+    language: '',
+    version: '',
+    versionDate: '',
+    ethicsSubmissionDate: '',
+    ethicsApprovalDate: ''
+  };
+  selectedDocumentId.value = null;
+  selectedFolderId.value = undefined;
+  documentList.value = [];
+};
+
+const handleSubmit = async () => {
+  if (!formRef.value || !props.document) return;
+
+  try {
+    await formRef.value.validate();
+
+    // 如果是指定递交缺失,需要选择一个文档
+    if (form.value.type === 'missing' && !selectedDocumentId.value) {
+      ElMessage.warning('请选择一个文档');
+      return;
+    }
+
+    // 如果是指定文件夹,需要选择一个文件夹
+    if (form.value.type === 'folder' && !selectedFolderId.value) {
+      ElMessage.warning('请选择一个文件夹');
+      return;
+    }
+
+    loading.value = true;
+
+    // 构建请求数据
+    const specifyData: DocumentSpecifyForm = {
+      documentId: props.document.id
+    };
+
+    if (form.value.type === 'missing') {
+      specifyData.missingDocumentId = selectedDocumentId.value as number | string;
+    } else if (form.value.type === 'folder') {
+      specifyData.folderId = selectedFolderId.value;
+    }
+
+    await specifyDocument(specifyData);
+    ElMessage.success('指定成功');
+    handleClose();
+    emit('success');
+  } catch (error) {
+    if (error !== false) {
+      console.error('指定失败:', error);
+      ElMessage.error('指定失败');
+    }
+  } finally {
+    loading.value = false;
+  }
+};
+</script>

+ 127 - 0
src/views/document/folder/document/components/SubmitDialog.vue

@@ -0,0 +1,127 @@
+<template>
+  <el-dialog v-model="visible" :title="title" width="500px" append-to-body @close="handleClose">
+    <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
+      <el-form-item :label="t('document.document.submitForm.file')" prop="actualDocument">
+        <fileUpload v-model="form.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="form.effectiveDate"
+          type="date"
+          value-format="YYYY-MM-DD"
+          :placeholder="t('document.document.submitForm.effectiveDatePlaceholder')"
+          style="width: 100%"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button :loading="loading" type="primary" @click="handleSubmit">
+          {{ t('document.document.button.submit') }}
+        </el-button>
+        <el-button @click="handleClose">{{ t('document.document.button.cancel') }}</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed, nextTick, watch } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { ElMessage } from 'element-plus';
+import type { FormInstance } from 'element-plus';
+import { submitDocument } from '@/api/document/document';
+import { DocumentVO, DocumentSubmitForm } from '@/api/document/document/types';
+import fileUpload from '@/components/FileUpload/index.vue';
+
+interface Props {
+  modelValue: boolean;
+  document: DocumentVO | null;
+}
+
+const props = defineProps<Props>();
+const emit = defineEmits<{
+  'update:modelValue': [value: boolean];
+  success: [];
+}>();
+
+const { t } = useI18n();
+const formRef = ref<FormInstance>();
+const loading = ref(false);
+
+const visible = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+});
+
+const title = computed(() => t('document.document.dialog.submitDocument'));
+
+const form = ref({
+  actualDocument: '',
+  effectiveDate: undefined as string | undefined
+});
+
+const rules = reactive({
+  actualDocument: [
+    {
+      required: true,
+      message: t('document.document.submitRule.fileRequired'),
+      trigger: 'blur'
+    }
+  ]
+});
+
+// 监听 document 变化,初始化表单
+watch(
+  () => props.document,
+  (newDoc) => {
+    if (newDoc) {
+      form.value = {
+        actualDocument: '',
+        effectiveDate: undefined
+      };
+      nextTick(() => {
+        formRef.value?.clearValidate();
+      });
+    }
+  },
+  { immediate: true }
+);
+
+const handleClose = () => {
+  visible.value = false;
+  form.value = {
+    actualDocument: '',
+    effectiveDate: undefined
+  };
+};
+
+const handleSubmit = async () => {
+  if (!formRef.value || !props.document) return;
+
+  try {
+    await formRef.value.validate();
+    loading.value = true;
+
+    const submitData: DocumentSubmitForm = {
+      documentId: props.document.id,
+      ossId: form.value.actualDocument,
+      effectiveDate: form.value.effectiveDate
+    };
+
+    await submitDocument(submitData);
+    ElMessage.success(t('document.document.message.submitSuccess'));
+    handleClose();
+    emit('success');
+  } catch (error) {
+    if (error !== false) {
+      console.error(t('document.document.message.submitFailed'), error);
+    }
+  } finally {
+    loading.value = false;
+  }
+};
+</script>

+ 4 - 0
src/views/document/folder/document/components/index.ts

@@ -0,0 +1,4 @@
+export { default as MarkDialog } from './MarkDialog.vue';
+export { default as SubmitDialog } from './SubmitDialog.vue';
+export { default as AuditDialog } from './AuditDialog.vue';
+export { default as SpecifyDialog } from './SpecifyDialog.vue';

+ 5 - 334
src/views/document/folder/document/index.vue

@@ -11,7 +11,7 @@
       <div class="content-wrapper">
         <!-- 文件夹树组件 -->
         <FolderTree ref="folderTreeRef" :project-id="projectId" @folder-click="handleFolderClick"
-          @add-document="handleAddDocument" @refresh="handleTreeRefresh" />
+          @refresh="handleTreeRefresh" />
 
         <!-- 文档列表组件 -->
         <div class="content-container">
@@ -20,107 +20,14 @@
         </div>
       </div>
     </el-card>
-
-    <!-- 添加文档对话框 -->
-    <el-dialog v-model="documentDialog.visible" :title="documentDialog.title" width="700px" append-to-body>
-      <el-form ref="documentFormRef" :model="documentForm" :rules="documentRules" label-width="140px">
-        <el-form-item
-          :label="documentForm.type === 1 ? t('document.document.documentForm.planName') : t('document.document.documentForm.name')"
-          prop="name">
-          <div style="display: flex; align-items: center; gap: 8px; width: 100%;">
-            <span v-if="documentNamePrefix" style="white-space: nowrap; color: #606266;">{{ documentNamePrefix }}</span>
-            <el-input v-model="documentNameInput" :placeholder="t('document.document.documentForm.namePlaceholder')"
-              clearable style="flex: 1;" @input="handleNameInputChange" />
-            <span v-if="documentNamePrefix" style="white-space: nowrap; color: #606266;">-</span>
-            <el-date-picker v-model="documentForm.documentDate" type="date" value-format="YYYYMMDD" format="YYYYMMDD"
-              placeholder="请选择生效日期" style="width: 180px;" @change="handleDocumentDateChange" />
-          </div>
-        </el-form-item>
-
-        <el-form-item :label="t('document.document.documentForm.type')" prop="type" v-if="hasAddPlanPermission">
-          <el-radio-group v-model="documentForm.type" @change="handleDocumentTypeChange">
-            <el-radio :label="0">{{ t('document.document.documentForm.normalDocument') }}</el-radio>
-            <el-radio :label="1">{{ t('document.document.documentForm.planDocument') }}</el-radio>
-          </el-radio-group>
-        </el-form-item>
-
-        <el-form-item v-if="documentForm.type === 1" :label="t('document.document.documentForm.planSubmitter')"
-          prop="planSubmitter">
-          <el-select v-model="documentForm.planSubmitter" filterable remote reserve-keyword
-            :placeholder="t('document.document.documentForm.planSubmitterPlaceholder')"
-            :remote-method="searchSubmitters" :loading="submitterSearchLoading" style="width: 100%">
-            <el-option v-for="submitter in submitterOptions" :key="submitter.id"
-              :label="`${submitter.name} / ${submitter.dept} --- ${submitter.phoneNumber}`" :value="submitter.id" />
-          </el-select>
-        </el-form-item>
-
-        <el-form-item v-if="documentForm.type === 1" :label="t('document.document.documentForm.submitDeadline')"
-          prop="submitDeadline">
-          <el-date-picker v-model="documentForm.submitDeadline" type="date" value-format="YYYY-MM-DD"
-            :placeholder="t('document.document.documentForm.submitDeadlinePlaceholder')" style="width: 100%" />
-        </el-form-item>
-
-        <el-form-item v-if="documentForm.type === 1" :label="t('document.document.documentForm.planType')"
-          prop="planType">
-          <el-select v-model="documentForm.planType"
-            :placeholder="t('document.document.documentForm.planTypePlaceholder')" clearable style="width: 100%">
-            <el-option v-for="dict in plan_document_type" :key="dict.value" :label="parseI18nName(dict.label)"
-              :value="dict.value" />
-          </el-select>
-        </el-form-item>
-
-        <el-form-item v-if="documentForm.type === 0" :label="t('document.document.documentForm.actualFile')"
-          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')">
-          <el-input v-model="documentForm.submitTime" disabled />
-        </el-form-item>
-
-        <el-form-item :label="t('document.document.documentForm.sendFlag')" prop="sendFlag">
-          <el-switch v-model="documentForm.sendFlag" :active-value="true" :inactive-value="false" />
-        </el-form-item>
-
-        <el-form-item :label="t('document.document.documentForm.note')" prop="note">
-          <el-input v-model="documentForm.note" type="textarea" :rows="4"
-            :placeholder="t('document.document.documentForm.notePlaceholder')" />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button :loading="documentButtonLoading" type="primary" @click="submitDocumentForm">{{
-            t('document.document.button.confirm')
-          }}</el-button>
-          <el-button @click="cancelDocument">{{ t('document.document.button.cancel') }}</el-button>
-        </div>
-      </template>
-    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, getCurrentInstance, watch, computed, toRefs, onMounted } from 'vue';
+import { ref, watch, computed, onMounted, getCurrentInstance, type ComponentInternalInstance } 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';
-import { queryMemberNotInCenter } from '@/api/project/management';
-import { MemberNotInCenterVO, MemberNotInCenterQuery } from '@/api/project/management/types';
-import { ElMessage } from 'element-plus';
-import type { FormInstance } from 'element-plus';
-import type { ComponentInternalInstance } from 'vue';
-import { useUserStore } from '@/store/modules/user';
-import { checkPermi } from '@/utils/permission';
-import { parseI18nName } from '@/utils/i18n';
-import fileUpload from '@/components/FileUpload/index.vue';
 import FolderTree from './FolderTree.vue';
 import DocumentList from './DocumentList.vue';
 
@@ -128,8 +35,6 @@ 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(() => {
@@ -143,7 +48,6 @@ const projectId = computed(() => {
 // 组件引用
 const folderTreeRef = ref<InstanceType<typeof FolderTree>>();
 const documentListRef = ref<InstanceType<typeof DocumentList>>();
-const documentFormRef = ref<FormInstance>();
 
 // 当前选中的文件夹
 const selectedFolder = ref<FolderListVO | null>(null);
@@ -151,88 +55,6 @@ const selectedFolder = ref<FolderListVO | null>(null);
 // 树数据(用于传递给DocumentList)
 const treeData = ref<FolderListVO[]>([]);
 
-// 文档对话框
-const documentDialog = reactive({
-  visible: false,
-  title: ''
-});
-
-const documentButtonLoading = ref(false);
-
-// 检查是否有计划文档添加权限
-const hasAddPlanPermission = computed(() => checkPermi(['document:document:addPlan']));
-
-// 文档表单初始数据
-// 文档表单初始数据
-const initDocumentFormData: DocumentForm = {
-  id: undefined,
-  name: '',
-  type: 0,
-  planSubmitter: undefined,
-  folderId: undefined,
-  submitDeadline: undefined,
-  planType: undefined,
-  actualDocument: undefined,
-  submitTime: undefined,
-  projectId: projectId.value,
-  note: '',
-  sendFlag: false,
-  effectiveDate: undefined
-};
-
-// 文件上传的actualDocument
-const uploadedFileId = ref<string>('');
-
-// 文档表单数据
-const documentForm = ref<DocumentForm & { documentDate?: string }>({ ...initDocumentFormData });
-
-// 存储项目信息前缀(用于拼接文档名称)
-const documentNamePrefix = ref<string>('');
-
-// 用户输入的名称部分(不包含前缀和日期)
-const documentNameInput = ref<string>('');
-
-// 处理名称输入变化
-const handleNameInputChange = (value: string) => {
-  documentNameInput.value = value;
-  updateDocumentName();
-};
-
-// 处理文档日期变化
-const handleDocumentDateChange = (value: string) => {
-  updateDocumentName();
-};
-
-// 更新文档名称
-const updateDocumentName = () => {
-  if (!documentNamePrefix.value) {
-    // 如果没有前缀,直接使用用户输入
-    documentForm.value.name = documentNameInput.value;
-    return;
-  }
-
-  // 拼接完整名称:{projectName}-{centerId}-{用户输入}-{日期}
-  let fullName = documentNamePrefix.value + documentNameInput.value;
-
-  if (documentForm.value.documentDate) {
-    fullName += `-${documentForm.value.documentDate}`;
-  }
-
-  documentForm.value.name = fullName;
-};
-
-// 递交人搜索相关
-const submitterSearchLoading = ref(false);
-const submitterOptions = ref<MemberNotInCenterVO[]>([]);
-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' }],
-  actualDocument: [{ required: true, message: t('document.document.documentRule.fileRequired'), trigger: 'change' }]
-};
-
 // 返回项目列表
 const handleBack = () => {
   router.push('/document/folder');
@@ -250,24 +72,6 @@ const handleFolderClick = (folder: FolderListVO) => {
   handleTreeRefresh();
 };
 
-// 处理添加文档
-const handleAddDocument = (folder: FolderListVO, projectInfo?: { projectName: string; centerId: number }) => {
-  resetDocumentForm();
-  documentForm.value.folderId = folder.id;
-
-  // 如果有项目信息,设置文档名称前缀为:{projectName}-{centerId}-
-  if (projectInfo) {
-    documentNamePrefix.value = `${projectInfo.projectName}-${projectInfo.centerId}-`;
-    // 初始化时更新一次名称
-    updateDocumentName();
-  } else {
-    documentNamePrefix.value = '';
-  }
-
-  documentDialog.visible = true;
-  documentDialog.title = t('document.document.dialog.addDocument');
-};
-
 // 处理树刷新
 const handleTreeRefresh = () => {
   // 刷新树数据以传递给DocumentList
@@ -277,143 +81,10 @@ const handleTreeRefresh = () => {
     // 刷新临时文件夹数量
     (folderTreeRef.value as any).getTempDocumentCount?.();
   }
-};
-
-// 重置文档表单
-const resetDocumentForm = () => {
-  documentForm.value = { ...initDocumentFormData };
-  uploadedFileId.value = '';
-  documentNamePrefix.value = '';
-  documentNameInput.value = '';
-  if (!hasAddPlanPermission.value) {
-    documentForm.value.type = 0;
-  }
-  // 非计划文件,计划递交人默认为空,递交人为自己
-  if (documentForm.value.type === 0) {
-    documentForm.value.planSubmitter = undefined;
-    documentForm.value.submitter = userStore.userId;
-  } else {
-    // 计划文件,计划递交人需要填写,递交人不默认
-    documentForm.value.planSubmitter = undefined;
-    documentForm.value.submitter = undefined;
-  }
-  documentForm.value.projectId = projectId.value;
-  submitterOptions.value = [];
-  documentFormRef.value?.resetFields();
-};
-
-// 处理文档类型变化
-const handleDocumentTypeChange = (value: number) => {
-  if (value === 0) {
-    // 非计划文件,计划递交人默认为空,递交人为自己
-    documentForm.value.planSubmitter = undefined;
-    documentForm.value.submitter = userStore.userId;
-    documentForm.value.submitDeadline = undefined;
-    documentForm.value.planType = undefined;
-  } else {
-    // 计划文件,计划递交人需要填写,递交人不默认
-    documentForm.value.planSubmitter = undefined;
-    documentForm.value.submitter = undefined;
+  // 刷新文档列表
+  if (documentListRef.value) {
+    (documentListRef.value as any).getDocumentList?.();
   }
-}
-
-// 搜索递交人
-const searchSubmitters = async (query: string) => {
-  if (!query || query.trim() === '') {
-    submitterOptions.value = [];
-    return;
-  }
-
-  if (submitterSearchTimer) {
-    clearTimeout(submitterSearchTimer);
-  }
-
-  submitterSearchTimer = setTimeout(async () => {
-    submitterSearchLoading.value = true;
-    try {
-      const queryParams: MemberNotInCenterQuery = {
-        pageNum: 1,
-        pageSize: 10,
-        projectId: projectId.value || 0,
-        folderId: 0,
-        name: query
-      };
-      const res = await queryMemberNotInCenter(queryParams);
-      submitterOptions.value = res.rows || [];
-    } catch (error) {
-      console.error('Failed to search submitters:', error);
-      ElMessage.error(t('document.document.message.searchSubmitterFailed'));
-    } finally {
-      submitterSearchLoading.value = false;
-    }
-  }, 300);
-};
-
-// 监听文件上传变化
-watch(uploadedFileId, (newVal) => {
-  if (newVal) {
-    const ids = newVal.split(',').filter((id) => id.trim());
-    if (ids.length > 0) {
-      documentForm.value.actualDocument = parseInt(ids[0]);
-      const now = new Date();
-      documentForm.value.submitTime = proxy?.parseTime(now, '{y}-{m}-{d} {h}:{i}:{s}');
-
-      // 设置递交人为当前用户
-      documentForm.value.submitter = userStore.userId;
-    }
-  } else {
-    documentForm.value.actualDocument = undefined;
-    documentForm.value.submitTime = undefined;
-    documentForm.value.submitter = undefined;
-  }
-});
-
-// 取消文档对话框
-const cancelDocument = () => {
-  resetDocumentForm();
-  documentDialog.visible = false;
-};
-
-// 提交文档表单
-const submitDocumentForm = () => {
-  documentFormRef.value?.validate(async (valid: boolean) => {
-    if (valid) {
-      documentButtonLoading.value = true;
-      try {
-        const hasUploadedFile = !!uploadedFileId.value;
-
-        const submitData: DocumentForm = {
-          id: documentForm.value.id || 0,
-          name: documentForm.value.name || '',
-          type: documentForm.value.type !== undefined ? documentForm.value.type : 0,
-          // 非计划文件:计划递交人默认为空,递交人为当前用户
-          // 计划文件:计划递交人需要填写,递交人不默认
-          planSubmitter: documentForm.value.type === 0 ? undefined : documentForm.value.planSubmitter,
-          submitter: documentForm.value.type === 0 ? userStore.userId : documentForm.value.submitter,
-          folderId: documentForm.value.folderId || 0,
-          submitDeadline: documentForm.value.submitDeadline || '',
-          planType: documentForm.value.planType || '',
-          actualDocument: hasUploadedFile ? uploadedFileId.value : null,
-          submitTime: documentForm.value.submitTime || (hasUploadedFile ? new Date().toISOString() : ''),
-          projectId: documentForm.value.projectId || projectId.value,
-          status: hasUploadedFile ? 1 : 0,
-          note: documentForm.value.note || '',
-          sendFlag: documentForm.value.sendFlag !== undefined ? documentForm.value.sendFlag : false,
-          effectiveDate: documentForm.value.effectiveDate || ''
-        };
-
-        await addDocument(submitData);
-        proxy?.$modal.msgSuccess(t('document.document.message.addDocumentSuccess'));
-        documentDialog.visible = false;
-        // 刷新文档列表
-        documentListRef.value?.getDocumentList();
-      } catch (error) {
-        console.error(t('document.document.message.addDocumentFailed'), error);
-      } finally {
-        documentButtonLoading.value = false;
-      }
-    }
-  });
 };
 
 // 监听路由参数变化

+ 2 - 1
src/views/home/taskCenter/audit/index.vue

@@ -30,7 +30,7 @@
         <el-table-column prop="name" label="名称" min-width="150" show-overflow-tooltip>
           <template #default="scope">
             <el-badge v-if="scope.row.status === 1" is-dot class="status-badge" />
-            <span>{{ scope.row.name }}</span>
+            <span>{{ formatDocumentName(scope.row.name) }}</span>
           </template>
         </el-table-column>
         <el-table-column prop="type" label="类型" min-width="120" show-overflow-tooltip>
@@ -133,6 +133,7 @@ import type { FormInstance } from 'element-plus';
 import { listAuditTasks, auditDocument } from '@/api/home/taskCenter/audit';
 import { AuditTaskVO, AuditTaskAuditForm } from '@/api/home/taskCenter/audit/types';
 import { downloadDocumentFile } from '@/api/document/document';
+import { formatDocumentName } from '@/utils/ruoyi';
 import { Edit, Download, Check } from '@element-plus/icons-vue';
 import DictTag from '@/components/DictTag/index.vue';
 import DocumentStatusTag from '@/components/DocumentStatusTag/index.vue';

+ 2 - 1
src/views/home/taskCenter/filing/index.vue

@@ -37,7 +37,7 @@
         <el-table-column prop="name" label="名称" min-width="150" show-overflow-tooltip>
           <template #default="scope">
             <el-badge v-if="scope.row.status === 3" is-dot class="status-badge" />
-            <span>{{ scope.row.name }}</span>
+            <span>{{ formatDocumentName(scope.row.name) }}</span>
           </template>
         </el-table-column>
         <el-table-column prop="type" label="类型" min-width="120" show-overflow-tooltip>
@@ -106,6 +106,7 @@ import { ElMessage } from 'element-plus';
 import { listFilingTasks, filingDocument } from '@/api/home/taskCenter/filing';
 import { FilingTaskVO, FilingTaskFilingForm } from '@/api/home/taskCenter/filing/types';
 import { downloadDocumentFile } from '@/api/document/document';
+import { formatDocumentName } from '@/utils/ruoyi';
 import { FolderAdd, Download } from '@element-plus/icons-vue';
 import DictTag from '@/components/DictTag/index.vue';
 import DocumentStatusTag from '@/components/DocumentStatusTag/index.vue';

+ 6 - 1
src/views/home/taskCenter/qc/index.vue

@@ -37,7 +37,11 @@
       <!-- 任务列表 -->
       <el-table v-loading="loading" :data="taskList" border style="margin-top: 10px">
         <el-table-column prop="id" label="序号" width="80" align="center" />
-        <el-table-column prop="name" label="任务名称" min-width="150" show-overflow-tooltip />
+        <el-table-column prop="name" label="任务名称" min-width="150" show-overflow-tooltip>
+          <template #default="scope">
+            {{ formatDocumentName(scope.row.name) }}
+          </template>
+        </el-table-column>
         <el-table-column prop="projectName" label="项目名称" min-width="150" show-overflow-tooltip />
         <el-table-column prop="initiator" label="发起人" width="120" align="center" />
         <el-table-column prop="executor" label="执行人" width="120" align="center" />
@@ -191,6 +195,7 @@ import { QcTaskVO, QcTaskQuery, QcTaskSubmitForm, QcTaskAuditForm } from '@/api/
 import { queryMemberNotInCenter } from '@/api/project/management';
 import { MemberNotInCenterVO, MemberNotInCenterQuery } from '@/api/project/management/types';
 import { parseI18nName } from '@/utils/i18n';
+import { formatDocumentName } from '@/utils/ruoyi';
 import fileUpload from '@/components/FileUpload/index.vue';
 
 const { proxy } = getCurrentInstance() as any;

+ 2 - 1
src/views/home/taskCenter/submission/index.vue

@@ -43,7 +43,7 @@
         <el-table-column prop="name" label="名称" min-width="150" show-overflow-tooltip>
           <template #default="scope">
             <el-badge v-if="scope.row.status === 0 || scope.row.status === 2" is-dot class="badge-dot" />
-            <span>{{ scope.row.name }}</span>
+            <span>{{ formatDocumentName(scope.row.name) }}</span>
           </template>
         </el-table-column>
         <el-table-column prop="type" label="类型" min-width="120" show-overflow-tooltip>
@@ -152,6 +152,7 @@ import { ref, reactive, onMounted, nextTick, getCurrentInstance } from 'vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import type { FormInstance } from 'element-plus';
 import { listSubmissionTasks, submitDocument, listSubmissionAuditLog, sendDocument } from '@/api/home/taskCenter/submission';
+import { formatDocumentName } from '@/utils/ruoyi';
 import fileUpload from '@/components/FileUpload/index.vue';
 import DictTag from '@/components/DictTag/index.vue';
 import DocumentStatusTag from '@/components/DocumentStatusTag/index.vue';

+ 62 - 3
src/views/search/index.vue

@@ -54,6 +54,15 @@
         <el-form-item>
           <el-button type="primary" icon="Search" @click="handleQuery">{{ t('search.button.search') }}</el-button>
           <el-button icon="Refresh" @click="resetQuery">{{ t('search.button.reset') }}</el-button>
+          <el-button
+            v-hasPermi="['search:index:export']"
+            type="warning"
+            plain
+            icon="Download"
+            @click="handleExport"
+          >
+            {{ t('search.button.export') }}
+          </el-button>
         </el-form-item>
       </el-form>
 
@@ -62,7 +71,11 @@
         <el-table-column prop="id" :label="t('search.table.id')" width="80" align="center" />
         <el-table-column prop="folderName" :label="t('search.table.folderName')" min-width="120"
           show-overflow-tooltip />
-        <el-table-column prop="name" :label="t('search.table.name')" min-width="150" show-overflow-tooltip />
+        <el-table-column prop="name" :label="t('search.table.name')" min-width="150" show-overflow-tooltip>
+          <template #default="scope">
+            {{ formatDocumentName(scope.row.name) }}
+          </template>
+        </el-table-column>
         <el-table-column prop="type" :label="t('search.table.type')" width="120" align="center">
           <template #default="scope">
             <span v-if="scope.row.type === 0">{{ t('search.type.normalDocument') }}</span>
@@ -149,11 +162,11 @@
 import { ref, reactive, onMounted, getCurrentInstance, toRefs } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { ElMessage } from 'element-plus';
-import { listSearchDocuments } from '@/api/search';
+import { listSearchDocuments, exportSearchDocuments } from '@/api/search';
 import { downloadDocumentFile } from '@/api/document/document';
 import DocumentStatusTag from '@/components/DocumentStatusTag/index.vue';
 import { parseI18nName } from '@/utils/i18n';
-import { parseTime } from '@/utils/ruoyi';
+import { parseTime, formatDocumentName } from '@/utils/ruoyi';
 
 const { t } = useI18n();
 const { proxy } = getCurrentInstance() as any;
@@ -298,6 +311,52 @@ const handleDownload = async (actualDocument: number, actualDocumentName: string
   await downloadDocumentFile(actualDocument, actualDocumentName);
 };
 
+/**
+ * 导出按钮操作
+ */
+const handleExport = async () => {
+  try {
+    loading.value = true;
+    
+    // 构建导出参数
+    const exportParams: any = {
+      name: queryParams.name,
+      projectCode: queryParams.projectCode,
+      projectName: queryParams.projectName,
+      type: queryParams.type,
+      status: queryParams.status,
+      params: {}
+    };
+
+    // 处理时间范围
+    proxy?.addDateRange(exportParams, createTimeRange.value, 'CreateTime', 'createTimeEarliest', 'createTimeLatest');
+    proxy?.addDateRange(exportParams, updateTimeRange.value, 'UpdateTime', 'updateTimeEarliest', 'updateTimeLatest');
+
+    // 根据当前语言环境设置文件名
+    const fileName = t('search.exportFileName');
+
+    // 调用导出接口
+    const blob = await exportSearchDocuments(exportParams);
+    
+    // 下载文件
+    const url = window.URL.createObjectURL(new Blob([blob]));
+    const link = document.createElement('a');
+    link.href = url;
+    link.setAttribute('download', `${fileName}.xlsx`);
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+    window.URL.revokeObjectURL(url);
+    
+    ElMessage.success(t('search.message.exportSuccess'));
+  } catch (error) {
+    console.error('导出失败:', error);
+    ElMessage.error(t('search.message.exportFailed'));
+  } finally {
+    loading.value = false;
+  }
+};
+
 onMounted(() => {
   getList();
 });