ソースを参照

- 文档管理
- 下载文档修改完成
- 首页
- 递交任务、审核任务均已完成
- 文件检索初步完成
- 添加AI设置

Huanyi 2 日 前
コミット
49fab1b499
33 ファイル変更3105 行追加2016 行削除
  1. 51 0
      src/api/document/document/index.ts
  2. 12 0
      src/api/home/taskCenter/submission/index.ts
  3. 12 0
      src/api/search/index.ts
  4. 23 0
      src/api/setting/ai/index.ts
  5. 13 0
      src/api/setting/ai/types.ts
  6. 188 0
      src/components/AuditLogDialog/index.vue
  7. 21 0
      src/components/DocumentStatusTag/index.vue
  8. 77 0
      src/enums/documentStatus.ts
  9. 7 1
      src/lang/en_US.ts
  10. 14 5
      src/lang/modules/document/document/en_US.ts
  11. 2 1
      src/lang/modules/document/document/zh_CN.ts
  12. 6 0
      src/lang/modules/home/index.ts
  13. 6 0
      src/lang/modules/home/index_en.ts
  14. 146 0
      src/lang/modules/home/taskCenter/en_US.ts
  15. 143 0
      src/lang/modules/home/taskCenter/zh_CN.ts
  16. 59 0
      src/lang/modules/search/en_US.ts
  17. 59 0
      src/lang/modules/search/zh_CN.ts
  18. 22 0
      src/lang/modules/setting/ai/en_US.ts
  19. 22 0
      src/lang/modules/setting/ai/zh_CN.ts
  20. 3 1
      src/lang/modules/setting/index.ts
  21. 3 1
      src/lang/modules/setting/index_en.ts
  22. 7 1
      src/lang/zh_CN.ts
  23. 24 24
      src/utils/request.ts
  24. 0 1767
      src/views/document/folder/document.vue
  25. 665 0
      src/views/document/folder/document/DocumentList.vue
  26. 628 0
      src/views/document/folder/document/FolderTree.vue
  27. 0 182
      src/views/document/folder/document/components/DocumentAuditLog.vue
  28. 382 0
      src/views/document/folder/document/index.vue
  29. 1 1
      src/views/document/folder/project.vue
  30. 13 22
      src/views/home/taskCenter/audit/index.vue
  31. 34 7
      src/views/home/taskCenter/submission/index.vue
  32. 342 0
      src/views/search/index.vue
  33. 120 3
      src/views/setting/ai/index.vue

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

@@ -1,6 +1,12 @@
 import request from '@/utils/request';
 import { DocumentForm, DocumentQuery, DocumentVO, DocumentMarkForm, DocumentAuditForm, DocumentSubmitForm } from './types';
 import { AxiosPromise } from 'axios';
+import axios from 'axios';
+import FileSaver from 'file-saver';
+import { blobValidate } from '@/utils/ruoyi';
+import { globalHeaders } from '@/utils/request';
+
+const baseURL = import.meta.env.VITE_APP_BASE_API;
 
 /**
  * 查询文档列表
@@ -96,4 +102,49 @@ export const filingDocument = (documentId: number) => {
     method: 'put',
     data: { documentId }
   });
+};
+
+/**
+ * 下载文档(带加载提示和错误处理)
+ * @param ossId OSS文件ID
+ * @param fileName 文件名(可选,作为备用文件名)
+ */
+export const downloadDocumentFile = async (ossId: number, fileName?: string) => {
+  const loadingInstance = ElLoading.service({
+    text: '正在下载文件,请稍候',
+    background: 'rgba(0, 0, 0, 0.7)'
+  });
+
+  try {
+    const res = await axios({
+      method: 'post',
+      url: `${baseURL}/document/document/download/${ossId}`,
+      responseType: 'blob',
+      headers: globalHeaders()
+    });
+
+    const isBlob = blobValidate(res.data);
+
+    if (isBlob) {
+      const blob = new Blob([res.data], { type: 'application/octet-stream' });
+      // 优先使用后端返回的文件名(包含正确的扩展名)
+      const downloadFileName = res.headers['download-filename']
+        ? decodeURIComponent(res.headers['download-filename'])
+        : (fileName || `download_${ossId}`);
+      FileSaver.saveAs(blob, downloadFileName);
+      return true;
+    } else {
+      // 处理错误响应
+      const resText = await res.data.text();
+      const rspObj = JSON.parse(resText);
+      ElMessage.error(rspObj.msg || '下载文件失败');
+      return false;
+    }
+  } catch (error) {
+    console.error('下载文件出错:', error);
+    ElMessage.error('下载文件出现错误,请联系管理员!');
+    return false;
+  } finally {
+    loadingInstance.close();
+  }
 };

+ 12 - 0
src/api/home/taskCenter/submission/index.ts

@@ -27,4 +27,16 @@ export const submitDocument = (data: SubmissionTaskSubmitForm) => {
     method: 'put',
     data: data
   });
+};
+
+/**
+ * 查询文档审核记录
+ * @param query 查询参数
+ */
+export const listSubmissionAuditLog = (query: any): AxiosPromise<any> => {
+  return request({
+    url: '/home/taskCenter/logAudit',
+    method: 'get',
+    params: query
+  });
 };

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

@@ -0,0 +1,12 @@
+import request from '@/utils/request';
+
+/**
+ * 搜索文档列表
+ */
+export function listSearchDocuments(query: any) {
+    return request({
+        url: '/search/list',
+        method: 'get',
+        params: query
+    });
+}

+ 23 - 0
src/api/setting/ai/index.ts

@@ -0,0 +1,23 @@
+import request from '@/utils/request';
+import { AiSettingVO } from './types';
+
+/**
+ * 获取AI设置
+ */
+export function getAiSetting() {
+    return request({
+        url: '/setting/ai',
+        method: 'get'
+    });
+}
+
+/**
+ * 更新AI设置
+ */
+export function updateAiSetting(data: AiSettingVO) {
+    return request({
+        url: '/setting/ai',
+        method: 'put',
+        data: data
+    });
+}

+ 13 - 0
src/api/setting/ai/types.ts

@@ -0,0 +1,13 @@
+/**
+ * AI设置对象
+ */
+export interface AiSettingVO {
+    id?: number;
+    enabledFlag?: number;
+    createDept?: number;
+    createBy?: number;
+    createTime?: string;
+    updateBy?: number;
+    updateTime?: string;
+    params?: Record<string, any>;
+}

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

@@ -0,0 +1,188 @@
+<template>
+  <div>
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="1000px" append-to-body>
+      <!-- 搜索栏 -->
+      <el-form :model="queryParams" :inline="true" class="search-form mb-4">
+        <el-form-item :label="resultLabel">
+          <el-select v-model="queryParams.result" :placeholder="selectResultPlaceholder" clearable>
+            <el-option :label="passLabel" value="3" />
+            <el-option :label="rejectLabel" value="2" />
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="auditTimeLabel">
+          <el-date-picker v-model="auditTimeRange" type="daterange" range-separator="-"
+            :start-placeholder="startTimePlaceholder" :end-placeholder="endTimePlaceholder"
+            value-format="YYYY-MM-DD HH:mm:ss" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="Search" @click="handleQuery">{{ searchButtonText }}</el-button>
+          <el-button icon="Refresh" @click="handleReset">{{ resetButtonText }}</el-button>
+        </el-form-item>
+      </el-form>
+
+      <!-- 审核记录列表 -->
+      <el-table v-loading="loading" :data="auditLogList" border>
+        <el-table-column prop="id" label="ID" width="80" align="center" />
+        <el-table-column prop="documentName" :label="documentNameLabel" min-width="150" show-overflow-tooltip />
+        <el-table-column prop="auditorType" :label="auditorTypeLabel" width="120" align="center">
+          <template #default="scope">
+            <dict-tag v-if="scope.row.auditorType" :options="auditor_type" :value="scope.row.auditorType" />
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="auditorName" :label="auditorNameLabel" width="120" align="center" />
+        <el-table-column prop="result" :label="resultLabel" width="100" align="center">
+          <template #default="scope">
+            <el-tag v-if="scope.row.result === 3" size="small" type="success">
+              {{ passLabel }}
+            </el-tag>
+            <el-tag v-else-if="scope.row.result === 2" size="small" type="danger">
+              {{ rejectLabel }}
+            </el-tag>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="rejectReason" :label="rejectReasonLabel" min-width="180" show-overflow-tooltip />
+        <el-table-column prop="auditTime" :label="auditTimeLabel" width="180" align="center">
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.auditTime) }}</span>
+          </template>
+        </el-table-column>
+        <!-- 操作栏 -->
+        <el-table-column :label="operationLabel" width="120" align="center" fixed="right">
+          <template #default="scope">
+            <el-button v-if="scope.row.ossId" type="primary" link icon="Download"
+              @click="handleDownload(scope.row.ossId, scope.row.documentName)" :title="downloadButtonText" />
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize"
+        :total="total" @pagination="getList" />
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch, getCurrentInstance, toRefs } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { parseTime } from '@/utils/ruoyi';
+import { downloadDocumentFile } from '@/api/document/document';
+import type { ComponentInternalInstance } from 'vue';
+
+interface Props {
+  documentId: number | string;
+  visible: boolean;
+  apiFunction: (params: any) => Promise<any>;
+  i18nPrefix?: string; // 国际化前缀,如 'document.document' 或 'home.taskCenter'
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  i18nPrefix: 'document.document'
+});
+
+const emit = defineEmits(['update:visible']);
+
+const { t } = useI18n();
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+// 获取审核人类型字典
+const { auditor_type } = toRefs<any>(proxy?.useDict('auditor_type'));
+
+const dialogVisible = computed({
+  get: () => props.visible,
+  set: (val) => emit('update:visible', val)
+});
+
+// 国际化文本计算属性
+const dialogTitle = computed(() => t(`${props.i18nPrefix}.dialog.auditLog`));
+const resultLabel = computed(() => t(`${props.i18nPrefix}.auditLog.result`));
+const selectResultPlaceholder = computed(() => t(`${props.i18nPrefix}.auditLog.selectResult`));
+const passLabel = computed(() => t(`${props.i18nPrefix}.auditLog.pass`));
+const rejectLabel = computed(() => t(`${props.i18nPrefix}.auditLog.reject`));
+const auditTimeLabel = computed(() => t(`${props.i18nPrefix}.auditLog.auditTime`));
+const startTimePlaceholder = computed(() => t(`${props.i18nPrefix}.auditLog.startTime`));
+const endTimePlaceholder = computed(() => t(`${props.i18nPrefix}.auditLog.endTime`));
+const searchButtonText = computed(() => t(`${props.i18nPrefix}.button.search`));
+const resetButtonText = computed(() => t(`${props.i18nPrefix}.button.reset`));
+const documentNameLabel = computed(() => t(`${props.i18nPrefix}.auditLog.documentName`));
+const auditorTypeLabel = computed(() => t(`${props.i18nPrefix}.auditLog.auditorType`));
+const auditorNameLabel = computed(() => t(`${props.i18nPrefix}.auditLog.auditorName`));
+const rejectReasonLabel = computed(() => t(`${props.i18nPrefix}.auditLog.rejectReason`));
+const operationLabel = computed(() => t(`${props.i18nPrefix}.auditLog.operation`));
+const downloadButtonText = computed(() => t(`${props.i18nPrefix}.button.download`));
+
+const loading = ref(false);
+const total = ref(0);
+const auditLogList = ref<any[]>([]);
+const auditTimeRange = ref<[string, string]>(['', '']);
+
+const queryParams = ref({
+  pageNum: 1,
+  pageSize: 10,
+  documentId: props.documentId,
+  result: undefined,
+  params: {}
+});
+
+/** 查询审核记录列表 */
+const getList = async () => {
+  loading.value = true;
+  // 更新documentId
+  queryParams.value.documentId = props.documentId;
+  // 处理时间范围
+  queryParams.value.params = {};
+  proxy?.addDateRange(queryParams.value, auditTimeRange.value, 'AuditTime', 'earliestTime', 'latestTime');
+
+  try {
+    const res = await props.apiFunction(queryParams.value);
+    auditLogList.value = res.rows;
+    total.value = res.total;
+  } catch (error) {
+    console.error('获取审核记录失败:', error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+/** 重置按钮操作 */
+const handleReset = () => {
+  queryParams.value.result = undefined;
+  auditTimeRange.value = ['', ''];
+  handleQuery();
+};
+
+/** 监听文档ID变化,重新查询 */
+watch(() => props.documentId, (newVal) => {
+  if (newVal && dialogVisible.value) {
+    getList();
+  }
+});
+
+/** 监听可见性变化,打开时重新查询 */
+watch(() => props.visible, (newVal) => {
+  if (newVal) {
+    getList();
+  }
+});
+
+/** 下载历史文件 */
+const handleDownload = async (ossId: number, fileName: string) => {
+  if (!ossId) return;
+  await downloadDocumentFile(ossId, fileName);
+};
+</script>
+
+<style scoped>
+.search-form {
+  margin-bottom: 16px;
+}
+</style>

+ 21 - 0
src/components/DocumentStatusTag/index.vue

@@ -0,0 +1,21 @@
+<template>
+  <el-tag v-if="config" :type="config.type" :color="config.color" size="small">
+    {{ t(config.label) }}
+  </el-tag>
+  <span v-else>-</span>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { getDocumentStatusConfig } from '@/enums/documentStatus';
+
+interface Props {
+  status: number;
+}
+
+const props = defineProps<Props>();
+const { t } = useI18n();
+
+const config = computed(() => getDocumentStatusConfig(props.status));
+</script>

+ 77 - 0
src/enums/documentStatus.ts

@@ -0,0 +1,77 @@
+/**
+ * 文档状态枚举
+ */
+export enum DocumentStatus {
+    /** 未上传 */
+    UN_UPLOAD = 0,
+    /** 待审核 */
+    UN_AUDIT = 1,
+    /** 审核拒绝 */
+    AUDIT_REJECT = 2,
+    /** 待归档 */
+    UN_FILING = 3,
+    /** 已归档 */
+    FILING = 4,
+    /** 待质控 */
+    UN_QUALITY_CONTROL = 5,
+    /** 质控通过 */
+    QUALITY_CONTROL_PASS = 6,
+    /** 质控拒绝 */
+    QUALITY_CONTROL_REJECT = 7
+}
+
+/**
+ * 文档状态配置
+ */
+export interface DocumentStatusConfig {
+    label: string;
+    type?: 'success' | 'warning' | 'danger' | 'info' | '';
+    color?: string;
+}
+
+/**
+ * 文档状态配置映射
+ */
+export const DOCUMENT_STATUS_CONFIG: Record<DocumentStatus, DocumentStatusConfig> = {
+    [DocumentStatus.UN_UPLOAD]: {
+        label: 'document.document.documentList.statusOptions.unUpload',
+        type: 'info'
+    },
+    [DocumentStatus.UN_AUDIT]: {
+        label: 'document.document.documentList.statusOptions.unAudit',
+        type: 'warning'
+    },
+    [DocumentStatus.AUDIT_REJECT]: {
+        label: 'document.document.documentList.statusOptions.auditReject',
+        type: 'danger'
+    },
+    [DocumentStatus.UN_FILING]: {
+        label: 'document.document.documentList.statusOptions.unFiling',
+        type: ''
+    },
+    [DocumentStatus.FILING]: {
+        label: 'document.document.documentList.statusOptions.filing',
+        type: 'success'
+    },
+    [DocumentStatus.UN_QUALITY_CONTROL]: {
+        label: 'document.document.documentList.statusOptions.unQualityControl',
+        color: '#E6A23C'
+    },
+    [DocumentStatus.QUALITY_CONTROL_PASS]: {
+        label: 'document.document.documentList.statusOptions.qualityControlPass',
+        color: '#67C23A'
+    },
+    [DocumentStatus.QUALITY_CONTROL_REJECT]: {
+        label: 'document.document.documentList.statusOptions.qualityControlReject',
+        color: '#F56C6C'
+    }
+};
+
+/**
+ * 获取文档状态配置
+ * @param status 状态值
+ * @returns 状态配置
+ */
+export const getDocumentStatusConfig = (status: number): DocumentStatusConfig | null => {
+    return DOCUMENT_STATUS_CONFIG[status as DocumentStatus] || null;
+};

+ 7 - 1
src/lang/en_US.ts

@@ -6,6 +6,8 @@ import setting from './modules/setting/index_en';
 import tool from './modules/tool/en_US';
 import project from './modules/project/index_en';
 import document from './modules/document/index_en';
+import home from './modules/home/index_en';
+import search from './modules/search/en_US';
 
 export default {
   // 路由国际化
@@ -108,5 +110,9 @@ export default {
   // 项目管理模块
   project,
   // 文档管理模块
-  document
+  document,
+  // 首页模块
+  home,
+  // 搜索模块
+  search
 };

+ 14 - 5
src/lang/modules/document/document/en_US.ts

@@ -16,7 +16,9 @@ export default {
     mark: 'Mark',
     download: 'Download',
     confirmSubmit: 'Confirm Submit',
-    viewAuditLog: 'View Audit Log'
+    viewAuditLog: 'View Audit Log',
+    archive: 'Archive',
+    confirm: 'Confirm'
   },
   // Menu
   menu: {
@@ -40,7 +42,11 @@ export default {
     markDocument: 'Mark Document',
     auditDocument: 'Audit Document',
     submitDocument: 'Submit Document',
-    auditLog: 'Audit Log'
+    auditLog: 'Audit Log',
+    archiveDialog: {
+      title: 'Archive Confirmation',
+      confirmMessage: 'Confirm that the file has been successfully archived?'
+    }
   },
   // Form
   form: {
@@ -96,7 +102,9 @@ export default {
     auditSuccess: 'Audit successfully',
     auditFailed: 'Audit failed',
     submitSuccess: 'Submit successfully',
-    submitFailed: 'Submit failed'
+    submitFailed: 'Submit failed',
+    archiveSuccess: 'Archive successfully',
+    archiveFailed: 'Archive failed'
   },
   // Validation Rules
   rule: {
@@ -191,7 +199,7 @@ export default {
       qualityControlReject: 'Quality Control Reject'
     }
   },
-  // 审核记录
+  // Audit Log
   auditLog: {
     result: 'Audit Result',
     selectResult: 'Please select audit result',
@@ -203,6 +211,7 @@ export default {
     documentName: 'Document Name',
     auditorType: 'Auditor Type',
     auditorName: 'Auditor',
-    rejectReason: 'Reject Reason'
+    rejectReason: 'Reject Reason',
+    operation: 'Operation'
   }
 };

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

@@ -211,6 +211,7 @@ export default {
     documentName: '文档名称',
     auditorType: '审核人类型',
     auditorName: '审核人',
-    rejectReason: '驳回理由'
+    rejectReason: '驳回理由',
+    operation: '操作'
   }
 };

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

@@ -0,0 +1,6 @@
+// 首页模块 - 统一导出
+import taskCenter from './taskCenter/zh_CN';
+
+export default {
+    taskCenter
+};

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

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

+ 146 - 0
src/lang/modules/home/taskCenter/en_US.ts

@@ -0,0 +1,146 @@
+// Task Center Module - English Translation
+export default {
+    // Audit Task
+    audit: {
+        title: 'File Approval Task',
+        button: {
+            search: 'Search',
+            reset: 'Reset',
+            download: 'Download'
+        },
+        search: {
+            projectCode: 'Project Code',
+            projectCodePlaceholder: 'Please enter project code',
+            projectName: 'Project Name',
+            projectNamePlaceholder: 'Please enter project name',
+            name: 'Name',
+            namePlaceholder: 'Please enter name',
+            search: 'Search',
+            reset: 'Reset'
+        },
+        table: {
+            id: 'No.',
+            name: 'Name',
+            type: 'Type',
+            normalDocument: 'Normal Document',
+            planDocument: 'Plan Document',
+            documentType: 'Plan Document Type',
+            status: 'Status',
+            submitter: 'Submitter',
+            deadline: 'Deadline',
+            submitTime: 'Submit Time',
+            createTime: 'Create Time',
+            action: 'Action',
+            audit: 'Audit',
+            download: 'Download'
+        },
+        dialog: {
+            title: 'Audit Document',
+            result: 'Audit Result',
+            pass: 'Pass',
+            reject: 'Reject',
+            rejectReason: 'Rejection Reason',
+            rejectReasonPlaceholder: 'Please enter rejection reason',
+            confirm: 'Confirm Audit',
+            cancel: 'Cancel'
+        },
+        message: {
+            getListFailed: 'Failed to get task list',
+            auditSuccess: 'Audit successfully',
+            auditFailed: 'Audit failed',
+            noFileToDownload: 'No file available for download'
+        },
+        rule: {
+            resultRequired: 'Please select audit result',
+            rejectReasonRequired: 'Please enter rejection reason'
+        },
+        dialog: {
+            auditLog: 'Audit Log'
+        },
+        auditLog: {
+            result: 'Audit Result',
+            selectResult: 'Please select audit result',
+            pass: 'Pass',
+            reject: 'Reject',
+            auditTime: 'Audit Time',
+            startTime: 'Start Time',
+            endTime: 'End Time',
+            documentName: 'Document Name',
+            auditorType: 'Auditor Type',
+            auditorName: 'Auditor',
+            rejectReason: 'Reject Reason',
+            operation: 'Operation'
+        }
+    },
+    // Submission Task
+    submission: {
+        title: 'File Submission Task',
+        button: {
+            search: 'Search',
+            reset: 'Reset',
+            download: 'Download'
+        },
+        search: {
+            name: 'Name',
+            namePlaceholder: 'Please enter name',
+            projectCode: 'Project Code',
+            projectCodePlaceholder: 'Please enter project code',
+            projectName: 'Project Name',
+            projectNamePlaceholder: 'Please enter project name',
+            status: 'Status',
+            statusPlaceholder: 'Please select status',
+            unSubmit: 'Un Submit',
+            unAudit: 'Un Audit',
+            auditReject: 'Audit Reject',
+            search: 'Search',
+            reset: 'Reset'
+        },
+        table: {
+            id: 'ID',
+            name: 'Name',
+            type: 'Type',
+            normalDocument: 'Normal Document',
+            planDocument: 'Plan Document',
+            documentType: 'Plan Document Type',
+            status: 'Status',
+            submitter: 'Submitter',
+            deadline: 'Deadline',
+            overdueDays: 'Overdue Days',
+            submitTime: 'Submit Time',
+            createTime: 'Create Time',
+            action: 'Action',
+            submit: 'Submit'
+        },
+        dialog: {
+            title: 'Submit Document',
+            file: 'File',
+            confirm: 'Confirm Submit',
+            cancel: 'Cancel'
+        },
+        message: {
+            getListFailed: 'Failed to get task list',
+            submitSuccess: 'Submit successfully',
+            submitFailed: 'Submit failed'
+        },
+        rule: {
+            fileRequired: 'Please upload file'
+        },
+        dialog: {
+            auditLog: 'Audit Log'
+        },
+        auditLog: {
+            result: 'Audit Result',
+            selectResult: 'Please select audit result',
+            pass: 'Pass',
+            reject: 'Reject',
+            auditTime: 'Audit Time',
+            startTime: 'Start Time',
+            endTime: 'End Time',
+            documentName: 'Document Name',
+            auditorType: 'Auditor Type',
+            auditorName: 'Auditor',
+            rejectReason: 'Reject Reason',
+            operation: 'Operation'
+        }
+    }
+};

+ 143 - 0
src/lang/modules/home/taskCenter/zh_CN.ts

@@ -0,0 +1,143 @@
+// 任务中心模块 - 中文翻译
+export default {
+    // 审核任务
+    audit: {
+        title: '文件审批任务',
+        button: {
+            search: '搜索',
+            reset: '重置',
+            download: '下载'
+        },
+        search: {
+            projectCode: '项目编号',
+            projectCodePlaceholder: '请输入项目编号',
+            projectName: '项目名称',
+            projectNamePlaceholder: '请输入项目名称',
+            name: '名称',
+            namePlaceholder: '请输入名称',
+            search: '搜索',
+            reset: '重置'
+        },
+        table: {
+            id: '序号',
+            name: '名称',
+            type: '类型',
+            normalDocument: '非计划文档',
+            planDocument: '计划文档',
+            documentType: '计划文档类型',
+            status: '状态',
+            submitter: '递交人',
+            deadline: '截止时间',
+            submitTime: '递交时间',
+            createTime: '创建时间',
+            action: '操作',
+            audit: '审核',
+            download: '下载'
+        },
+        dialog: {
+            title: '审核文档',
+            result: '审批结果',
+            pass: '通过',
+            reject: '拒绝',
+            rejectReason: '拒绝原因',
+            rejectReasonPlaceholder: '请输入拒绝原因',
+            confirm: '确认审批',
+            cancel: '取消'
+        },
+        message: {
+            getListFailed: '获取任务列表失败',
+            auditSuccess: '审批成功',
+            auditFailed: '审批失败',
+            noFileToDownload: '暂无文件可下载'
+        },
+        rule: {
+            resultRequired: '请选择审批结果',
+            rejectReasonRequired: '请输入拒绝原因'
+        },
+        auditLog: {
+            result: '审核结果',
+            selectResult: '请选择审核结果',
+            pass: '通过',
+            reject: '驳回',
+            auditTime: '审核时间',
+            startTime: '开始时间',
+            endTime: '结束时间',
+            documentName: '文档名称',
+            auditorType: '审核人类型',
+            auditorName: '审核人',
+            rejectReason: '驳回理由',
+            operation: '操作'
+        }
+    },
+    // 递交任务
+    submission: {
+        title: '文件递交任务',
+        button: {
+            search: '搜索',
+            reset: '重置',
+            download: '下载'
+        },
+        search: {
+            name: '名称',
+            namePlaceholder: '请输入名称',
+            projectCode: '项目编号',
+            projectCodePlaceholder: '请输入项目编号',
+            projectName: '项目名称',
+            projectNamePlaceholder: '请输入项目名称',
+            status: '状态',
+            statusPlaceholder: '请选择状态',
+            unSubmit: '未递交',
+            unAudit: '待审核',
+            auditReject: '审核拒绝',
+            search: '搜索',
+            reset: '重置'
+        },
+        table: {
+            id: 'ID',
+            name: '名称',
+            type: '类型',
+            normalDocument: '非计划文档',
+            planDocument: '计划文档',
+            documentType: '计划文档类型',
+            status: '状态',
+            submitter: '递交人',
+            deadline: '截止时间',
+            overdueDays: '递交逾期天数',
+            submitTime: '递交时间',
+            createTime: '创建时间',
+            action: '操作',
+            submit: '递交'
+        },
+        dialog: {
+            title: '递交文档',
+            file: '文件',
+            confirm: '确认递交',
+            cancel: '取消'
+        },
+        message: {
+            getListFailed: '获取任务列表失败',
+            submitSuccess: '递交成功',
+            submitFailed: '递交失败'
+        },
+        rule: {
+            fileRequired: '请上传文件'
+        },
+        dialog: {
+            auditLog: '审核记录'
+        },
+        auditLog: {
+            result: '审核结果',
+            selectResult: '请选择审核结果',
+            pass: '通过',
+            reject: '驳回',
+            auditTime: '审核时间',
+            startTime: '开始时间',
+            endTime: '结束时间',
+            documentName: '文档名称',
+            auditorType: '审核人类型',
+            auditorName: '审核人',
+            rejectReason: '驳回理由',
+            operation: '操作'
+        }
+    }
+};

+ 59 - 0
src/lang/modules/search/en_US.ts

@@ -0,0 +1,59 @@
+// Search Module - English Translation
+export default {
+    title: 'Document Search',
+    search: {
+        name: 'Document Name',
+        namePlaceholder: 'Please enter document name',
+        projectCode: 'Project Code',
+        projectCodePlaceholder: 'Please enter project code',
+        projectName: 'Project Name',
+        projectNamePlaceholder: 'Please enter project name',
+        type: 'Type',
+        typePlaceholder: 'Please select type',
+        status: 'Status',
+        statusPlaceholder: 'Please select status',
+        createTime: 'Create Time',
+        updateTime: 'Update Time',
+        startTime: 'Start Time',
+        endTime: 'End Time'
+    },
+    button: {
+        search: 'Search',
+        reset: 'Reset',
+        download: 'Download'
+    },
+    table: {
+        id: 'ID',
+        folderName: 'Folder',
+        name: 'Document Name',
+        type: 'Type',
+        specification: 'Specification',
+        planDocumentType: 'Plan Document Type',
+        status: 'Status',
+        submitter: 'Submitter',
+        submitDeadline: 'Submit Deadline',
+        submitTime: 'Submit Time',
+        fileName: 'File Name',
+        note: 'Note',
+        createTime: 'Create Time',
+        updateTime: 'Update Time',
+        action: 'Action'
+    },
+    type: {
+        normalDocument: 'Normal Document',
+        planDocument: 'Plan Document'
+    },
+    status: {
+        unUpload: 'Un Upload',
+        unAudit: 'Un Audit',
+        auditReject: 'Audit Reject',
+        unFiling: 'Un Filing',
+        filing: 'Filing',
+        unQualityControl: 'Un Quality Control',
+        qualityControlPass: 'Quality Control Pass',
+        qualityControlReject: 'Quality Control Reject'
+    },
+    message: {
+        fetchFailed: 'Failed to get document list'
+    }
+};

+ 59 - 0
src/lang/modules/search/zh_CN.ts

@@ -0,0 +1,59 @@
+// 搜索模块 - 中文翻译
+export default {
+    title: '文档搜索',
+    search: {
+        name: '文档名称',
+        namePlaceholder: '请输入文档名称',
+        projectCode: '项目编号',
+        projectCodePlaceholder: '请输入项目编号',
+        projectName: '项目名称',
+        projectNamePlaceholder: '请输入项目名称',
+        type: '类型',
+        typePlaceholder: '请选择类型',
+        status: '状态',
+        statusPlaceholder: '请选择状态',
+        createTime: '创建时间',
+        updateTime: '更新时间',
+        startTime: '开始时间',
+        endTime: '结束时间'
+    },
+    button: {
+        search: '搜索',
+        reset: '重置',
+        download: '下载'
+    },
+    table: {
+        id: 'ID',
+        folderName: '文件夹',
+        name: '文档名称',
+        type: '类型',
+        specification: '文档标识',
+        planDocumentType: '计划文档类型',
+        status: '状态',
+        submitter: '递交人',
+        submitDeadline: '递交截止日期',
+        submitTime: '递交时间',
+        fileName: '文件名',
+        note: '备注',
+        createTime: '创建时间',
+        updateTime: '更新时间',
+        action: '操作'
+    },
+    type: {
+        normalDocument: '非计划文档',
+        planDocument: '计划文档'
+    },
+    status: {
+        unUpload: '未上传',
+        unAudit: '待审核',
+        auditReject: '审核拒绝',
+        unFiling: '待归档',
+        filing: '已归档',
+        unQualityControl: '待质控',
+        qualityControlPass: '质控通过',
+        qualityControlReject: '质控拒绝'
+    },
+    message: {
+        fetchFailed: '获取文档列表失败'
+    }
+};

+ 22 - 0
src/lang/modules/setting/ai/en_US.ts

@@ -0,0 +1,22 @@
+// AI Settings Module - English Translation
+export default {
+    title: 'AI Settings',
+    form: {
+        enabledFlag: 'Enabled Status',
+        enabled: 'Enabled',
+        disabled: 'Disabled'
+    },
+    button: {
+        save: 'Save',
+        reset: 'Reset'
+    },
+    message: {
+        noData: 'No Data',
+        fetchFailed: 'Failed to get AI settings',
+        saveSuccess: 'Save successfully',
+        saveFailed: 'Save failed'
+    },
+    rule: {
+        enabledFlagRequired: 'Please select enabled status'
+    }
+};

+ 22 - 0
src/lang/modules/setting/ai/zh_CN.ts

@@ -0,0 +1,22 @@
+// AI设置模块 - 中文翻译
+export default {
+    title: 'AI设置',
+    form: {
+        enabledFlag: '启用状态',
+        enabled: '启用',
+        disabled: '禁用'
+    },
+    button: {
+        save: '保存',
+        reset: '重置'
+    },
+    message: {
+        noData: '暂无数据',
+        fetchFailed: '获取AI设置失败',
+        saveSuccess: '保存成功',
+        saveFailed: '保存失败'
+    },
+    rule: {
+        enabledFlagRequired: '请选择启用状态'
+    }
+};

+ 3 - 1
src/lang/modules/setting/index.ts

@@ -1,6 +1,8 @@
 // 设置管理模块 - 统一导出
 import applet from './applet/zh_CN';
+import ai from './ai/zh_CN';
 
 export default {
-  applet
+  applet,
+  ai
 };

+ 3 - 1
src/lang/modules/setting/index_en.ts

@@ -1,6 +1,8 @@
 // Settings Module - Unified Export (English)
 import applet from './applet/en_US';
+import ai from './ai/en_US';
 
 export default {
-  applet
+  applet,
+  ai
 };

+ 7 - 1
src/lang/zh_CN.ts

@@ -6,6 +6,8 @@ import setting from './modules/setting/index';
 import tool from './modules/tool/zh_CN';
 import project from './modules/project/index';
 import document from './modules/document/index';
+import home from './modules/home/index';
+import search from './modules/search/zh_CN';
 
 export default {
   // 路由国际化
@@ -108,5 +110,9 @@ export default {
   // 项目管理模块
   project,
   // 文档管理模块
-  document
+  document,
+  // 首页模块
+  home,
+  // 搜索模块
+  search
 };

+ 24 - 24
src/utils/request.ts

@@ -178,31 +178,31 @@ export function download(url: string, params: any, fileName: string) {
   downloadLoadingInstance = ElLoading.service({ text: '正在下载数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' });
   // prettier-ignore
   return service.post(url, params, {
-      transformRequest: [
-        (params: any) => {
-          return tansParams(params);
-        }
-      ],
-      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
-      responseType: 'blob'
-    }).then(async (resp: any) => {
-      const isLogin = blobValidate(resp);
-      if (isLogin) {
-        const blob = new Blob([resp]);
-        FileSaver.saveAs(blob, fileName);
-      } else {
-        const blob = new Blob([resp]);
-        const resText = await blob.text();
-        const rspObj = JSON.parse(resText);
-        const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'];
-        ElMessage.error(errMsg);
+    transformRequest: [
+      (params: any) => {
+        return tansParams(params);
       }
-      downloadLoadingInstance.close();
-    }).catch((r: any) => {
-      console.error(r);
-      ElMessage.error('下载文件出现错误,请联系管理员!');
-      downloadLoadingInstance.close();
-    });
+    ],
+    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+    responseType: 'blob'
+  }).then(async (resp: any) => {
+    const isLogin = blobValidate(resp);
+    if (isLogin) {
+      const blob = new Blob([resp]);
+      FileSaver.saveAs(blob, fileName);
+    } else {
+      const blob = new Blob([resp]);
+      const resText = await blob.text();
+      const rspObj = JSON.parse(resText);
+      const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'];
+      ElMessage.error(errMsg);
+    }
+    downloadLoadingInstance.close();
+  }).catch((r: any) => {
+    console.error(r);
+    ElMessage.error('下载文件出现错误,请联系管理员!');
+    downloadLoadingInstance.close();
+  });
 }
 // 导出 axios 实例
 export default service;

+ 0 - 1767
src/views/document/folder/document.vue

@@ -1,1767 +0,0 @@
-<template>
-  <div>
-    <el-card shadow="never">
-      <template #header>
-        <div class="flex justify-between items-center">
-          <span class="text-lg font-bold">{{ t('document.document.header.title') }}</span>
-          <el-button type="primary" @click="handleBack">{{ t('document.document.header.backToList') }}</el-button>
-        </div>
-      </template>
-
-      <div class="content-wrapper">
-        <div class="tree-container">
-          <div class="tree-header">
-            <el-button v-hasPermi="['document:folder:add']" type="primary" icon="Plus" size="small" @click="handleAddFolder">{{
-              t('document.document.button.newFolder')
-            }}</el-button>
-          </div>
-          <el-scrollbar class="tree-scrollbar">
-            <el-tree v-loading="loading" :data="treeData" :props="treeProps" node-key="id" default-expand-all :expand-on-click-node="false">
-              <template #default="{ node, data }">
-                <span class="custom-tree-node">
-                  <el-icon>
-                    <Folder v-if="data.type === 0" />
-                    <Location v-else-if="data.type === 1" />
-                    <OfficeBuilding v-else-if="data.type === 2" />
-                    <Document v-else />
-                  </el-icon>
-                  <span class="node-label" @click="handleFolderClick(data)">{{ node.label }}</span>
-                  <span class="node-actions">
-                    <span class="menu-trigger" @click="toggleMenu($event, data)">
-                      <el-icon>
-                        <MoreFilled />
-                      </el-icon>
-                    </span>
-                  </span>
-                </span>
-              </template>
-            </el-tree>
-          </el-scrollbar>
-        </div>
-
-        <!-- 一级菜单 -->
-        <ul class="primary-menu" v-if="activeMenu !== null" :style="primaryMenuStyle">
-          <li class="menu-item has-submenu" v-hasPermi="['document:folder:add']" @click.stop="toggleSubmenu($event)">
-            <span>{{ t('document.document.menu.add') }}</span>
-            <el-icon class="arrow-icon">
-              <ArrowRight />
-            </el-icon>
-          </li>
-          <li class="menu-item" v-hasPermi="['document:folder:edit']" @click="handleMenuItemClick('edit', currentMenuData)">
-            <span>{{ t('document.document.menu.edit') }}</span>
-          </li>
-          <li class="menu-item" v-hasPermi="['document:folder:remove']" @click="handleMenuItemClick('delete', currentMenuData)">
-            <span>{{ t('document.document.menu.delete') }}</span>
-          </li>
-        </ul>
-
-        <!-- 二级菜单 -->
-        <ul class="secondary-menu" v-if="showSecondaryMenu" :style="secondaryMenuStyle">
-          <!-- 国家或中心:显示中心和文件夹 -->
-          <template v-if="currentMenuData && (currentMenuData.type === 1 || currentMenuData.type === 2)">
-            <li class="menu-item" @click="handleMenuItemClick('add:2', currentMenuData)">
-              <span>{{ t('document.document.menu.center') }}</span>
-            </li>
-            <li class="menu-item" @click="handleMenuItemClick('add:0', currentMenuData)">
-              <span>{{ t('document.document.menu.folder') }}</span>
-            </li>
-          </template>
-          <!-- 文件夹:只显示文件夹和文档 -->
-          <template v-else-if="currentMenuData && currentMenuData.type === 0">
-            <li class="menu-item" @click="handleMenuItemClick('add:0', currentMenuData)">
-              <span>{{ t('document.document.menu.folder') }}</span>
-            </li>
-            <li class="menu-item" v-hasPermi="['document:document:add']" @click="handleMenuItemClick('add:document', currentMenuData)">
-              <span>{{ t('document.document.menu.document') }}</span>
-            </li>
-          </template>
-        </ul>
-
-        <div class="content-container">
-          <!-- 文档列表展示区域 -->
-          <div v-if="selectedFolder" class="document-list-container">
-            <!-- 搜索栏 -->
-            <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-form-item>
-              <el-form-item>
-                <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>
-
-            <!-- 文档列表 -->
-            <el-table v-loading="documentLoading" :data="documentList" border style="margin-top: 10px">
-              <el-table-column prop="id" width="55" align="center" :label="t('document.document.documentList.index')">
-                <template #default="scope">
-                  {{ 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
-              >
-                <template #default="scope">
-                  <dict-tag v-if="scope.row.specification" 
-                            :options="specificationDict" 
-                            :value="scope.row.specification" />
-                  <span v-else>-</span>
-                </template>
-              </el-table-column>
-              <el-table-column prop="planType" :label="t('document.document.documentList.planDocumentType')" width="120" align="center">
-                <template #default="scope">
-                  <dict-tag v-if="scope.row.planType" :options="plan_document_type" :value="scope.row.planType" />
-                  <span v-else>-</span>
-                </template>
-              </el-table-column>
-              <el-table-column prop="status" :label="t('document.document.documentList.status')" width="120" align="center">
-                <template #default="scope">
-                  <el-tag v-if="scope.row.status === 0" size="small" type="info">{{
-                    t('document.document.documentList.statusOptions.unUpload')
-                  }}</el-tag>
-                  <el-tag v-else-if="scope.row.status === 1" size="small" type="warning">{{
-                    t('document.document.documentList.statusOptions.unAudit')
-                  }}</el-tag>
-                  <el-tag v-else-if="scope.row.status === 2" size="small" type="danger">{{
-                    t('document.document.documentList.statusOptions.auditReject')
-                  }}</el-tag>
-                  <el-tag v-else-if="scope.row.status === 3" size="small" type="warning">{{
-                    t('document.document.documentList.statusOptions.unFiling')
-                  }}</el-tag>
-                  <el-tag v-else-if="scope.row.status === 4" size="small" type="success">{{
-                    t('document.document.documentList.statusOptions.filing')
-                  }}</el-tag>
-                  <el-tag v-else-if="scope.row.status === 5" size="small" type="warning">{{
-                    t('document.document.documentList.statusOptions.unQualityControl')
-                  }}</el-tag>
-                  <el-tag v-else-if="scope.row.status === 6" size="small" type="success">{{
-                    t('document.document.documentList.statusOptions.qualityControlPass')
-                  }}</el-tag>
-                  <el-tag v-else-if="scope.row.status === 7" size="small" type="danger">{{
-                    t('document.document.documentList.statusOptions.qualityControlReject')
-                  }}</el-tag>
-                  <el-tag v-else size="small" type="default">-</el-tag>
-                </template>
-              </el-table-column>
-              <el-table-column prop="submitter" :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">
-                <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">
-                <template #default="scope">
-                  <span v-if="scope.row.submitTime">{{ parseTime(scope.row.submitTime) }}</span>
-                  <span v-else>-</span>
-                </template>
-              </el-table-column>
-              <el-table-column prop="url" :label="t('document.document.documentList.url')" min-width="200">
-                <template #default="scope">
-                  <div v-if="scope.row.fileName" class="file-name-cell">
-                    <svg-icon :icon-class="getFileIconClass(scope.row.fileName)" class="file-icon" :size="18" />
-                    <span class="file-name-text show-overflow-tooltip">{{ scope.row.fileName }}</span>
-                  </div>
-                  <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">
-                <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">
-                <template #default="scope">
-                  <span v-if="scope.row.updateTime">{{ parseTime(scope.row.updateTime) }}</span>
-                  <span v-else>-</span>
-                </template>
-              </el-table-column>
-              <!-- 操作列 -->
-              <el-table-column :label="t('document.document.documentList.action')" width="200" align="center" fixed="right">
-                <template #default="scope">
-                  <!-- 审核按钮 -->
-                  <el-button
-                    v-if="scope.row.url && scope.row.status === 1"
-                    v-hasPermi="['document:document:audit']"
-                    type="primary"
-                    link
-                    :icon="Select"
-                    @click="handleAuditClick(scope.row)"
-                    :title="t('document.document.button.audit')"
-                  />
-
-                  <!-- 递交按钮 -->
-                  <el-button
-                    v-if="(scope.row.status === 0 || scope.row.status === 2) && scope.row.submitterId === userStore.userId"
-                    v-hasPermi="['document:document:submit']"
-                    type="primary"
-                    link
-                    :icon="Upload"
-                    @click="handleSubmit(scope.row)"
-                    :title="t('document.document.button.submit')"
-                  />
-
-                  <!-- 确认递交按钮 -->
-                  <el-button
-                    v-if="(scope.row.status === 0 || scope.row.status === 2) && scope.row.submitterId === userStore.userId"
-                    v-hasPermi="['document:document:confirmSubmit']"
-                    type="primary"
-                    link
-                    icon="Delete"
-                    @click="handleConfirmSubmit(scope.row)"
-                    :title="t('document.document.button.confirmSubmit')"
-                  />
-
-                  <!-- 原有按钮 -->
-                  <el-button
-                    v-hasPermi="['document:document:mark']"
-                    type="primary"
-                    link
-                    icon="Flag"
-                    @click="handleMark(scope.row)"
-                    :title="t('document.document.button.mark')"
-                  />
-                  <el-button
-                    v-if="scope.row.url"
-                    type="primary"
-                    link
-                    icon="Download"
-                    @click="handleDownload(scope.row)"
-                    :title="t('document.document.button.download')"
-                  />
-
-                  <!-- 查看审核记录按钮 -->
-                  <el-button
-                    v-hasPermi="['document:document:logAudit']"
-                    type="primary"
-                    link
-                    icon="DocumentCopy"
-                    @click="handleViewAuditLog(scope.row)"
-                    :title="t('document.document.button.viewAuditLog')"
-                  />
-
-                  <!-- 归档按钮 -->
-                  <el-button
-                    v-if="scope.row.status === 3"
-                    v-hasPermi="['document:document:filing']"
-                    type="primary"
-                    link
-                    icon="UploadFilled"
-                    @click="handleArchive(scope.row)"
-                    :title="t('document.document.button.archive')"
-                  />
-                </template>
-              </el-table-column>
-            </el-table>
-
-            <!-- 分页 -->
-            <pagination
-              v-show="documentTotal > 0"
-              v-model:page="documentQueryParams.pageNum"
-              v-model:limit="documentQueryParams.pageSize"
-              :total="documentTotal"
-              @pagination="getDocumentList"
-            />
-          </div>
-
-          <!-- 空状态 -->
-          <el-empty v-else :description="t('document.document.empty.description')"> </el-empty>
-        </div>
-      </div>
-    </el-card>
-
-    <!-- 添加文件夹对话框 -->
-    <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 :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>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button :loading="buttonLoading" type="primary" @click="submitFolderForm">{{ t('document.document.button.submit') }}</el-button>
-          <el-button @click="cancel">{{ t('document.document.button.cancel') }}</el-button>
-        </div>
-      </template>
-    </el-dialog>
-
-    <!-- 添加文档对话框 -->
-    <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"
-        >
-          <el-input v-model="documentForm.name" :placeholder="t('document.document.documentForm.namePlaceholder')" clearable />
-        </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.submitter')" prop="submitterId">
-          <el-select
-            v-model="documentForm.submitterId"
-            filterable
-            remote
-            reserve-keyword
-            :placeholder="t('document.document.documentForm.submitterPlaceholder')"
-            :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="dict.label" :value="dict.value" />
-          </el-select>
-        </el-form-item>
-
-        <el-form-item :label="t('document.document.documentForm.file')" :prop="documentForm.type === 0 ? 'ossId' : ''">
-          <fileUpload v-model="uploadedFileId" :limit="1" />
-        </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.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.submit')
-          }}</el-button>
-          <el-button @click="cancelDocument">{{ t('document.document.button.cancel') }}</el-button>
-        </div>
-      </template>
-    </el-dialog>
-
-    <!-- 标识文档对话框 -->
-    <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 specificationDict" :key="dict.value" :label="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>
-
-    <!-- 递交文档对话框 -->
-    <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="ossId">
-          <fileUpload v-model="submitForm.ossId" :limit="1" :action="'/common/resource/oss/upload'" />
-        </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>
-
-    <!-- 文档审核记录对话框 -->
-    <DocumentAuditLog v-model:visible="auditLogDialog.visible" :document-id="auditLogDialog.documentId" />
-
-    <!-- 归档确认对话框 -->
-    <ArchiveConfirmDialog v-model="archiveDialog.visible" :document="archiveDialog.currentDocument" @confirm="handleArchiveConfirm" />
-
-    <!-- 审核文档对话框 -->
-    <DocumentAuditDialog
-      v-model="auditDialog.visible"
-      :document="auditDialog.document"
-      :title="t('document.document.dialog.auditDocument')"
-      @success="getDocumentList"
-    />
-  </div>
-</template>
-
-<script setup lang="ts">
-import { ref, reactive, onMounted, onUnmounted, nextTick, getCurrentInstance, watch, computed } from 'vue';
-import { useI18n } from 'vue-i18n';
-import { listFolder, addFolder, delFolder, getFolder, updateFolder } from '@/api/document/folder';
-import { FolderListVO, FolderForm } from '@/api/document/folder/types';
-import {
-  addDocument,
-  listDocument,
-  markDocument,
-  auditDocument,
-  submitDocument,
-  confirmSubmit,
-  listDocumentAuditLog,
-  filingDocument
-} from '@/api/document/document';
-import {
-  DocumentForm,
-  DocumentQuery,
-  DocumentVO,
-  DocumentMarkForm,
-  DocumentAuditForm,
-  DocumentSubmitForm,
-  DocumentAuditLogVO,
-  DocumentAuditLogQuery
-} from '@/api/document/document/types';
-import { queryMemberNotInCenter } from '@/api/project/management';
-import { MemberNotInCenterVO, MemberNotInCenterQuery } from '@/api/project/management/types';
-import {
-  Folder,
-  Document,
-  Edit,
-  Delete,
-  Plus,
-  MoreFilled,
-  Location,
-  OfficeBuilding,
-  ArrowRight,
-  Download,
-  Select,
-  Grid,
-  Monitor,
-  Reading,
-  Flag,
-  Upload,
-  UploadFilled
-} 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 { checkPermi } from '@/utils/permission';
-import fileUpload from '@/components/FileUpload/index.vue';
-import DocumentAuditLog from './document/components/DocumentAuditLog.vue';
-import ArchiveConfirmDialog from './document/components/ArchiveConfirmDialog.vue';
-import DocumentAuditDialog from './document/components/DocumentAuditDialog.vue';
-
-interface Props {
-  projectId?: number | string;
-}
-
-const props = defineProps<Props>();
-
-const emit = defineEmits<{
-  back: [];
-}>();
-
-const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const { t } = useI18n();
-const userStore = useUserStore();
-const { plan_document_type, center_file_specification, project_file_specification } = toRefs<any>(
-  proxy?.useDict('plan_document_type', 'center_file_specification', 'project_file_specification')
-);
-
-// 数据定义
-const loading = ref(false);
-const buttonLoading = ref(false);
-const treeData = ref<FolderListVO[]>([]);
-const folderFormRef = ref<FormInstance>();
-const documentFormRef = ref<FormInstance>();
-const documentButtonLoading = ref(false);
-
-// 检查是否有计划文档添加权限
-const hasAddPlanPermission = computed(() => checkPermi(['document:document:addPlan']));
-
-// 当前用户信息
-const currentUserName = ref(userStore.nickname || '');
-
-// 审核记录对话框
-const auditLogDialog = reactive({
-  visible: false,
-  documentId: ''
-});
-
-// 归档对话框
-const archiveDialog = reactive({
-  visible: false,
-  currentDocument: null as DocumentVO | null
-});
-
-// 对话框
-const dialog = reactive({
-  visible: false,
-  title: '',
-  isEdit: false
-});
-
-// 文档对话框
-const documentDialog = reactive({
-  visible: false,
-  title: ''
-});
-
-// 标识文档对话框
-const markDialog = reactive({
-  visible: false,
-  title: ''
-});
-
-// 标识表单ref
-const markFormRef = ref<FormInstance>();
-const markButtonLoading = ref(false);
-
-// 当前操作的节点
-const currentNode = ref<FolderListVO | null>(null);
-
-// 限制层级相关状态
-const isRestricted = ref(false); // 是否限制
-const restrictionLevelValue = ref(0); // 限制层级值
-
-// 表单初始数据
-const initFormData: FolderForm = {
-  id: undefined,
-  projectId: undefined,
-  parentId: undefined,
-  type: 0,
-  name: '',
-  status: 0,
-  note: '',
-  restrictionLevel: -1
-};
-
-// 表单数据
-const form = ref<FolderForm>({ ...initFormData });
-
-// 文档表单初始数据
-const initDocumentFormData: DocumentForm = {
-  id: undefined,
-  name: '',
-  type: 0,
-  submitterId: undefined,
-  folderId: undefined,
-  submitDeadline: undefined,
-  planType: undefined,
-  ossId: undefined,
-  submitTime: undefined,
-  projectId: props.projectId,
-  note: ''
-};
-
-// 文件上传的ossId(字符串格式)
-const uploadedFileId = ref<string>('');
-
-// 文档表单数据
-const documentForm = ref<DocumentForm>({ ...initDocumentFormData });
-
-// 标识表单数据
-const markForm = ref<DocumentMarkForm>({
-  id: 0,
-  type: ''
-});
-
-// 递交文档对话框
-const submitDialog = reactive({
-  visible: false,
-  title: ''
-});
-
-// 递交表单ref
-const submitFormRef = ref<FormInstance>();
-const submitButtonLoading = ref(false);
-
-// 递交表单数据
-interface SubmitForm {
-  ossId: string; // 文件OSS ID
-}
-
-const submitForm = ref<SubmitForm>({
-  ossId: ''
-});
-
-// 递交表单验证规则
-const submitRules = reactive({
-  ossId: [
-    {
-      required: true,
-      message: t('document.document.submitRule.fileRequired'),
-      trigger: 'blur'
-    }
-  ]
-});
-
-// 当前操作的文档
-const currentDocument = ref<DocumentVO | null>(null);
-
-// 审核对话框状态
-const auditDialog = reactive({
-  visible: false,
-  document: null as DocumentVO | null
-});
-
-// 递交人搜索相关
-const submitterSearchLoading = ref(false);
-const submitterOptions = ref<MemberNotInCenterVO[]>([]);
-let submitterSearchTimer: NodeJS.Timeout | null = null;
-
-// ========== 文档列表相关 ==========
-// 文档列表数据
-const documentList = ref<DocumentVO[]>([]);
-const documentLoading = ref(false);
-const documentTotal = ref(0);
-
-// 当前选中的文件夹
-const selectedFolder = ref<FolderListVO | null>(null);
-
-// 文档查询参数
-const documentQueryParams = reactive<DocumentQuery>({
-  pageNum: 1,
-  pageSize: 10,
-  name: '',
-  folderId: undefined
-});
-
-// 表单验证规则
-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 documentRules = {
-  name: [{ required: true, message: t('document.document.documentRule.nameRequired'), trigger: 'blur' }],
-  submitterId: [{ required: true, message: t('document.document.documentRule.submitterRequired'), trigger: 'change' }],
-  ossId: [{ required: true, message: t('document.document.documentRule.fileRequired'), trigger: 'change' }]
-};
-
-// 标识表单验证规则
-const markRules = {
-  type: [{ required: true, message: t('document.document.markRule.typeRequired'), trigger: 'change' }]
-};
-
-// 树形组件配置
-const treeProps = {
-  children: 'children',
-  label: 'name'
-};
-
-// 根据当前文件夹获取对应的字典
-const specificationDict = computed(() => {
-  if (!selectedFolder.value) return [];
-
-  // 判断是否在中心底下
-  const isUnderCenter = checkIfUnderCenter(selectedFolder.value.id);
-
-  if (isUnderCenter) {
-    return center_file_specification?.value || [];
-  } else {
-    return project_file_specification?.value || [];
-  }
-});
-
-// 检查文件夹是否在中心底下
-const checkIfUnderCenter = (folderId: string | number): boolean => {
-  // 从当前文件夹往上遍历,如果中间存在着中心类型的文件夹,那么他就是中心层级文件
-  // 递归查找目标节点并收集从根到目标的路径
-  const findPathToNode = (tree: FolderListVO[], targetId: string | number, path: FolderListVO[] = []): FolderListVO[] | null => {
-    for (const node of tree) {
-      const currentPath = [...path, node];
-
-      if (node.id === targetId) {
-        // 找到目标节点,返回路径
-        return currentPath;
-      }
-
-      // 在子节点中递归查找
-      if (node.children && node.children.length > 0) {
-        const result = findPathToNode(node.children, targetId, currentPath);
-        if (result) {
-          return result;
-        }
-      }
-    }
-    return null;
-  };
-
-  // 获取从根到目标文件夹的路径
-  const path = findPathToNode(treeData.value, folderId);
-
-  if (!path) {
-    return false;
-  }
-
-  // 检查路径中是否存在type为1或2的节点(国家或中心)
-  return path.some((node) => node.type === 1 || node.type === 2);
-};
-
-// 获取文件夹列表
-const getList = async () => {
-  if (!props.projectId) {
-    ElMessage.warning(t('document.document.message.projectIdNotExist'));
-    return;
-  }
-
-  loading.value = true;
-  try {
-    const res = await listFolder({ projectId: props.projectId } as any);
-    treeData.value = res.data || [];
-  } catch (error) {
-    ElMessage.error(t('document.document.message.getFolderListFailed'));
-    console.error(error);
-  } finally {
-    loading.value = false;
-  }
-};
-
-// 返回项目列表
-const handleBack = () => {
-  emit('back');
-};
-
-// 表单重置
-const reset = () => {
-  form.value = { ...initFormData };
-  isRestricted.value = false;
-  restrictionLevelValue.value = 0;
-  folderFormRef.value?.resetFields();
-};
-
-// 处理限制状态变化
-const handleRestrictionChange = (value: boolean) => {
-  if (value) {
-    // 选择限制,使用restrictionLevelValue的值
-    form.value.restrictionLevel = restrictionLevelValue.value;
-  } else {
-    // 选择不限制,设置为-1
-    form.value.restrictionLevel = -1;
-  }
-};
-
-// 监听restrictionLevelValue变化,同步更新form.restrictionLevel
-watch(restrictionLevelValue, (newValue) => {
-  if (isRestricted.value) {
-    form.value.restrictionLevel = newValue;
-  }
-});
-
-// 取消按钮
-const cancel = () => {
-  reset();
-  dialog.visible = false;
-};
-
-// 新建文件夹(顶级)
-const handleAddFolder = () => {
-  reset();
-  currentNode.value = null;
-  form.value.projectId = props.projectId;
-  form.value.parentId = undefined;
-  dialog.visible = true;
-  dialog.title = t('document.document.dialog.addFolder');
-  dialog.isEdit = false;
-};
-
-// 新增子节点
-const handleAddChild = (data: FolderListVO) => {
-  reset();
-  currentNode.value = data;
-  form.value.projectId = props.projectId;
-  form.value.parentId = data.id;
-  dialog.visible = true;
-  dialog.title = t('document.document.dialog.addChild');
-  dialog.isEdit = false;
-};
-
-// 获取可选类型(根据父节点类型限制)
-const getAvailableTypes = () => {
-  if (!currentNode.value) {
-    // 顶级节点,可以选择所有类型
-    return [
-      { label: t('document.document.type.folder'), value: 0 },
-      { label: t('document.document.type.country'), value: 1 },
-      { label: t('document.document.type.center'), value: 2 }
-    ];
-  }
-
-  const parentType = currentNode.value.type;
-
-  if (parentType === 1 || parentType === 2) {
-    // 父节点是国家或中心,子节点只能是中心或文件夹
-    return [
-      { label: t('document.document.type.folder'), value: 0 },
-      { label: t('document.document.type.center'), value: 2 }
-    ];
-  } else {
-    // 父节点是文件夹,子节点只能是文件夹
-    return [{ label: t('document.document.type.folder'), value: 0 }];
-  }
-};
-
-// 下拉菜单命令处理
-const handleCommand = (command: string, data: FolderListVO) => {
-  if (command.startsWith('add:')) {
-    const cmdPart = command.split(':')[1];
-    if (cmdPart === 'document') {
-      handleAddDocument(data);
-    } else {
-      const type = parseInt(cmdPart);
-      handleAddChildWithType(data, type);
-    }
-  } else if (command === 'edit') {
-    handleEdit(data);
-  } else if (command === 'delete') {
-    handleDelete(data);
-  }
-};
-
-// 新增子节点(指定类型)
-const handleAddChildWithType = (data: FolderListVO, type: number) => {
-  reset();
-  currentNode.value = data;
-  form.value.projectId = props.projectId;
-  form.value.parentId = data.id;
-  form.value.type = type;
-  dialog.visible = true;
-  const typeLabel =
-    type === 0 ? t('document.document.type.folder') : type === 1 ? t('document.document.type.country') : t('document.document.type.center');
-  dialog.title =
-    type === 0
-      ? t('document.document.dialog.addFolder')
-      : type === 1
-        ? t('document.document.dialog.addCountry')
-        : t('document.document.dialog.addCenter');
-  dialog.isEdit = false;
-};
-
-// 提交表单
-const 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}</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();
-      } catch (error) {
-        console.error(dialog.isEdit ? t('document.document.message.editFailed') : t('document.document.message.addFailed'), error);
-      } finally {
-        buttonLoading.value = false;
-      }
-    }
-  });
-};
-
-// 编辑
-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;
-    }
-
-    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 handleDelete = async (data: FolderListVO) => {
-  // 检查是否有子节点
-  if (data.children && data.children.length > 0) {
-    ElMessage.warning(t('document.document.message.hasChildren'));
-    return;
-  }
-
-  try {
-    await ElMessageBox.confirm(t('document.document.message.deleteConfirm', { name: data.name }), t('document.document.message.deleteTitle'), {
-      confirmButtonText: t('document.document.message.confirmButton'),
-      cancelButtonText: t('document.document.message.cancelButton'),
-      type: 'warning'
-    });
-
-    loading.value = true;
-    await delFolder(data.id);
-    ElMessage.success(t('document.document.message.deleteSuccess'));
-    await getList();
-  } catch (error: any) {
-    // 用户取消删除或删除失败
-    if (error !== 'cancel') {
-      console.error(t('document.document.message.deleteFailed'), error);
-    }
-  } finally {
-    loading.value = false;
-  }
-};
-
-// 菜单状态管理
-const activeMenu = ref<string | number | null>(null); // 当前激活的一级菜单
-const showSecondaryMenu = ref(false); // 是否显示二级菜单
-const primaryMenuStyle = ref<any>({}); // 一级菜单的样式(位置)
-const secondaryMenuStyle = ref<any>({}); // 二级菜单的样式(位置)
-const currentMenuData = ref<FolderListVO | null>(null); // 当前操作的菜单数据
-
-// 切换菜单显示
-const toggleMenu = (event: MouseEvent, data: FolderListVO) => {
-  // 只处理点击事件,忽略其他事件
-  if (event.type !== 'click') {
-    return;
-  }
-
-  event.stopPropagation();
-  event.preventDefault();
-
-  const trigger = event.currentTarget as HTMLElement;
-  const rect = trigger.getBoundingClientRect();
-
-  if (activeMenu.value === data.id) {
-    // 如果点击的是同一个菜单,关闭它
-    closeAllMenus();
-  } else {
-    // 关闭之前的菜单,打开新菜单
-    // 计算一级菜单位置
-    primaryMenuStyle.value = {
-      left: `${rect.left}px`,
-      top: `${rect.bottom + 2}px`
-    };
-
-    activeMenu.value = data.id;
-    currentMenuData.value = data;
-    showSecondaryMenu.value = false;
-  }
-};
-
-// 切换二级菜单显示
-const toggleSubmenu = (event: MouseEvent) => {
-  // 只处理点击事件,忽略其他事件
-  if (event.type !== 'click') {
-    return;
-  }
-
-  event.stopPropagation();
-  event.preventDefault();
-
-  const target = event.currentTarget as HTMLElement;
-  const rect = target.getBoundingClientRect();
-
-  if (showSecondaryMenu.value) {
-    // 如果已经显示,则关闭
-    showSecondaryMenu.value = false;
-  } else {
-    // 先设置位置,再显示(避免位置计算前就显示)
-    secondaryMenuStyle.value = {
-      left: `${rect.right + 5}px`,
-      top: `${rect.top}px`
-    };
-
-    // 使用 nextTick 确保位置设置后再显示
-    nextTick(() => {
-      showSecondaryMenu.value = true;
-    });
-  }
-};
-
-// 处理菜单项点击
-const handleMenuItemClick = (command: string, data: FolderListVO | null) => {
-  if (!data) return;
-
-  // 执行命令
-  handleCommand(command, data);
-
-  // 关闭所有菜单
-  closeAllMenus();
-};
-
-// ========== 文档相关函数 ==========
-
-// 重置文档表单
-const resetDocumentForm = () => {
-  documentForm.value = { ...initDocumentFormData };
-  uploadedFileId.value = '';
-  // 如果没有计划文档权限,默认为非计划文档
-  if (!hasAddPlanPermission.value) {
-    documentForm.value.type = 0;
-  }
-  documentForm.value.submitterId = userStore.userId;
-  documentForm.value.projectId = props.projectId;
-  currentUserName.value = userStore.nickname || '';
-  submitterOptions.value = [];
-  documentFormRef.value?.resetFields();
-};
-
-// 添加文档
-const handleAddDocument = (data: FolderListVO) => {
-  resetDocumentForm();
-  documentForm.value.folderId = data.id;
-  documentDialog.visible = true;
-  documentDialog.title = t('document.document.dialog.addDocument');
-};
-
-// 处理文档类型变化
-const handleDocumentTypeChange = (value: number) => {
-  if (value === 0) {
-    // 非计划文档,递交人为当前用户
-    documentForm.value.submitterId = userStore.userId;
-    documentForm.value.submitDeadline = undefined;
-    documentForm.value.planType = undefined;
-  } else {
-    // 计划文档,清空递交人
-    documentForm.value.submitterId = undefined;
-  }
-};
-
-// 搜索递交人
-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: props.projectId || 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) {
-    // 解析文件ID(可能是逗号分隔的字符串)
-    const ids = newVal.split(',').filter((id) => id.trim());
-    if (ids.length > 0) {
-      documentForm.value.ossId = parseInt(ids[0]);
-      // 自动设置递交时间为当前时间
-      const now = new Date();
-      documentForm.value.submitTime = proxy?.parseTime(now, '{y}-{m}-{d} {h}:{i}:{s}');
-
-      // 对于非计划文档,自动设置递交人为当前用户
-      if (documentForm.value.type === 0) {
-        documentForm.value.submitterId = userStore.userId;
-      }
-    }
-  } else {
-    documentForm.value.ossId = undefined;
-    documentForm.value.submitTime = undefined;
-    documentForm.value.submitterId = undefined;
-  }
-});
-
-// 取消文档对话框
-const cancelDocument = () => {
-  resetDocumentForm();
-  documentDialog.visible = false;
-};
-
-// 提交文档表单
-const submitDocumentForm = () => {
-  documentFormRef.value?.validate(async (valid: boolean) => {
-    if (valid) {
-      documentButtonLoading.value = true;
-      try {
-        // 根据是否上传了文件设置status字段:上传了文件status为1,否则为0
-        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,
-          submitterId: documentForm.value.submitterId || (documentForm.value.type === 0 ? userStore.userId : 0),
-          folderId: documentForm.value.folderId || 0,
-          submitDeadline: documentForm.value.submitDeadline || '',
-          planType: documentForm.value.planType || '',
-          ossId: hasUploadedFile ? uploadedFileId.value : null,
-          submitTime: documentForm.value.submitTime || (hasUploadedFile ? new Date().toISOString() : ''),
-          projectId: documentForm.value.projectId || props.projectId,
-          status: hasUploadedFile ? 1 : 0,
-          note: documentForm.value.note || ''
-        };
-
-        await addDocument(submitData);
-        proxy?.$modal.msgSuccess(t('document.document.message.addDocumentSuccess'));
-        documentDialog.visible = false;
-        // 刷新文档列表
-        await getDocumentList();
-      } catch (error) {
-        console.error(t('document.document.message.addDocumentFailed'), error);
-      } finally {
-        documentButtonLoading.value = false;
-      }
-    }
-  });
-};
-
-// ========== 文档列表相关函数 ==========
-
-// 点击文件夹节点
-const handleFolderClick = (data: FolderListVO) => {
-  // 只有文件夹类型(type=0)才显示文档列表
-  if (data.type === 0) {
-    selectedFolder.value = data;
-    // 重置查询参数
-    documentQueryParams.name = '';
-    documentQueryParams.pageNum = 1;
-    // 加载文档列表
-    getDocumentList();
-  } else {
-    selectedFolder.value = null;
-    documentList.value = [];
-  }
-};
-
-// 查询文档列表
-const getDocumentList = async () => {
-  if (!selectedFolder.value) return;
-
-  documentLoading.value = true;
-  documentQueryParams.folderId = selectedFolder.value.id;
-
-  try {
-    const res = await listDocument(documentQueryParams);
-    documentList.value = res.rows || [];
-    documentTotal.value = res.total || 0;
-  } catch (error) {
-    console.error('Failed to get document list:', error);
-    ElMessage.error(t('document.document.message.getDocumentListFailed'));
-  } finally {
-    documentLoading.value = false;
-  }
-};
-
-// 搜索按钮
-const handleDocumentQuery = () => {
-  documentQueryParams.pageNum = 1;
-  getDocumentList();
-};
-
-// 重置按钮
-const handleDocumentReset = () => {
-  documentQueryParams.name = '';
-  documentQueryParams.pageNum = 1;
-  getDocumentList();
-};
-
-// 查看审核记录
-const handleViewAuditLog = (row: DocumentVO) => {
-  console.log('handleViewAuditLog called with row:', row);
-  auditLogDialog.documentId = row.id;
-  auditLogDialog.visible = true;
-};
-
-// 审核按钮点击事件
-const handleAuditClick = (row: DocumentVO) => {
-  auditDialog.document = row;
-  auditDialog.visible = true;
-};
-
-// 下载文档
-const handleDownload = (row: DocumentVO) => {
-  if (!row.url) {
-    ElMessage.warning(t('document.document.message.noFileToDownload'));
-    return;
-  }
-
-  // 新建a标签下载文件
-  const a = document.createElement('a');
-  a.href = row.url;
-  a.download = row.fileName || row.name || 'document';
-  document.body.appendChild(a);
-  a.click();
-  document.body.removeChild(a);
-};
-
-// 递交文档
-const handleSubmit = (row: DocumentVO) => {
-  currentDocument.value = row;
-  submitForm.value = {
-    ossId: ''
-  };
-  submitDialog.visible = true;
-  submitDialog.title = t('document.document.dialog.submitDocument');
-  // 重置表单验证
-  nextTick(() => {
-    submitFormRef.value?.clearValidate();
-  });
-};
-
-// 取消递交
-const cancelSubmit = () => {
-  submitDialog.visible = false;
-  submitForm.value = {
-    ossId: ''
-  };
-  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.ossId
-        };
-        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;
-      }
-    }
-  });
-};
-
-// 确认递交文档
-const handleConfirmSubmit = async (row: DocumentVO) => {
-  try {
-    await ElMessageBox.confirm('确认该文件已成功递交?', '操作确认', {
-      confirmButtonText: '确认',
-      cancelButtonText: '取消',
-      type: 'warning'
-    });
-
-    await confirmSubmit(row.id);
-    ElMessage.success('确认递交成功');
-    // 刷新文档列表
-    await getDocumentList();
-  } catch (error) {
-    if (error !== 'cancel') {
-      console.error('确认递交失败:', error);
-      ElMessage.error('确认递交失败');
-    }
-  }
-};
-
-// 归档文档
-const handleArchive = (row: DocumentVO) => {
-  archiveDialog.currentDocument = row;
-  archiveDialog.visible = true;
-};
-
-// 处理归档确认
-const handleArchiveConfirm = async (row: DocumentVO) => {
-  try {
-    // 调用归档API
-    await filingDocument(row.id);
-    ElMessage.success(t('document.document.message.archiveSuccess'));
-    // 关闭对话框
-    archiveDialog.visible = false;
-    // 刷新文档列表
-    await getDocumentList();
-  } catch (error) {
-    console.error(t('document.document.message.archiveFailed'), error);
-    ElMessage.error(t('document.document.message.archiveFailed'));
-  }
-};
-
-// 标识文档
-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 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;
-      }
-    }
-  });
-};
-
-// ========== 文件类型判断函数 ==========
-
-// 判断是否为Word文档
-const isWordFile = (fileName: string): boolean => {
-  if (!fileName) return false;
-  const lowerFileName = fileName.toLowerCase();
-  return lowerFileName.endsWith('.doc') || lowerFileName.endsWith('.docx');
-};
-
-// 判断是否为Excel文档
-const isExcelFile = (fileName: string): boolean => {
-  if (!fileName) return false;
-  const lowerFileName = fileName.toLowerCase();
-  return lowerFileName.endsWith('.xls') || lowerFileName.endsWith('.xlsx') || lowerFileName.endsWith('.csv');
-};
-
-// 判断是否为PPT文档
-const isPPTFile = (fileName: string): boolean => {
-  if (!fileName) return false;
-  const lowerFileName = fileName.toLowerCase();
-  return lowerFileName.endsWith('.ppt') || lowerFileName.endsWith('.pptx');
-};
-
-// 判断是否为PDF文档
-const isPDFFile = (fileName: string): boolean => {
-  if (!fileName) return false;
-  return fileName.toLowerCase().endsWith('.pdf');
-};
-
-// 获取文件类型对应的图标类名
-const getFileIconClass = (fileName: string): string => {
-  if (isWordFile(fileName)) {
-    return 'document-word';
-  } else if (isExcelFile(fileName)) {
-    return 'document-excel';
-  } else if (isPPTFile(fileName)) {
-    return 'document-ppt';
-  } else if (isPDFFile(fileName)) {
-    return 'document-pdf';
-  } else {
-    return 'document-document';
-  }
-};
-
-// 关闭所有菜单
-const closeAllMenus = () => {
-  showSecondaryMenu.value = false;
-  activeMenu.value = null;
-  currentMenuData.value = null;
-  primaryMenuStyle.value = {};
-  secondaryMenuStyle.value = {};
-};
-
-// 点击页面其他地方关闭菜单
-const handleClickOutside = (event: Event) => {
-  // 如果没有激活的菜单,直接返回
-  if (!activeMenu.value && !showSecondaryMenu.value) {
-    return;
-  }
-
-  const target = event.target as HTMLElement;
-
-  // 检查点击是否在菜单内部或触发器上
-  const isClickInsideMenu = target.closest('.primary-menu') || target.closest('.secondary-menu') || target.closest('.menu-trigger');
-
-  // 如果点击在菜单外部,立即关闭所有菜单
-  if (!isClickInsideMenu) {
-    closeAllMenus();
-  }
-};
-
-// 处理滚动事件,滚动时关闭菜单
-const handleScroll = () => {
-  if (activeMenu.value || showSecondaryMenu.value) {
-    closeAllMenus();
-  }
-};
-
-// 初始化
-onMounted(() => {
-  getList();
-  // 添加全局点击监听(捕获阶段)
-  document.addEventListener('click', handleClickOutside, true);
-  // 添加滚动监听
-  document.addEventListener('scroll', handleScroll, true);
-});
-
-// 清理
-onUnmounted(() => {
-  // 移除全局点击监听
-  document.removeEventListener('click', handleClickOutside, true);
-  // 移除滚动监听
-  document.removeEventListener('scroll', handleScroll, true);
-  // 清理菜单状态
-  closeAllMenus();
-  // 清理搜索定时器
-  if (submitterSearchTimer) {
-    clearTimeout(submitterSearchTimer);
-    submitterSearchTimer = null;
-  }
-});
-</script>
-
-<style scoped lang="scss">
-.flex {
-  display: flex;
-}
-
-.justify-between {
-  justify-content: space-between;
-}
-
-.items-center {
-  align-items: center;
-}
-
-.text-lg {
-  font-size: 1.125rem;
-}
-
-.font-bold {
-  font-weight: 700;
-}
-
-.content-wrapper {
-  display: flex;
-  height: calc(100vh - 250px);
-  min-height: 500px;
-}
-
-.tree-container {
-  width: 300px;
-  border-right: 1px solid #e4e7ed;
-  display: flex;
-  flex-direction: column;
-}
-
-.tree-header {
-  padding: 10px;
-  border-bottom: 1px solid #e4e7ed;
-}
-
-.tree-scrollbar {
-  flex: 1;
-  overflow: hidden;
-
-  :deep(.el-scrollbar__view) {
-    padding: 10px;
-  }
-}
-
-.custom-tree-node {
-  flex: 1;
-  display: flex;
-  align-items: center;
-  font-size: 14px;
-  padding-right: 8px;
-
-  .el-icon {
-    margin-right: 8px;
-    font-size: 16px;
-  }
-
-  .node-label {
-    flex: 1;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    white-space: nowrap;
-    cursor: pointer;
-
-    &:hover {
-      color: var(--el-color-primary);
-    }
-  }
-
-  .node-actions {
-    display: none;
-    position: relative;
-  }
-
-  &:hover .node-actions {
-    display: inline-flex;
-    gap: 4px;
-  }
-}
-
-.menu-trigger {
-  cursor: pointer;
-  display: flex;
-  align-items: center;
-  font-size: 16px;
-  padding: 4px;
-  border-radius: 4px;
-  transition:
-    background-color 0.3s,
-    color 0.3s;
-
-  &:hover {
-    background-color: #f5f7fa;
-    color: var(--el-color-primary);
-  }
-}
-
-.content-container {
-  flex: 1;
-  padding: 20px;
-  overflow: auto;
-}
-
-.document-list-container {
-  width: 100%;
-
-  .search-form {
-    margin-bottom: 16px;
-  }
-
-  .file-name-cell {
-    display: flex;
-    align-items: center;
-    gap: 8px;
-    padding: 0 8px;
-
-    .file-icon {
-      flex-shrink: 0;
-      width: 18px;
-      height: 18px;
-      vertical-align: middle;
-    }
-
-    .file-name-text {
-      flex: 1;
-      text-align: left;
-      overflow: hidden;
-      text-overflow: ellipsis;
-      white-space: nowrap;
-    }
-
-    .download-btn {
-      flex-shrink: 0;
-      font-size: 16px;
-
-      &:hover {
-        transform: scale(1.1);
-      }
-    }
-  }
-}
-
-.detail-content {
-  max-width: 800px;
-}
-
-/* 一级菜单样式 */
-.primary-menu {
-  position: fixed;
-  min-width: 120px;
-  background: #fff;
-  border: 1px solid #e4e7ed;
-  border-radius: 4px;
-  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
-  padding: 5px 0;
-  margin: 0;
-  list-style: none;
-  z-index: 2000;
-
-  .menu-item {
-    padding: 8px 16px;
-    cursor: pointer;
-    font-size: 14px;
-    color: #606266;
-    transition:
-      background-color 0.3s,
-      color 0.3s;
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    white-space: nowrap;
-
-    &:hover {
-      background-color: #f5f7fa;
-      color: var(--el-color-primary);
-    }
-
-    &.has-submenu {
-      .arrow-icon {
-        margin-left: 8px;
-        font-size: 12px;
-      }
-    }
-  }
-}
-
-/* 二级菜单样式 */
-.secondary-menu {
-  position: fixed;
-  min-width: 120px;
-  background: #fff;
-  border: 1px solid #e4e7ed;
-  border-radius: 4px;
-  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
-  padding: 5px 0;
-  margin: 0;
-  list-style: none;
-  z-index: 3000;
-
-  .menu-item {
-    padding: 8px 16px;
-    cursor: pointer;
-    font-size: 14px;
-    color: #606266;
-    transition:
-      background-color 0.3s,
-      color 0.3s;
-
-    &:hover {
-      background-color: #f5f7fa;
-      color: var(--el-color-primary);
-    }
-  }
-}
-
-.show-overflow-tooltip {
-  position: relative;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-  max-width: 100%;
-  cursor: help;
-}
-
-.show-overflow-tooltip:hover {
-  overflow: visible;
-  white-space: normal;
-  word-break: break-all;
-  position: absolute;
-  background-color: rgba(0, 0, 0, 0.8);
-  color: white;
-  padding: 4px 8px;
-  border-radius: 4px;
-  z-index: 1000;
-  max-width: 300px;
-  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
-}
-</style>

+ 665 - 0
src/views/document/folder/document/DocumentList.vue

@@ -0,0 +1,665 @@
+<template>
+  <div v-if="selectedFolder" class="document-list-container">
+    <!-- 搜索栏 -->
+    <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-form-item>
+      <el-form-item>
+        <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>
+
+    <!-- 文档列表 -->
+    <el-table v-loading="documentLoading" :data="documentList" border style="margin-top: 10px">
+      <el-table-column prop="id" width="55" align="center" :label="t('document.document.documentList.index')">
+        <template #default="scope">
+          {{ 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
+      >
+        <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')" width="120" align="center">
+        <template #default="scope">
+          <dict-tag v-if="scope.row.planType" :options="plan_document_type" :value="scope.row.planType" />
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="status" :label="t('document.document.documentList.status')" width="120" align="center">
+        <template #default="scope">
+          <DocumentStatusTag :status="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column prop="submitter" :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">
+        <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">
+        <template #default="scope">
+          <span v-if="scope.row.submitTime">{{ parseTime(scope.row.submitTime) }}</span>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="url" :label="t('document.document.documentList.url')" min-width="200">
+        <template #default="scope">
+          <div v-if="scope.row.fileName" class="file-name-cell">
+            <svg-icon :icon-class="getFileIconClass(scope.row.fileName)" class="file-icon" :size="18" />
+            <span class="file-name-text show-overflow-tooltip">{{ scope.row.fileName }}</span>
+          </div>
+          <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">
+        <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">
+        <template #default="scope">
+          <span v-if="scope.row.updateTime">{{ parseTime(scope.row.updateTime) }}</span>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <!-- 操作列 -->
+      <el-table-column :label="t('document.document.documentList.action')" width="200" align="center" fixed="right">
+        <template #default="scope">
+          <el-button
+            v-if="scope.row.url && scope.row.status === 1"
+            v-hasPermi="['document:document:audit']"
+            type="primary"
+            link
+            :icon="Select"
+            @click="handleAuditClick(scope.row)"
+            :title="t('document.document.button.audit')"
+          />
+          <el-button
+            v-if="(scope.row.status === 0 || scope.row.status === 2) && scope.row.submitterId === userStore.userId"
+            v-hasPermi="['document:document:submit']"
+            type="primary"
+            link
+            :icon="Upload"
+            @click="handleSubmit(scope.row)"
+            :title="t('document.document.button.submit')"
+          />
+          <el-button
+            v-if="(scope.row.status === 0 || scope.row.status === 2) && scope.row.submitterId === userStore.userId"
+            v-hasPermi="['document:document:confirmSubmit']"
+            type="primary"
+            link
+            icon="Delete"
+            @click="handleConfirmSubmit(scope.row)"
+            :title="t('document.document.button.confirmSubmit')"
+          />
+          <el-button
+            v-hasPermi="['document:document:mark']"
+            type="primary"
+            link
+            icon="Flag"
+            @click="handleMark(scope.row)"
+            :title="t('document.document.button.mark')"
+          />
+          <el-button
+            v-if="scope.row.ossId"
+            type="primary"
+            link
+            icon="Download"
+            @click="handleDownload(scope.row)"
+            :title="t('document.document.button.download')"
+          />
+          <el-button
+            v-hasPermi="['document:document:logAudit']"
+            type="primary"
+            link
+            icon="DocumentCopy"
+            @click="handleViewAuditLog(scope.row)"
+            :title="t('document.document.button.viewAuditLog')"
+          />
+          <el-button
+            v-if="scope.row.status === 3"
+            v-hasPermi="['document:document:filing']"
+            type="primary"
+            link
+            icon="UploadFilled"
+            @click="handleArchive(scope.row)"
+            :title="t('document.document.button.archive')"
+          />
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <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>
+
+    <!-- 递交文档对话框 -->
+    <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="ossId">
+          <fileUpload v-model="submitForm.ossId" :limit="1" :action="'/common/resource/oss/upload'" />
+        </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"
+    />
+
+    <!-- 归档确认对话框 -->
+    <ArchiveConfirmDialog v-model="archiveDialog.visible" :document="archiveDialog.currentDocument" @confirm="handleArchiveConfirm" />
+
+    <!-- 审核文档对话框 -->
+    <DocumentAuditDialog
+      v-model="auditDialog.visible"
+      :document="auditDialog.document"
+      :title="t('document.document.dialog.auditDocument')"
+      @success="getDocumentList"
+    />
+  </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 { useI18n } from 'vue-i18n';
+import {
+  listDocument,
+  markDocument,
+  submitDocument,
+  confirmSubmit,
+  filingDocument,
+  downloadDocumentFile
+} from '@/api/document/document';
+import { DocumentQuery, DocumentVO, DocumentMarkForm, DocumentSubmitForm } from '@/api/document/document/types';
+import { 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 AuditLogDialog from '@/components/AuditLogDialog/index.vue';
+import { listDocumentAuditLog } from '@/api/document/document';
+import ArchiveConfirmDialog from './components/ArchiveConfirmDialog.vue';
+import DocumentAuditDialog from './components/DocumentAuditDialog.vue';
+import DocumentStatusTag from '@/components/DocumentStatusTag/index.vue';
+
+interface Props {
+  selectedFolder: FolderListVO | null;
+  treeData: FolderListVO[];
+}
+
+const props = defineProps<Props>();
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { t } = useI18n();
+const userStore = useUserStore();
+const { plan_document_type, center_file_specification, project_file_specification } = toRefs<any>(
+  proxy?.useDict('plan_document_type', 'center_file_specification', 'project_file_specification')
+);
+
+// 文档列表数据
+const documentList = ref<DocumentVO[]>([]);
+const documentLoading = ref(false);
+const documentTotal = ref(0);
+
+// 文档查询参数
+const documentQueryParams = reactive<DocumentQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  name: '',
+  folderId: undefined
+});
+
+// 标识文档对话框
+const markDialog = reactive({
+  visible: false,
+  title: ''
+});
+
+// 标识表单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: ''
+});
+
+// 递交表单ref
+const submitFormRef = ref<FormInstance>();
+const submitButtonLoading = ref(false);
+
+// 递交表单数据
+interface SubmitForm {
+  ossId: string;
+}
+
+const submitForm = ref<SubmitForm>({
+  ossId: ''
+});
+
+// 递交表单验证规则
+const submitRules = reactive({
+  ossId: [
+    {
+      required: true,
+      message: t('document.document.submitRule.fileRequired'),
+      trigger: 'blur'
+    }
+  ]
+});
+
+// 当前操作的文档
+const currentDocument = ref<DocumentVO | null>(null);
+
+// 审核记录对话框
+const auditLogDialog = reactive({
+  visible: false,
+  documentId: ''
+});
+
+// 归档对话框
+const archiveDialog = reactive({
+  visible: false,
+  currentDocument: null as DocumentVO | null
+});
+
+// 审核对话框状态
+const auditDialog = reactive({
+  visible: false,
+  document: null as DocumentVO | null
+});
+
+/**
+ * 根据 specificationType 获取对应的字典
+ * @param specificationType 规格类型 (0: 中心文件, 1: 项目文件)
+ */
+const getSpecificationDict = (specificationType: number | undefined) => {
+  if (specificationType === 0) {
+    return center_file_specification?.value || [];
+  } else if (specificationType === 1) {
+    return project_file_specification?.value || [];
+  }
+  // 默认返回空数组
+  return [];
+};
+
+// 查询文档列表
+const getDocumentList = async () => {
+  if (!props.selectedFolder) return;
+
+  documentLoading.value = true;
+  documentQueryParams.folderId = props.selectedFolder.id;
+
+  try {
+    const res = await listDocument(documentQueryParams);
+    documentList.value = res.rows || [];
+    documentTotal.value = res.total || 0;
+  } catch (error) {
+    console.error('Failed to get document list:', error);
+    ElMessage.error(t('document.document.message.getDocumentListFailed'));
+  } finally {
+    documentLoading.value = false;
+  }
+};
+
+// 搜索按钮
+const handleDocumentQuery = () => {
+  documentQueryParams.pageNum = 1;
+  getDocumentList();
+};
+
+// 重置按钮
+const handleDocumentReset = () => {
+  documentQueryParams.name = '';
+  documentQueryParams.pageNum = 1;
+  getDocumentList();
+};
+
+// 查看审核记录
+const handleViewAuditLog = (row: DocumentVO) => {
+  auditLogDialog.documentId = row.id;
+  auditLogDialog.visible = true;
+};
+
+// 审核按钮点击事件
+const handleAuditClick = (row: DocumentVO) => {
+  auditDialog.document = row;
+  auditDialog.visible = true;
+};
+
+// 下载文档
+const handleDownload = async (row: DocumentVO) => {
+  if (!row.ossId) {
+    ElMessage.warning(t('document.document.message.noFileToDownload'));
+    return;
+  }
+
+  await downloadDocumentFile(row.ossId, row.fileName || row.name);
+};
+
+// 递交文档
+const handleSubmit = (row: DocumentVO) => {
+  currentDocument.value = row;
+  submitForm.value = {
+    ossId: ''
+  };
+  submitDialog.visible = true;
+  submitDialog.title = t('document.document.dialog.submitDocument');
+  nextTick(() => {
+    submitFormRef.value?.clearValidate();
+  });
+};
+
+// 取消递交
+const cancelSubmit = () => {
+  submitDialog.visible = false;
+  submitForm.value = {
+    ossId: ''
+  };
+  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.ossId
+        };
+        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;
+      }
+    }
+  });
+};
+
+// 确认递交文档
+const handleConfirmSubmit = async (row: DocumentVO) => {
+  try {
+    await ElMessageBox.confirm('确认该文件已成功递交?', '操作确认', {
+      confirmButtonText: '确认',
+      cancelButtonText: '取消',
+      type: 'warning'
+    });
+
+    await confirmSubmit(row.id);
+    ElMessage.success('确认递交成功');
+    await getDocumentList();
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('确认递交失败:', error);
+      ElMessage.error('确认递交失败');
+    }
+  }
+};
+
+// 归档文档
+const handleArchive = (row: DocumentVO) => {
+  archiveDialog.currentDocument = row;
+  archiveDialog.visible = true;
+};
+
+// 处理归档确认
+const handleArchiveConfirm = async (row: DocumentVO) => {
+  try {
+    await filingDocument(row.id);
+    ElMessage.success(t('document.document.message.archiveSuccess'));
+    archiveDialog.visible = false;
+    await getDocumentList();
+  } catch (error) {
+    console.error(t('document.document.message.archiveFailed'), error);
+    ElMessage.error(t('document.document.message.archiveFailed'));
+  }
+};
+
+// 标识文档
+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 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 isWordFile = (fileName: string): boolean => {
+  if (!fileName) return false;
+  const lowerFileName = fileName.toLowerCase();
+  return lowerFileName.endsWith('.doc') || lowerFileName.endsWith('.docx');
+};
+
+const isExcelFile = (fileName: string): boolean => {
+  if (!fileName) return false;
+  const lowerFileName = fileName.toLowerCase();
+  return lowerFileName.endsWith('.xls') || lowerFileName.endsWith('.xlsx') || lowerFileName.endsWith('.csv');
+};
+
+const isPPTFile = (fileName: string): boolean => {
+  if (!fileName) return false;
+  const lowerFileName = fileName.toLowerCase();
+  return lowerFileName.endsWith('.ppt') || lowerFileName.endsWith('.pptx');
+};
+
+const isPDFFile = (fileName: string): boolean => {
+  if (!fileName) return false;
+  return fileName.toLowerCase().endsWith('.pdf');
+};
+
+const getFileIconClass = (fileName: string): string => {
+  if (isWordFile(fileName)) {
+    return 'document-word';
+  } else if (isExcelFile(fileName)) {
+    return 'document-excel';
+  } else if (isPPTFile(fileName)) {
+    return 'document-ppt';
+  } else if (isPDFFile(fileName)) {
+    return 'document-pdf';
+  } else {
+    return 'document-document';
+  }
+};
+
+// 监听选中文件夹变化
+watch(
+  () => props.selectedFolder,
+  (newFolder) => {
+    if (newFolder) {
+      documentQueryParams.name = '';
+      documentQueryParams.pageNum = 1;
+      getDocumentList();
+    } else {
+      documentList.value = [];
+    }
+  }
+);
+
+// 暴露方法给父组件
+defineExpose({
+  getDocumentList
+});
+</script>
+
+<style scoped lang="scss">
+.document-list-container {
+  width: 100%;
+
+  .search-form {
+    margin-bottom: 16px;
+  }
+
+  .file-name-cell {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 0 8px;
+
+    .file-icon {
+      flex-shrink: 0;
+      width: 18px;
+      height: 18px;
+      vertical-align: middle;
+    }
+
+    .file-name-text {
+      flex: 1;
+      text-align: left;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+
+    .download-btn {
+      flex-shrink: 0;
+      font-size: 16px;
+
+      &:hover {
+        transform: scale(1.1);
+      }
+    }
+  }
+}
+
+.show-overflow-tooltip {
+  position: relative;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  max-width: 100%;
+  cursor: help;
+}
+
+.show-overflow-tooltip:hover {
+  overflow: visible;
+  white-space: normal;
+  word-break: break-all;
+  position: absolute;
+  background-color: rgba(0, 0, 0, 0.8);
+  color: white;
+  padding: 4px 8px;
+  border-radius: 4px;
+  z-index: 1000;
+  max-width: 300px;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
+}
+</style>

+ 628 - 0
src/views/document/folder/document/FolderTree.vue

@@ -0,0 +1,628 @@
+<template>
+  <div class="tree-container">
+    <div class="tree-header">
+      <el-button v-hasPermi="['document:folder:add']" type="primary" icon="Plus" size="small" @click="handleAddFolder">
+        {{ t('document.document.button.newFolder') }}
+      </el-button>
+    </div>
+    <el-scrollbar class="tree-scrollbar">
+      <el-tree v-loading="loading" :data="treeData" :props="treeProps" node-key="id" default-expand-all :expand-on-click-node="false">
+        <template #default="{ node, data }">
+          <span class="custom-tree-node">
+            <el-icon>
+              <Folder v-if="data.type === 0" />
+              <Location v-else-if="data.type === 1" />
+              <OfficeBuilding v-else-if="data.type === 2" />
+              <Document v-else />
+            </el-icon>
+            <span class="node-label" @click="handleFolderClick(data)">{{ node.label }}</span>
+            <span class="node-actions">
+              <span class="menu-trigger" @click="toggleMenu($event, data)">
+                <el-icon>
+                  <MoreFilled />
+                </el-icon>
+              </span>
+            </span>
+          </span>
+        </template>
+      </el-tree>
+    </el-scrollbar>
+
+    <!-- 一级菜单 -->
+    <ul class="primary-menu" v-if="activeMenu !== null" :style="primaryMenuStyle">
+      <li class="menu-item has-submenu" v-hasPermi="['document:folder:add']" @click.stop="toggleSubmenu($event)">
+        <span>{{ t('document.document.menu.add') }}</span>
+        <el-icon class="arrow-icon">
+          <ArrowRight />
+        </el-icon>
+      </li>
+      <li class="menu-item" v-hasPermi="['document:folder:edit']" @click="handleMenuItemClick('edit', currentMenuData)">
+        <span>{{ t('document.document.menu.edit') }}</span>
+      </li>
+      <li class="menu-item" v-hasPermi="['document:folder:remove']" @click="handleMenuItemClick('delete', currentMenuData)">
+        <span>{{ t('document.document.menu.delete') }}</span>
+      </li>
+    </ul>
+
+    <!-- 二级菜单 -->
+    <ul class="secondary-menu" v-if="showSecondaryMenu" :style="secondaryMenuStyle">
+      <!-- 国家或中心:显示中心和文件夹 -->
+      <template v-if="currentMenuData && (currentMenuData.type === 1 || currentMenuData.type === 2)">
+        <li class="menu-item" @click="handleMenuItemClick('add:2', currentMenuData)">
+          <span>{{ t('document.document.menu.center') }}</span>
+        </li>
+        <li class="menu-item" @click="handleMenuItemClick('add:0', currentMenuData)">
+          <span>{{ t('document.document.menu.folder') }}</span>
+        </li>
+      </template>
+      <!-- 文件夹:只显示文件夹和文档 -->
+      <template v-else-if="currentMenuData && currentMenuData.type === 0">
+        <li class="menu-item" @click="handleMenuItemClick('add:0', currentMenuData)">
+          <span>{{ t('document.document.menu.folder') }}</span>
+        </li>
+        <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 :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>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="buttonLoading" type="primary" @click="submitFolderForm">{{ t('document.document.button.submit') }}</el-button>
+          <el-button @click="cancel">{{ t('document.document.button.cancel') }}</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, onUnmounted, nextTick, getCurrentInstance, watch } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { listFolder, addFolder, delFolder, getFolder, updateFolder } from '@/api/document/folder';
+import { FolderListVO, FolderForm } from '@/api/document/folder/types';
+import { Folder, Document, 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';
+
+interface Props {
+  projectId?: number | string;
+}
+
+const props = defineProps<Props>();
+
+const emit = defineEmits<{
+  folderClick: [folder: FolderListVO];
+  addDocument: [folder: FolderListVO];
+  refresh: [];
+}>();
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { t } = useI18n();
+
+// 数据定义
+const loading = ref(false);
+const buttonLoading = ref(false);
+const treeData = ref<FolderListVO[]>([]);
+const folderFormRef = ref<FormInstance>();
+
+// 对话框
+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 = {
+  id: undefined,
+  projectId: undefined,
+  parentId: undefined,
+  type: 0,
+  name: '',
+  status: 0,
+  note: '',
+  restrictionLevel: -1
+};
+
+// 表单数据
+const form = ref<FolderForm>({ ...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 = {
+  children: 'children',
+  label: 'name'
+};
+
+// 菜单状态管理
+const activeMenu = ref<string | number | null>(null);
+const showSecondaryMenu = ref(false);
+const primaryMenuStyle = ref<any>({});
+const secondaryMenuStyle = ref<any>({});
+const currentMenuData = ref<FolderListVO | null>(null);
+
+// 获取文件夹列表
+const getList = async () => {
+  if (!props.projectId) {
+    ElMessage.warning(t('document.document.message.projectIdNotExist'));
+    return;
+  }
+
+  loading.value = true;
+  try {
+    const res = await listFolder({ projectId: props.projectId } as any);
+    treeData.value = res.data || [];
+  } catch (error) {
+    ElMessage.error(t('document.document.message.getFolderListFailed'));
+    console.error(error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 表单重置
+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 = () => {
+  reset();
+  currentNode.value = null;
+  form.value.projectId = props.projectId;
+  form.value.parentId = undefined;
+  dialog.visible = true;
+  dialog.title = t('document.document.dialog.addFolder');
+  dialog.isEdit = false;
+};
+
+// 新增子节点(指定类型)
+const handleAddChildWithType = (data: FolderListVO, type: number) => {
+  reset();
+  currentNode.value = data;
+  form.value.projectId = props.projectId;
+  form.value.parentId = data.id;
+  form.value.type = type;
+  dialog.visible = true;
+  const typeLabel =
+    type === 0 ? t('document.document.type.folder') : type === 1 ? t('document.document.type.country') : t('document.document.type.center');
+  dialog.title =
+    type === 0
+      ? t('document.document.dialog.addFolder')
+      : type === 1
+        ? t('document.document.dialog.addCountry')
+        : t('document.document.dialog.addCenter');
+  dialog.isEdit = false;
+};
+
+// 提交表单
+const 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}</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;
+      }
+    }
+  });
+};
+
+// 编辑
+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;
+    }
+
+    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 handleDelete = async (data: FolderListVO) => {
+  if (data.children && data.children.length > 0) {
+    ElMessage.warning(t('document.document.message.hasChildren'));
+    return;
+  }
+
+  try {
+    await ElMessageBox.confirm(t('document.document.message.deleteConfirm', { name: data.name }), t('document.document.message.deleteTitle'), {
+      confirmButtonText: t('document.document.message.confirmButton'),
+      cancelButtonText: t('document.document.message.cancelButton'),
+      type: 'warning'
+    });
+
+    loading.value = true;
+    await delFolder(data.id);
+    ElMessage.success(t('document.document.message.deleteSuccess'));
+    await getList();
+    emit('refresh');
+  } catch (error: any) {
+    if (error !== 'cancel') {
+      console.error(t('document.document.message.deleteFailed'), error);
+    }
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 切换菜单显示
+const toggleMenu = (event: MouseEvent, data: FolderListVO) => {
+  if (event.type !== 'click') return;
+
+  event.stopPropagation();
+  event.preventDefault();
+
+  const trigger = event.currentTarget as HTMLElement;
+  const rect = trigger.getBoundingClientRect();
+
+  if (activeMenu.value === data.id) {
+    closeAllMenus();
+  } else {
+    primaryMenuStyle.value = {
+      left: `${rect.left}px`,
+      top: `${rect.bottom + 2}px`
+    };
+
+    activeMenu.value = data.id;
+    currentMenuData.value = data;
+    showSecondaryMenu.value = false;
+  }
+};
+
+// 切换二级菜单显示
+const toggleSubmenu = (event: MouseEvent) => {
+  if (event.type !== 'click') return;
+
+  event.stopPropagation();
+  event.preventDefault();
+
+  const target = event.currentTarget as HTMLElement;
+  const rect = target.getBoundingClientRect();
+
+  if (showSecondaryMenu.value) {
+    showSecondaryMenu.value = false;
+  } else {
+    secondaryMenuStyle.value = {
+      left: `${rect.right + 5}px`,
+      top: `${rect.top}px`
+    };
+
+    nextTick(() => {
+      showSecondaryMenu.value = true;
+    });
+  }
+};
+
+// 处理菜单项点击
+const handleMenuItemClick = (command: string, data: FolderListVO | null) => {
+  if (!data) return;
+
+  handleCommand(command, data);
+  closeAllMenus();
+};
+
+// 下拉菜单命令处理
+const handleCommand = (command: string, data: FolderListVO) => {
+  if (command.startsWith('add:')) {
+    const cmdPart = command.split(':')[1];
+    if (cmdPart === 'document') {
+      emit('addDocument', data);
+    } else {
+      const type = parseInt(cmdPart);
+      handleAddChildWithType(data, type);
+    }
+  } else if (command === 'edit') {
+    handleEdit(data);
+  } else if (command === 'delete') {
+    handleDelete(data);
+  }
+};
+
+// 点击文件夹节点
+const handleFolderClick = (data: FolderListVO) => {
+  emit('folderClick', data);
+};
+
+// 关闭所有菜单
+const closeAllMenus = () => {
+  showSecondaryMenu.value = false;
+  activeMenu.value = null;
+  currentMenuData.value = null;
+  primaryMenuStyle.value = {};
+  secondaryMenuStyle.value = {};
+};
+
+// 点击页面其他地方关闭菜单
+const handleClickOutside = (event: Event) => {
+  if (!activeMenu.value && !showSecondaryMenu.value) return;
+
+  const target = event.target as HTMLElement;
+  const isClickInsideMenu = target.closest('.primary-menu') || target.closest('.secondary-menu') || target.closest('.menu-trigger');
+
+  if (!isClickInsideMenu) {
+    closeAllMenus();
+  }
+};
+
+// 处理滚动事件
+const handleScroll = () => {
+  if (activeMenu.value || showSecondaryMenu.value) {
+    closeAllMenus();
+  }
+};
+
+// 暴露方法给父组件
+defineExpose({
+  getList
+});
+
+// 初始化
+onMounted(() => {
+  getList();
+  document.addEventListener('click', handleClickOutside, true);
+  document.addEventListener('scroll', handleScroll, true);
+});
+
+// 清理
+onUnmounted(() => {
+  document.removeEventListener('click', handleClickOutside, true);
+  document.removeEventListener('scroll', handleScroll, true);
+  closeAllMenus();
+});
+</script>
+
+<style scoped lang="scss">
+.tree-container {
+  width: 300px;
+  border-right: 1px solid #e4e7ed;
+  display: flex;
+  flex-direction: column;
+}
+
+.tree-header {
+  padding: 10px;
+  border-bottom: 1px solid #e4e7ed;
+}
+
+.tree-scrollbar {
+  flex: 1;
+  overflow: hidden;
+
+  :deep(.el-scrollbar__view) {
+    padding: 10px;
+  }
+}
+
+.custom-tree-node {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  font-size: 14px;
+  padding-right: 8px;
+
+  .el-icon {
+    margin-right: 8px;
+    font-size: 16px;
+  }
+
+  .node-label {
+    flex: 1;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    cursor: pointer;
+
+    &:hover {
+      color: var(--el-color-primary);
+    }
+  }
+
+  .node-actions {
+    display: none;
+    position: relative;
+  }
+
+  &:hover .node-actions {
+    display: inline-flex;
+    gap: 4px;
+  }
+}
+
+.menu-trigger {
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  font-size: 16px;
+  padding: 4px;
+  border-radius: 4px;
+  transition:
+    background-color 0.3s,
+    color 0.3s;
+
+  &:hover {
+    background-color: #f5f7fa;
+    color: var(--el-color-primary);
+  }
+}
+
+.primary-menu {
+  position: fixed;
+  min-width: 120px;
+  background: #fff;
+  border: 1px solid #e4e7ed;
+  border-radius: 4px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+  padding: 5px 0;
+  margin: 0;
+  list-style: none;
+  z-index: 2000;
+
+  .menu-item {
+    padding: 8px 16px;
+    cursor: pointer;
+    font-size: 14px;
+    color: #606266;
+    transition:
+      background-color 0.3s,
+      color 0.3s;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    white-space: nowrap;
+
+    &:hover {
+      background-color: #f5f7fa;
+      color: var(--el-color-primary);
+    }
+
+    &.has-submenu {
+      .arrow-icon {
+        margin-left: 8px;
+        font-size: 12px;
+      }
+    }
+  }
+}
+
+.secondary-menu {
+  position: fixed;
+  min-width: 120px;
+  background: #fff;
+  border: 1px solid #e4e7ed;
+  border-radius: 4px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+  padding: 5px 0;
+  margin: 0;
+  list-style: none;
+  z-index: 3000;
+
+  .menu-item {
+    padding: 8px 16px;
+    cursor: pointer;
+    font-size: 14px;
+    color: #606266;
+    transition:
+      background-color 0.3s,
+      color 0.3s;
+
+    &:hover {
+      background-color: #f5f7fa;
+      color: var(--el-color-primary);
+    }
+  }
+}
+</style>

+ 0 - 182
src/views/document/folder/document/components/DocumentAuditLog.vue

@@ -1,182 +0,0 @@
-<template>
-    <div>
-        <el-dialog v-model="dialogVisible" :title="t('document.document.dialog.auditLog')" width="1000px"
-            append-to-body>
-            <!-- 搜索栏 -->
-            <el-form :model="queryParams" :inline="true" class="search-form mb-4">
-                <el-form-item :label="t('document.document.auditLog.result')">
-                    <el-select v-model="queryParams.result" :placeholder="t('document.document.auditLog.selectResult')"
-                        clearable>
-                        <el-option :label="t('document.document.auditLog.pass')" value="3" />
-                        <el-option :label="t('document.document.auditLog.reject')" value="2" />
-                    </el-select>
-                </el-form-item>
-                <el-form-item :label="t('document.document.auditLog.auditTime')">
-                    <el-date-picker v-model="auditTimeRange" type="daterange" range-separator="-"
-                        :start-placeholder="t('document.document.auditLog.startTime')"
-                        :end-placeholder="t('document.document.auditLog.endTime')" value-format="YYYY-MM-DD HH:mm:ss" />
-                </el-form-item>
-                <el-form-item>
-                    <el-button type="primary" icon="Search" @click="handleQuery">{{ 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="loading" :data="auditLogList" border>
-                <el-table-column prop="id" label="ID" width="80" align="center" />
-                <el-table-column prop="documentName" :label="t('document.document.auditLog.documentName')"
-                    min-width="150" show-overflow-tooltip />
-                <el-table-column prop="auditorType" :label="t('document.document.auditLog.auditorType')" width="120"
-                    align="center">
-                    <template #default="scope">
-                        <dict-tag v-if="scope.row.auditorType" :options="auditor_type" :value="scope.row.auditorType" />
-                        <span v-else>-</span>
-                    </template>
-                </el-table-column>
-                <el-table-column prop="auditorName" :label="t('document.document.auditLog.auditorName')" width="120"
-                    align="center" />
-                <el-table-column prop="result" :label="t('document.document.auditLog.result')" width="100"
-                    align="center">
-                    <template #default="scope">
-                        <el-tag v-if="scope.row.result === 3" size="small" type="success">
-                            {{ t('document.document.auditLog.pass') }}
-                        </el-tag>
-                        <el-tag v-else-if="scope.row.result === 2" size="small" type="danger">
-                            {{ t('document.document.auditLog.reject') }}
-                        </el-tag>
-                        <span v-else>-</span>
-                    </template>
-                </el-table-column>
-                <el-table-column prop="rejectReason" :label="t('document.document.auditLog.rejectReason')"
-                    min-width="180" show-overflow-tooltip />
-                <el-table-column prop="auditTime" :label="t('document.document.auditLog.auditTime')" width="180"
-                    align="center">
-                    <template #default="scope">
-                        <span>{{ parseTime(scope.row.auditTime) }}</span>
-                    </template>
-                </el-table-column>
-                <!-- 操作栏 -->
-                <el-table-column label="操作" width="120" align="center" fixed="right">
-                    <template #default="scope">
-                        <el-button v-if="scope.row.ossUrl" type="primary" link icon="Download"
-                            @click="handleDownload(scope.row.ossUrl, scope.row.documentName)"
-                            :title="t('document.document.button.download')" />
-                        <span v-else>-</span>
-                    </template>
-                </el-table-column>
-            </el-table>
-
-            <!-- 分页 -->
-            <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize"
-                :total="total" @pagination="getList" />
-        </el-dialog>
-    </div>
-</template>
-
-<script setup lang="ts">
-import { ref, computed, watch, onMounted, getCurrentInstance, toRefs } from 'vue';
-import { useI18n } from 'vue-i18n';
-import { listDocumentAuditLog } from '@/api/document/document';
-import { parseTime } from '@/utils/ruoyi';
-import type { ComponentInternalInstance } from 'vue';
-
-const props = defineProps({
-    documentId: {
-        type: [Number, String],
-        required: true
-    },
-    visible: {
-        type: Boolean,
-        default: false
-    }
-});
-
-const emit = defineEmits(['update:visible']);
-
-const { t } = useI18n();
-const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-
-// 获取审核人类型字典
-const { auditor_type } = toRefs<any>(proxy?.useDict('auditor_type'));
-
-const dialogVisible = computed({
-    get: () => props.visible,
-    set: (val) => emit('update:visible', val)
-});
-
-const loading = ref(false);
-const total = ref(0);
-const auditLogList = ref<any[]>([]);
-const auditTimeRange = ref<[string, string]>(['', '']);
-
-const queryParams = ref({
-    pageNum: 1,
-    pageSize: 10,
-    documentId: props.documentId,
-    result: undefined,
-    params: {}
-});
-
-/** 查询审核记录列表 */
-const getList = async () => {
-    loading.value = true;
-    // 更新documentId
-    queryParams.value.documentId = props.documentId;
-    // 处理时间范围
-    queryParams.value.params = {};
-    proxy?.addDateRange(queryParams.value, auditTimeRange.value, 'AuditTime', 'earliestTime', 'latestTime');
-
-    const res = await listDocumentAuditLog(queryParams.value);
-    auditLogList.value = res.rows;
-    total.value = res.total;
-    loading.value = false;
-};
-
-/** 搜索按钮操作 */
-const handleQuery = () => {
-    queryParams.value.pageNum = 1;
-    getList();
-};
-
-/** 重置按钮操作 */
-const handleReset = () => {
-    queryParams.value.result = undefined;
-    auditTimeRange.value = ['', ''];
-    handleQuery();
-};
-
-/** 监听文档ID变化,重新查询 */
-watch(() => props.documentId, (newVal) => {
-    if (newVal && dialogVisible.value) {
-        getList();
-    }
-});
-
-/** 监听可见性变化,打开时重新查询 */
-watch(() => props.visible, (newVal) => {
-    if (newVal) {
-        getList();
-    }
-});
-
-/** 下载历史文件 */
-const handleDownload = (ossUrl: string, fileName: string) => {
-    if (!ossUrl) return;
-
-    // 创建临时a标签下载文件
-    const link = document.createElement('a');
-    link.href = ossUrl;
-    link.download = fileName;
-    document.body.appendChild(link);
-    link.click();
-    document.body.removeChild(link);
-};
-</script>
-
-<style scoped>
-.search-form {
-    margin-bottom: 16px;
-}
-</style>

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

@@ -0,0 +1,382 @@
+<template>
+  <div>
+    <el-card shadow="never">
+      <template #header>
+        <div class="flex justify-between items-center">
+          <span class="text-lg font-bold">{{ t('document.document.header.title') }}</span>
+          <el-button type="primary" @click="handleBack">{{ t('document.document.header.backToList') }}</el-button>
+        </div>
+      </template>
+
+      <div class="content-wrapper">
+        <!-- 文件夹树组件 -->
+        <FolderTree
+          ref="folderTreeRef"
+          :project-id="projectId"
+          @folder-click="handleFolderClick"
+          @add-document="handleAddDocument"
+          @refresh="handleTreeRefresh"
+        />
+
+        <!-- 文档列表组件 -->
+        <div class="content-container">
+          <DocumentList ref="documentListRef" :selected-folder="selectedFolder" :tree-data="treeData" />
+        </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"
+        >
+          <el-input v-model="documentForm.name" :placeholder="t('document.document.documentForm.namePlaceholder')" clearable />
+        </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.submitter')" prop="submitterId">
+          <el-select
+            v-model="documentForm.submitterId"
+            filterable
+            remote
+            reserve-keyword
+            :placeholder="t('document.document.documentForm.submitterPlaceholder')"
+            :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 :label="t('document.document.documentForm.file')" :prop="documentForm.type === 0 ? 'ossId' : ''">
+          <fileUpload v-model="uploadedFileId" :limit="1" />
+        </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.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.submit')
+          }}</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 } from 'vue';
+import { useI18n } from 'vue-i18n';
+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';
+
+interface Props {
+  projectId?: number | string;
+}
+
+const props = defineProps<Props>();
+
+const emit = defineEmits<{
+  back: [];
+}>();
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { t } = useI18n();
+const userStore = useUserStore();
+const { plan_document_type } = toRefs<any>(proxy?.useDict('plan_document_type'));
+
+// 组件引用
+const folderTreeRef = ref<InstanceType<typeof FolderTree>>();
+const documentListRef = ref<InstanceType<typeof DocumentList>>();
+const documentFormRef = ref<FormInstance>();
+
+// 当前选中的文件夹
+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,
+  submitterId: undefined,
+  folderId: undefined,
+  submitDeadline: undefined,
+  planType: undefined,
+  ossId: undefined,
+  submitTime: undefined,
+  projectId: props.projectId,
+  note: ''
+};
+
+// 文件上传的ossId
+const uploadedFileId = ref<string>('');
+
+// 文档表单数据
+const documentForm = ref<DocumentForm>({ ...initDocumentFormData });
+
+// 递交人搜索相关
+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' }],
+  submitterId: [{ required: true, message: t('document.document.documentRule.submitterRequired'), trigger: 'change' }],
+  ossId: [{ required: true, message: t('document.document.documentRule.fileRequired'), trigger: 'change' }]
+};
+
+// 返回项目列表
+const handleBack = () => {
+  emit('back');
+};
+
+// 处理文件夹点击
+const handleFolderClick = (folder: FolderListVO) => {
+  // 只有文件夹类型(type=0)才显示文档列表
+  if (folder.type === 0) {
+    selectedFolder.value = folder;
+  } else {
+    selectedFolder.value = null;
+  }
+};
+
+// 处理添加文档
+const handleAddDocument = (folder: FolderListVO) => {
+  resetDocumentForm();
+  documentForm.value.folderId = folder.id;
+  documentDialog.visible = true;
+  documentDialog.title = t('document.document.dialog.addDocument');
+};
+
+// 处理树刷新
+const handleTreeRefresh = () => {
+  // 刷新树数据以传递给DocumentList
+  if (folderTreeRef.value) {
+    // 可以通过ref访问子组件的数据
+    treeData.value = (folderTreeRef.value as any).treeData || [];
+  }
+};
+
+// 重置文档表单
+const resetDocumentForm = () => {
+  documentForm.value = { ...initDocumentFormData };
+  uploadedFileId.value = '';
+  if (!hasAddPlanPermission.value) {
+    documentForm.value.type = 0;
+  }
+  documentForm.value.submitterId = userStore.userId;
+  documentForm.value.projectId = props.projectId;
+  submitterOptions.value = [];
+  documentFormRef.value?.resetFields();
+};
+
+// 处理文档类型变化
+const handleDocumentTypeChange = (value: number) => {
+  if (value === 0) {
+    documentForm.value.submitterId = userStore.userId;
+    documentForm.value.submitDeadline = undefined;
+    documentForm.value.planType = undefined;
+  } else {
+    documentForm.value.submitterId = undefined;
+  }
+};
+
+// 搜索递交人
+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: props.projectId || 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.ossId = parseInt(ids[0]);
+      const now = new Date();
+      documentForm.value.submitTime = proxy?.parseTime(now, '{y}-{m}-{d} {h}:{i}:{s}');
+
+      if (documentForm.value.type === 0) {
+        documentForm.value.submitterId = userStore.userId;
+      }
+    }
+  } else {
+    documentForm.value.ossId = undefined;
+    documentForm.value.submitTime = undefined;
+    documentForm.value.submitterId = 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,
+          submitterId: documentForm.value.submitterId || (documentForm.value.type === 0 ? userStore.userId : 0),
+          folderId: documentForm.value.folderId || 0,
+          submitDeadline: documentForm.value.submitDeadline || '',
+          planType: documentForm.value.planType || '',
+          ossId: hasUploadedFile ? uploadedFileId.value : null,
+          submitTime: documentForm.value.submitTime || (hasUploadedFile ? new Date().toISOString() : ''),
+          projectId: documentForm.value.projectId || props.projectId,
+          status: hasUploadedFile ? 1 : 0,
+          note: documentForm.value.note || ''
+        };
+
+        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;
+      }
+    }
+  });
+};
+</script>
+
+<style scoped lang="scss">
+.flex {
+  display: flex;
+}
+
+.justify-between {
+  justify-content: space-between;
+}
+
+.items-center {
+  align-items: center;
+}
+
+.text-lg {
+  font-size: 1.125rem;
+}
+
+.font-bold {
+  font-weight: 700;
+}
+
+.content-wrapper {
+  display: flex;
+  height: calc(100vh - 250px);
+  min-height: 500px;
+}
+
+.content-container {
+  flex: 1;
+  padding: 20px;
+  overflow: auto;
+}
+</style>

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

@@ -128,7 +128,7 @@ import { listProject } from '@/api/document/folder';
 import { ProjectVO, ProjectQuery } from '@/api/document/folder/types';
 import { useI18n } from 'vue-i18n';
 import { parseI18nName } from '@/utils/i18n';
-import DocumentView from './document.vue';
+import DocumentView from './document/index.vue';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { t } = useI18n();

+ 13 - 22
src/views/home/taskCenter/audit/index.vue

@@ -51,15 +51,7 @@
         </el-table-column>
         <el-table-column prop="status" label="状态" width="120" align="center">
           <template #default="scope">
-            <el-tag v-if="scope.row.status === 0" size="small" type="info">未上传</el-tag>
-            <el-tag v-else-if="scope.row.status === 1" size="small" type="warning">待审核</el-tag>
-            <el-tag v-else-if="scope.row.status === 2" size="small" type="danger">审核拒绝</el-tag>
-            <el-tag v-else-if="scope.row.status === 3" size="small" type="warning">待归档</el-tag>
-            <el-tag v-else-if="scope.row.status === 4" size="small" type="success">已归档</el-tag>
-            <el-tag v-else-if="scope.row.status === 5" size="small" type="warning">待质控</el-tag>
-            <el-tag v-else-if="scope.row.status === 6" size="small" type="success">质控通过</el-tag>
-            <el-tag v-else-if="scope.row.status === 7" size="small" type="danger">质控拒绝</el-tag>
-            <el-tag v-else size="small" type="default">-</el-tag>
+            <DocumentStatusTag :status="scope.row.status" />
           </template>
         </el-table-column>
         <el-table-column prop="submitter" label="递交人" width="120" align="center" />
@@ -86,10 +78,10 @@
           <template #default="scope">
             <el-button v-if="scope.row.status === 1" type="primary" link @click="handleAudit(scope.row)" title="审核">
               <el-icon>
-                <Edit />
+                <Check />
               </el-icon>
             </el-button>
-            <el-button v-if="scope.row.ossUrl" type="primary" link @click="downloadFile(scope.row.ossUrl)" title="下载">
+            <el-button v-if="scope.row.ossId" type="primary" link @click="handleDownload(scope.row)" title="下载">
               <el-icon>
                 <Download />
               </el-icon>
@@ -132,8 +124,10 @@ import { ElMessage, ElMessageBox } from 'element-plus';
 import type { FormInstance } from 'element-plus';
 import { listAuditTasks, auditDocument } from '@/api/home/taskCenter/audit';
 import { AuditTaskVO, AuditTaskAuditForm } from '@/api/home/taskCenter/audit/types';
-import { Edit, Download } from '@element-plus/icons-vue';
+import { downloadDocumentFile } from '@/api/document/document';
+import { Edit, Download, Check } from '@element-plus/icons-vue';
 import DictTag from '@/components/DictTag/index.vue';
+import DocumentStatusTag from '@/components/DocumentStatusTag/index.vue';
 
 const { proxy } = getCurrentInstance() as any;
 const { plan_document_type } = proxy.useDict('plan_document_type');
@@ -307,17 +301,14 @@ const submitAuditForm = () => {
 
 /**
  * 下载文件
- * @param url 文件URL
  */
-const downloadFile = (url: string) => {
-  // 创建一个隐藏的<a>标签
-  const link = document.createElement('a');
-  link.href = url;
-  link.target = '_blank';
-  link.download = ''; // 浏览器会根据URL自动设置文件名
-  document.body.appendChild(link);
-  link.click();
-  document.body.removeChild(link);
+const handleDownload = async (row: AuditTaskVO) => {
+  if (!row.ossId) {
+    ElMessage.warning('暂无文件可下载');
+    return;
+  }
+
+  await downloadDocumentFile(row.ossId, row.name);
 };
 
 onMounted(() => {

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

@@ -62,10 +62,7 @@
         </el-table-column>
         <el-table-column prop="status" label="状态" width="120" align="center">
           <template #default="scope">
-            <el-tag v-if="scope.row.status === 0" size="small" type="info">未递交</el-tag>
-            <el-tag v-else-if="scope.row.status === 1" size="small" type="warning">待审核</el-tag>
-            <el-tag v-else-if="scope.row.status === 2" size="small" type="danger">审核拒绝</el-tag>
-            <el-tag v-else size="small" type="default">未知</el-tag>
+            <DocumentStatusTag :status="scope.row.status" />
           </template>
         </el-table-column>
         <el-table-column prop="submitter" label="递交人" width="120" align="center" />
@@ -93,7 +90,7 @@
           </template>
         </el-table-column>
         <!-- 操作列 -->
-        <el-table-column label="操作" width="120" align="center" fixed="right">
+        <el-table-column label="操作" width="150" align="center" fixed="right">
           <template #default="scope">
             <el-button v-if="scope.row.status === 0 || scope.row.status === 2" type="primary" link
               @click="handleTaskSubmit(scope.row)" title="递交">
@@ -101,6 +98,12 @@
                 <Upload />
               </el-icon>
             </el-button>
+            <el-button v-hasPermi="['taskCenter:submission:logAudit']" type="primary" link
+              @click="handleViewAuditLog(scope.row)" title="查看审核记录">
+              <el-icon>
+                <DocumentCopy />
+              </el-icon>
+            </el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -124,6 +127,14 @@
         </div>
       </template>
     </el-dialog>
+
+    <!-- 审核记录对话框 -->
+    <AuditLogDialog 
+      v-model:visible="auditLogDialog.visible" 
+      :document-id="auditLogDialog.documentId"
+      :api-function="listSubmissionAuditLog"
+      i18n-prefix="home.taskCenter.submission"
+    />
   </div>
 </template>
 
@@ -131,10 +142,12 @@
 import { ref, reactive, onMounted, nextTick, getCurrentInstance } from 'vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
 import type { FormInstance } from 'element-plus';
-import { listSubmissionTasks, submitDocument } from '@/api/home/taskCenter/submission';
+import { listSubmissionTasks, submitDocument, listSubmissionAuditLog } from '@/api/home/taskCenter/submission';
 import fileUpload from '@/components/FileUpload/index.vue';
 import DictTag from '@/components/DictTag/index.vue';
-import { Upload } from '@element-plus/icons-vue';
+import DocumentStatusTag from '@/components/DocumentStatusTag/index.vue';
+import AuditLogDialog from '@/components/AuditLogDialog/index.vue';
+import { Upload, DocumentCopy } from '@element-plus/icons-vue';
 import { SubmissionTaskVO, SubmissionTaskSubmitForm } from '@/api/home/taskCenter/submission/types';
 
 const { proxy } = getCurrentInstance() as any;
@@ -180,6 +193,12 @@ const submitRules = {
   ossId: [{ required: true, message: '请上传文件', trigger: 'change' }]
 };
 
+// 审核记录对话框
+const auditLogDialog = reactive({
+  visible: false,
+  documentId: 0
+});
+
 /**
  * 解析时间格式
  * @param time 时间字符串
@@ -322,6 +341,14 @@ const handleTaskSubmit = (row: SubmissionTaskVO) => {
   });
 };
 
+/**
+ * 查看审核记录
+ */
+const handleViewAuditLog = (row: SubmissionTaskVO) => {
+  auditLogDialog.documentId = row.id;
+  auditLogDialog.visible = true;
+};
+
 /**
  * 取消递交
  */

+ 342 - 0
src/views/search/index.vue

@@ -0,0 +1,342 @@
+<template>
+  <div class="search-container">
+    <el-card shadow="never">
+      <template #header>
+        <div class="flex justify-between items-center">
+          <span class="text-lg font-bold">{{ t('search.title') }}</span>
+        </div>
+      </template>
+
+      <!-- 搜索栏 -->
+      <el-form :model="queryParams" :inline="true" class="search-form">
+        <el-form-item :label="t('search.search.name')">
+          <el-input v-model="queryParams.name" :placeholder="t('search.search.namePlaceholder')" clearable
+            style="width: 200px" @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item :label="t('search.search.projectCode')">
+          <el-input v-model="queryParams.projectCode" :placeholder="t('search.search.projectCodePlaceholder')" clearable
+            style="width: 200px" @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item :label="t('search.search.projectName')">
+          <el-input v-model="queryParams.projectName" :placeholder="t('search.search.projectNamePlaceholder')" clearable
+            style="width: 200px" @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item :label="t('search.search.type')">
+          <el-select v-model="queryParams.type" :placeholder="t('search.search.typePlaceholder')" clearable
+            style="width: 150px">
+            <el-option :label="t('search.type.normalDocument')" :value="0" />
+            <el-option :label="t('search.type.planDocument')" :value="1" />
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="t('search.search.status')">
+          <el-select v-model="queryParams.status" :placeholder="t('search.search.statusPlaceholder')" clearable
+            style="width: 150px">
+            <el-option :label="t('search.status.unUpload')" :value="0" />
+            <el-option :label="t('search.status.unAudit')" :value="1" />
+            <el-option :label="t('search.status.auditReject')" :value="2" />
+            <el-option :label="t('search.status.unFiling')" :value="3" />
+            <el-option :label="t('search.status.filing')" :value="4" />
+            <el-option :label="t('search.status.unQualityControl')" :value="5" />
+            <el-option :label="t('search.status.qualityControlPass')" :value="6" />
+            <el-option :label="t('search.status.qualityControlReject')" :value="7" />
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="t('search.search.createTime')">
+          <el-date-picker v-model="createTimeRange" type="daterange" range-separator="-"
+            :start-placeholder="t('search.search.startTime')" :end-placeholder="t('search.search.endTime')"
+            value-format="YYYY-MM-DD HH:mm:ss" style="width: 240px" />
+        </el-form-item>
+        <el-form-item :label="t('search.search.updateTime')">
+          <el-date-picker v-model="updateTimeRange" type="daterange" range-separator="-"
+            :start-placeholder="t('search.search.startTime')" :end-placeholder="t('search.search.endTime')"
+            value-format="YYYY-MM-DD HH:mm:ss" style="width: 240px" />
+        </el-form-item>
+        <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-form-item>
+      </el-form>
+
+      <!-- 文档列表 -->
+      <el-table v-loading="loading" :data="documentList" border style="margin-top: 10px">
+        <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="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>
+            <span v-else-if="scope.row.type === 1">{{ t('search.type.planDocument') }}</span>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="specificationLabel" :label="t('search.table.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="planDocumentTypeLabel" :label="t('search.table.planDocumentType')" min-width="120"
+          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>
+          </template>
+        </el-table-column>
+        <el-table-column prop="status" :label="t('search.table.status')" width="120" align="center">
+          <template #default="scope">
+            <DocumentStatusTag :status="scope.row.status" />
+          </template>
+        </el-table-column>
+        <el-table-column prop="submitter" :label="t('search.table.submitter')" width="120" align="center" />
+        <el-table-column prop="submitDeadline" :label="t('search.table.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('search.table.submitTime')" width="160" align="center">
+          <template #default="scope">
+            <span v-if="scope.row.submitTime">{{ parseTime(scope.row.submitTime) }}</span>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="fileName" :label="t('search.table.fileName')" min-width="200" show-overflow-tooltip>
+          <template #default="scope">
+            <div v-if="scope.row.fileName" class="file-name-cell">
+              <svg-icon :icon-class="getFileIconClass(scope.row.fileName)" class="file-icon" :size="18" />
+              <span class="file-name-text">{{ scope.row.fileName }}</span>
+            </div>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="note" :label="t('search.table.note')" min-width="150" show-overflow-tooltip />
+        <el-table-column prop="createTime" :label="t('search.table.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('search.table.updateTime')" width="160" align="center">
+          <template #default="scope">
+            <span v-if="scope.row.updateTime">{{ parseTime(scope.row.updateTime) }}</span>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <!-- 操作列 -->
+        <el-table-column :label="t('search.table.action')" width="100" align="center" fixed="right">
+          <template #default="scope">
+            <el-button v-if="scope.row.ossId" type="primary" link icon="Download"
+              @click="handleDownload(scope.row.ossId, scope.row.fileName)" :title="t('search.button.download')" />
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize"
+        :total="total" @pagination="getList" />
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted, getCurrentInstance, toRefs } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { ElMessage } from 'element-plus';
+import { listSearchDocuments } 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';
+
+const { t } = useI18n();
+const { proxy } = getCurrentInstance() as any;
+
+// 获取字典
+const { plan_document_type, center_file_specification, project_file_specification } = toRefs<any>(
+  proxy?.useDict('plan_document_type', 'center_file_specification', 'project_file_specification')
+);
+
+const loading = ref(false);
+
+// 查询参数
+const queryParams = reactive({
+  name: '',
+  projectCode: '',
+  projectName: '',
+  type: undefined as number | undefined,
+  status: undefined as number | undefined,
+  params: {},
+  pageNum: 1,
+  pageSize: 10
+});
+
+// 时间范围
+const createTimeRange = ref<[string, string]>(['', '']);
+const updateTimeRange = ref<[string, string]>(['', '']);
+
+// 文档列表数据
+const documentList = ref<any[]>([]);
+const total = ref(0);
+
+/**
+ * 根据 specificationType 获取对应的字典
+ * @param specificationType 规格类型 (0: 中心文件, 1: 项目文件)
+ */
+const getSpecificationDict = (specificationType: number | undefined) => {
+  if (specificationType === 0) {
+    return center_file_specification?.value || [];
+  } else if (specificationType === 1) {
+    return project_file_specification?.value || [];
+  }
+  // 默认返回空数组
+  return [];
+};
+
+/**
+ * 文件类型判断函数
+ */
+const isWordFile = (fileName: string): boolean => {
+  if (!fileName) return false;
+  const lowerFileName = fileName.toLowerCase();
+  return lowerFileName.endsWith('.doc') || lowerFileName.endsWith('.docx');
+};
+
+const isExcelFile = (fileName: string): boolean => {
+  if (!fileName) return false;
+  const lowerFileName = fileName.toLowerCase();
+  return lowerFileName.endsWith('.xls') || lowerFileName.endsWith('.xlsx') || lowerFileName.endsWith('.csv');
+};
+
+const isPPTFile = (fileName: string): boolean => {
+  if (!fileName) return false;
+  const lowerFileName = fileName.toLowerCase();
+  return lowerFileName.endsWith('.ppt') || lowerFileName.endsWith('.pptx');
+};
+
+const isPDFFile = (fileName: string): boolean => {
+  if (!fileName) return false;
+  return fileName.toLowerCase().endsWith('.pdf');
+};
+
+/**
+ * 获取文件图标类型
+ */
+const getFileIconClass = (fileName: string): string => {
+  if (isWordFile(fileName)) {
+    return 'document-word';
+  } else if (isExcelFile(fileName)) {
+    return 'document-excel';
+  } else if (isPPTFile(fileName)) {
+    return 'document-ppt';
+  } else if (isPDFFile(fileName)) {
+    return 'document-pdf';
+  } else {
+    return 'document-document';
+  }
+};
+
+/**
+ * 获取文档列表
+ */
+const getList = async () => {
+  try {
+    loading.value = true;
+    // 处理时间范围
+    queryParams.params = {};
+    proxy?.addDateRange(queryParams, createTimeRange.value, 'CreateTime', 'createTimeEarliest', 'createTimeLatest');
+    proxy?.addDateRange(queryParams, updateTimeRange.value, 'UpdateTime', 'updateTimeEarliest', 'updateTimeLatest');
+
+    const res = await listSearchDocuments(queryParams);
+    if (res.code === 200) {
+      documentList.value = res.rows || [];
+      total.value = res.total || 0;
+    } else {
+      ElMessage.error(res.msg || t('search.message.fetchFailed'));
+    }
+  } catch (error) {
+    console.error(t('search.message.fetchFailed'), error);
+    ElMessage.error(t('search.message.fetchFailed'));
+  } finally {
+    loading.value = false;
+  }
+};
+
+/**
+ * 处理搜索
+ */
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+/**
+ * 重置搜索
+ */
+const resetQuery = () => {
+  queryParams.name = '';
+  queryParams.projectCode = '';
+  queryParams.projectName = '';
+  queryParams.type = undefined;
+  queryParams.status = undefined;
+  createTimeRange.value = ['', ''];
+  updateTimeRange.value = ['', ''];
+  handleQuery();
+};
+
+/**
+ * 下载文件
+ */
+const handleDownload = async (ossId: number, fileName: string) => {
+  if (!ossId) return;
+  await downloadDocumentFile(ossId, fileName);
+};
+
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style scoped lang="scss">
+.search-container {
+  padding: 20px;
+
+  .search-form {
+    margin-bottom: 15px;
+  }
+
+  :deep(.el-table) {
+    .cell {
+      white-space: nowrap;
+    }
+  }
+
+  .file-name-cell {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 0 8px;
+
+    .file-icon {
+      flex-shrink: 0;
+      width: 18px;
+      height: 18px;
+      vertical-align: middle;
+    }
+
+    .file-name-text {
+      flex: 1;
+      text-align: left;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+  }
+}
+</style>

+ 120 - 3
src/views/setting/ai/index.vue

@@ -1,5 +1,122 @@
-<script setup lang="ts"></script>
+<template>
+  <div class="p-4">
+    <el-card v-loading="loading" shadow="never">
+      <template #header>
+        <div class="card-header">
+          <span class="text-lg font-semibold">{{ t('ai.title') }}</span>
+        </div>
+      </template>
 
-<template></template>
+      <el-form v-if="aiData" ref="formRef" :model="aiData" :rules="rules" label-width="120px" class="setting-form">
+        <el-form-item :label="t('ai.form.enabledFlag')" prop="enabledFlag">
+          <el-switch
+            v-model="aiData.enabledFlag"
+            :active-value="1"
+            :inactive-value="0"
+            :active-text="t('ai.form.enabled')"
+            :inactive-text="t('ai.form.disabled')"
+          />
+        </el-form-item>
 
-<style scoped lang="scss"></style>
+        <el-form-item>
+          <el-button type="primary" :loading="saveLoading" @click="handleSave">{{ t('ai.button.save') }}</el-button>
+          <el-button @click="handleReset">{{ t('ai.button.reset') }}</el-button>
+        </el-form-item>
+      </el-form>
+
+      <el-empty v-else :description="t('ai.message.noData')" />
+    </el-card>
+  </div>
+</template>
+
+<script setup name="AiSetting" lang="ts">
+import { ref, onMounted, getCurrentInstance } from 'vue';
+import { useI18n } from 'vue-i18n';
+import type { FormInstance, FormRules } from 'element-plus';
+import { getAiSetting, updateAiSetting } from '@/api/setting/ai';
+import { AiSettingVO } from '@/api/setting/ai/types';
+import type { ComponentInternalInstance } from 'vue';
+
+const { t } = useI18n();
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const loading = ref(true);
+const saveLoading = ref(false);
+const formRef = ref<FormInstance>();
+const aiData = ref<AiSettingVO | null>(null);
+const originalData = ref<AiSettingVO | null>(null);
+
+// 表单验证规则
+const rules: FormRules = {
+  enabledFlag: [
+    { required: true, message: t('ai.rule.enabledFlagRequired'), trigger: 'change' }
+  ]
+};
+
+/**
+ * 获取AI设置数据
+ */
+const fetchData = async () => {
+  loading.value = true;
+  try {
+    const res = await getAiSetting();
+    if (res.code === 200) {
+      aiData.value = res.data;
+      // 保存原始数据用于重置
+      originalData.value = JSON.parse(JSON.stringify(res.data));
+    } else {
+      proxy?.$modal.msgError(res.msg || t('ai.message.fetchFailed'));
+    }
+  } catch (error) {
+    console.error(t('ai.message.fetchFailed'), error);
+    proxy?.$modal.msgError(t('ai.message.fetchFailed'));
+  } finally {
+    loading.value = false;
+  }
+};
+
+/**
+ * 保存数据
+ */
+const handleSave = async () => {
+  if (!formRef.value || !aiData.value) return;
+
+  await formRef.value.validate(async (valid: boolean) => {
+    if (valid) {
+      saveLoading.value = true;
+      try {
+        await updateAiSetting(aiData.value as AiSettingVO);
+        proxy?.$modal.msgSuccess(t('ai.message.saveSuccess'));
+        // 重新获取数据
+        await fetchData();
+      } catch (error) {
+        console.error(t('ai.message.saveFailed'), error);
+        proxy?.$modal.msgError(t('ai.message.saveFailed'));
+      } finally {
+        saveLoading.value = false;
+      }
+    }
+  });
+};
+
+/**
+ * 重置表单
+ */
+const handleReset = () => {
+  if (originalData.value) {
+    aiData.value = JSON.parse(JSON.stringify(originalData.value));
+  }
+  formRef.value?.clearValidate();
+};
+
+onMounted(() => {
+  fetchData();
+});
+</script>
+
+<style scoped lang="scss">
+.setting-form {
+  max-width: 600px;
+  margin-top: 20px;
+}
+</style>