Explorar o código

指控流程初步跑完

Huanyi hai 2 meses
pai
achega
607199aa36

+ 25 - 1
src/api/home/taskCenter/qc/index.ts

@@ -1,6 +1,6 @@
 import request from '@/utils/request';
 import { AxiosPromise } from 'axios';
-import { QcTaskQuery, QcTaskVO } from './types';
+import { QcTaskQuery, QcTaskVO, QcTaskSubmitForm, QcTaskAuditForm } from './types';
 
 /**
  * 查询质控任务列表
@@ -13,3 +13,27 @@ export const listQcTasks = (query: QcTaskQuery): AxiosPromise<QcTaskVO[]> => {
         params: query
     });
 };
+
+/**
+ * 递交质控任务
+ * @param data
+ */
+export const submitQcTask = (data: QcTaskSubmitForm): AxiosPromise<any> => {
+    return request({
+        url: '/home/taskCenter/qc/submit',
+        method: 'put',
+        data: data
+    });
+};
+
+/**
+ * 审核质控任务
+ * @param data
+ */
+export const auditQcTask = (data: QcTaskAuditForm): AxiosPromise<any> => {
+    return request({
+        url: '/home/taskCenter/qc/audit',
+        method: 'put',
+        data: data
+    });
+};

+ 34 - 0
src/api/home/taskCenter/qc/types.ts

@@ -18,6 +18,10 @@ export interface QcTaskQuery extends PageQuery {
 export interface QcTaskVO {
     /** 序号 */
     id: number;
+    /** 任务ID */
+    taskId?: number;
+    /** 项目ID */
+    projectId?: number;
     /** 任务名称 */
     name: string;
     /** 项目名称 */
@@ -35,3 +39,33 @@ export interface QcTaskVO {
     /** 创建时间 */
     createTime: string;
 }
+
+/**
+ * 质控任务递交表单
+ */
+export interface QcTaskSubmitForm {
+    /** 详情ID */
+    detailId: number;
+    /** 文档OSS ID */
+    document: number;
+}
+
+/**
+ * 质控任务审核表单
+ */
+export interface QcTaskAuditForm {
+    /** 任务ID */
+    taskId: number;
+    /** 详情ID */
+    detailId: number;
+    /** 审核结果:1-通过,2-驳回 */
+    result: number;
+    /** 驳回类型(字典:qc_question_type) */
+    rejectionType?: string;
+    /** 意见 */
+    opinion?: string;
+    /** 指定处理人ID */
+    designatedDealer?: number;
+    /** 截止日期 */
+    deadline?: string;
+}

+ 1 - 1
src/permission.ts

@@ -23,7 +23,7 @@ router.beforeEach(async (to, from, next) => {
     to.meta.title && useSettingsStore().setTitle(to.meta.title as string);
     /* has token*/
     if (to.path === '/login') {
-      next({ path: '/home/workbench' });
+      next({ path: '/home/dashboard' });
       NProgress.done();
     } else if (isWhiteList(to.path)) {
       next();

+ 1 - 1
src/router/index.ts

@@ -68,7 +68,7 @@ export const constantRoutes: RouteRecordRaw[] = [
   {
     path: '',
     component: Layout,
-    redirect: '/home/workbench',
+    redirect: '/home/dashboard',
     children: [
       {
         path: '/index',

+ 1 - 1
src/utils/request.ts

@@ -28,7 +28,7 @@ axios.defaults.headers['clientid'] = import.meta.env.VITE_APP_CLIENT_ID;
 // 创建 axios 实例
 const service = axios.create({
   baseURL: import.meta.env.VITE_APP_BASE_API,
-  timeout: 50000
+  timeout: 0
 });
 
 // 请求拦截器

+ 7 - 7
src/views/document/folder/document/DocumentList.vue

@@ -110,7 +110,7 @@
           </template>
           <template v-else>
             <el-button
-              v-if="scope.row.actualDocument && scope.row.status === 1 && scope.row.createBy === userStore.userId"
+              v-if="scope.row.actualDocument && scope.row.status === 1"
               v-hasPermi="['document:document:audit']"
               type="primary"
               :icon="Select"
@@ -130,7 +130,7 @@
               {{ t('document.document.button.submit') }}
             </el-button>
             <el-button
-              v-if="(scope.row.status === 0 || scope.row.status === 2) && scope.row.createBy === userStore.userId"
+              v-if="(scope.row.status === 0 || scope.row.status === 2)"
               v-hasPermi="['document:document:confirmSubmit']"
               type="danger"
               icon="Delete"
@@ -804,7 +804,7 @@ const handleSpecify = (row: DocumentVO) => {
 // 获取可指定的文档列表
 const getSpecifyDocumentList = async () => {
   if (!props.projectId) return;
-  
+
   specifyDocumentLoading.value = true;
   try {
     const res = await listDocumentOnSpecify(specifyQueryParams);
@@ -876,20 +876,20 @@ const submitSpecifyForm = () => {
         ElMessage.warning('请选择一个文档');
         return;
       }
-      
+
       // 如果是指定文件夹,需要选择一个文件夹
       if (specifyForm.value.type === 'folder' && !selectedFolderId.value) {
         ElMessage.warning('请选择一个文件夹');
         return;
       }
-      
+
       specifyButtonLoading.value = true;
       try {
         // 构建请求数据
         const specifyData: DocumentSpecifyForm = {
           documentId: currentDocument.value.id
         };
-        
+
         if (specifyForm.value.type === 'missing') {
           // 指定递交缺失
           specifyData.missingDocumentId = selectedDocumentId.value as number | string;
@@ -897,7 +897,7 @@ const submitSpecifyForm = () => {
           // 指定文件夹
           specifyData.folderId = selectedFolderId.value;
         }
-        
+
         await specifyDocument(specifyData);
         ElMessage.success('指定成功');
         specifyDialog.visible = false;

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

@@ -211,7 +211,7 @@ const getList = async () => {
       projectId: props.projectId as number,
       parentId: undefined,
       type: 0,
-      name: '临时文件夹',
+      name: '待归档区',
       status: 0,
       note: '',
       restrictionLevel: -1,

+ 167 - 12
src/views/home/workbench/index.vue → src/views/home/dashboard/index.vue

@@ -1,11 +1,34 @@
 <script setup lang="ts">
 import { ref, onMounted, onUnmounted, nextTick, watch, reactive } from 'vue';
-import { Search } from '@element-plus/icons-vue';
+import { Search, Upload, Check, FolderAdd, Document } from '@element-plus/icons-vue';
 import * as echarts from 'echarts';
 import type { EChartsOption } from 'echarts';
 import { ElMessage } from 'element-plus';
 import request from '@/utils/request';
 
+// 待办统计
+const todoCount = ref({
+  toSubmit: 0,
+  toAudit: 0,
+  toFiling: 0,
+  toQc: 0
+});
+
+// 获取待办统计
+const getTodoCount = async () => {
+  try {
+    const res = await request({
+      url: '/home/dashboard/countTodo',
+      method: 'get'
+    });
+    if (res.code === 200 && res.data) {
+      todoCount.value = res.data;
+    }
+  } catch (error) {
+    console.error('获取待办统计失败:', error);
+  }
+};
+
 // 项目列表相关
 const loading = ref(false);
 const projectList = ref<any[]>([]);
@@ -21,7 +44,7 @@ const getProjectList = async () => {
   try {
     loading.value = true;
     const res = await request({
-      url: '/home/workbench/list',
+      url: '/home/dashboard/list',
       method: 'get',
       params: queryParams
     });
@@ -112,6 +135,7 @@ const viewProjectDetail = (row: any) => {
 };
 
 onMounted(async () => {
+  await getTodoCount();
   await getProjectList();
 });
 
@@ -120,6 +144,64 @@ onUnmounted(() => {});
 
 <template>
   <div class="workbench-container">
+    <!-- 待办统计卡片 -->
+    <div class="todo-cards">
+      <el-row :gutter="20">
+        <el-col :span="6">
+          <el-card shadow="hover" class="todo-card todo-submit">
+            <div class="todo-content">
+              <div class="todo-icon">
+                <el-icon size="32"><Upload /></el-icon>
+              </div>
+              <div class="todo-info">
+                <div class="todo-count">{{ todoCount.toSubmit }}</div>
+                <div class="todo-label">待递交</div>
+              </div>
+            </div>
+          </el-card>
+        </el-col>
+        <el-col :span="6">
+          <el-card shadow="hover" class="todo-card todo-audit">
+            <div class="todo-content">
+              <div class="todo-icon">
+                <el-icon size="32"><Check /></el-icon>
+              </div>
+              <div class="todo-info">
+                <div class="todo-count">{{ todoCount.toAudit }}</div>
+                <div class="todo-label">待审核</div>
+              </div>
+            </div>
+          </el-card>
+        </el-col>
+        <el-col :span="6">
+          <el-card shadow="hover" class="todo-card todo-filing">
+            <div class="todo-content">
+              <div class="todo-icon">
+                <el-icon size="32"><FolderAdd /></el-icon>
+              </div>
+              <div class="todo-info">
+                <div class="todo-count">{{ todoCount.toFiling }}</div>
+                <div class="todo-label">待归档</div>
+              </div>
+            </div>
+          </el-card>
+        </el-col>
+        <el-col :span="6">
+          <el-card shadow="hover" class="todo-card todo-qc">
+            <div class="todo-content">
+              <div class="todo-icon">
+                <el-icon size="32"><Document /></el-icon>
+              </div>
+              <div class="todo-info">
+                <div class="todo-count">{{ todoCount.toQc }}</div>
+                <div class="todo-label">待质控</div>
+              </div>
+            </div>
+          </el-card>
+        </el-col>
+      </el-row>
+    </div>
+
     <!-- 项目列表 -->
     <div class="project-list">
       <el-card shadow="hover">
@@ -200,16 +282,6 @@ onUnmounted(() => {});
               {{ formatTime(row.endTime, true) }}
             </template>
           </el-table-column>
-
-          <el-table-column label="操作" width="100" fixed="right" align="center">
-            <template #default="{ row }">
-              <el-button link type="primary" size="small" @click="viewProjectDetail(row)">
-                <el-icon>
-                  <Search />
-                </el-icon>
-              </el-button>
-            </template>
-          </el-table-column>
         </el-table>
 
         <!-- 分页 -->
@@ -233,6 +305,89 @@ onUnmounted(() => {});
 .workbench-container {
   padding: 20px;
 
+  .todo-cards {
+    margin-bottom: 20px;
+
+    .todo-card {
+      cursor: pointer;
+      transition: all 0.3s;
+
+      &:hover {
+        transform: translateY(-5px);
+      }
+
+      .todo-content {
+        display: flex;
+        align-items: center;
+        padding: 10px 0;
+
+        .todo-icon {
+          width: 60px;
+          height: 60px;
+          border-radius: 50%;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          margin-right: 15px;
+        }
+
+        .todo-info {
+          .todo-count {
+            font-size: 28px;
+            font-weight: 600;
+            line-height: 1.2;
+          }
+
+          .todo-label {
+            font-size: 14px;
+            color: #909399;
+            margin-top: 5px;
+          }
+        }
+      }
+
+      &.todo-submit {
+        .todo-icon {
+          background-color: rgba(64, 158, 255, 0.1);
+          color: #409eff;
+        }
+        .todo-count {
+          color: #409eff;
+        }
+      }
+
+      &.todo-audit {
+        .todo-icon {
+          background-color: rgba(230, 162, 60, 0.1);
+          color: #e6a23c;
+        }
+        .todo-count {
+          color: #e6a23c;
+        }
+      }
+
+      &.todo-filing {
+        .todo-icon {
+          background-color: rgba(103, 194, 58, 0.1);
+          color: #67c23a;
+        }
+        .todo-count {
+          color: #67c23a;
+        }
+      }
+
+      &.todo-qc {
+        .todo-icon {
+          background-color: rgba(144, 147, 153, 0.1);
+          color: #909399;
+        }
+        .todo-count {
+          color: #909399;
+        }
+      }
+    }
+  }
+
   .project-list {
     margin-top: 20px;
   }

+ 335 - 3
src/views/home/taskCenter/qc/index.vue

@@ -62,22 +62,146 @@
             <span v-else>-</span>
           </template>
         </el-table-column>
+        <!-- 操作列 -->
+        <el-table-column label="操作" width="180" align="center" fixed="right" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-button
+              v-if="scope.row.status === 2"
+              v-hasPermi="['taskCenter:qc:submit']"
+              type="primary"
+              icon="Upload"
+              style="padding: 0 5px; font-size: 10px; height: 24px"
+              @click="handleSubmit(scope.row)"
+            >
+              递交
+            </el-button>
+            <el-button
+              v-if="scope.row.status === 0 || scope.row.status === 3"
+              v-hasPermi="['taskCenter:qc:audit']"
+              type="success"
+              icon="Check"
+              style="padding: 0 5px; font-size: 10px; height: 24px"
+              @click="handleAudit(scope.row)"
+            >
+              审核
+            </el-button>
+          </template>
+        </el-table-column>
       </el-table>
 
       <!-- 分页 -->
       <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize"
         :total="total" @pagination="getTaskList" />
     </el-card>
+
+    <!-- 递交文档对话框 -->
+    <el-dialog v-model="submitDialog.visible" :title="submitDialog.title" width="500px" append-to-body>
+      <el-alert title="仅支持上传 PDF 格式文件" type="info" :closable="false" style="margin-bottom: 20px" />
+      <el-form ref="submitFormRef" :model="submitForm" :rules="submitRules" label-width="100px">
+        <el-form-item label="文件" prop="document">
+          <fileUpload v-model="submitForm.document" :limit="1" :file-type="['pdf']" :is-show-tip="false" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="cancelSubmit">取消</el-button>
+          <el-button type="primary" :loading="submitButtonLoading" @click="submitSubmitForm">确认</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 审核弹窗 -->
+    <el-dialog v-model="auditDialog.visible" title="审核" width="500px" append-to-body>
+      <el-form ref="auditFormRef" :model="auditForm" :rules="auditRules" label-width="100px">
+        <el-form-item label="结果" prop="result">
+          <el-radio-group v-model="auditForm.result">
+            <el-radio :label="1">通过</el-radio>
+            <el-radio :label="2">驳回</el-radio>
+          </el-radio-group>
+        </el-form-item>
+
+        <template v-if="auditForm.result === 2">
+          <el-form-item label="问题类型" prop="rejectionType">
+            <el-select v-model="auditForm.rejectionType" placeholder="请选择问题类型" style="width: 100%">
+              <el-option
+                v-for="dict in qc_question_type"
+                :key="dict.value"
+                :label="parseI18nName(dict.label)"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+
+          <el-form-item label="意见" prop="opinion">
+            <el-input
+              v-model="auditForm.opinion"
+              type="textarea"
+              :rows="3"
+              placeholder="请输入意见"
+            />
+          </el-form-item>
+
+          <el-form-item label="指定处理人" prop="designatedDealer">
+            <el-select
+              v-model="auditForm.designatedDealer"
+              filterable
+              remote
+              reserve-keyword
+              placeholder="请输入成员昵称搜索"
+              :remote-method="searchDealers"
+              :loading="dealerSearchLoading"
+              style="width: 100%"
+            >
+              <el-option
+                v-for="dealer in dealerOptions"
+                :key="dealer.id"
+                :label="`${dealer.name} / ${dealer.dept} --- ${dealer.phoneNumber}`"
+                :value="dealer.id"
+              />
+            </el-select>
+          </el-form-item>
+
+          <el-form-item label="截止日期" prop="deadline">
+            <el-date-picker
+              v-model="auditForm.deadline"
+              type="date"
+              value-format="YYYY-MM-DD"
+              placeholder="请选择截止日期"
+              style="width: 100%"
+            />
+          </el-form-item>
+        </template>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="cancelAudit">取消</el-button>
+          <el-button type="primary" :loading="auditLoading" @click="submitAudit">确认</el-button>
+        </div>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted } from 'vue';
+import { ref, reactive, onMounted, nextTick, computed, toRefs, getCurrentInstance } from 'vue';
 import { ElMessage } from 'element-plus';
-import { listQcTasks } from '@/api/home/taskCenter/qc';
-import { QcTaskVO, QcTaskQuery } from '@/api/home/taskCenter/qc/types';
+import type { FormInstance } from 'element-plus';
+import { listQcTasks, submitQcTask, auditQcTask } from '@/api/home/taskCenter/qc';
+import { QcTaskVO, QcTaskQuery, QcTaskSubmitForm, QcTaskAuditForm } from '@/api/home/taskCenter/qc/types';
+import { queryMemberNotInCenter } from '@/api/project/management';
+import { MemberNotInCenterVO, MemberNotInCenterQuery } from '@/api/project/management/types';
+import { parseI18nName } from '@/utils/i18n';
+import fileUpload from '@/components/FileUpload/index.vue';
+
+const { proxy } = getCurrentInstance() as any;
+const { qc_question_type } = toRefs<any>(proxy?.useDict('qc_question_type'));
 
 const loading = ref(false);
+const submitButtonLoading = ref(false);
+const auditLoading = ref(false);
+
+// 当前任务
+const currentTask = ref<QcTaskVO | null>(null);
 
 // 查询参数
 const queryParams = reactive<QcTaskQuery>({
@@ -93,6 +217,58 @@ const queryParams = reactive<QcTaskQuery>({
 const taskList = ref<QcTaskVO[]>([]);
 const total = ref(0);
 
+// 递交对话框
+const submitDialog = reactive({
+  visible: false,
+  title: ''
+});
+
+// 递交表单ref
+const submitFormRef = ref<FormInstance>();
+
+// 递交表单数据
+const submitForm = ref({
+  document: ''
+});
+
+// 递交表单验证规则
+const submitRules = {
+  document: [{ required: true, message: '请上传文件', trigger: 'change' }]
+};
+
+// 审核相关
+const auditFormRef = ref<FormInstance>();
+const auditDialog = reactive({
+  visible: false
+});
+
+// 审核表单初始数据
+const initAuditForm: QcTaskAuditForm = {
+  taskId: 0,
+  detailId: 0,
+  result: 1,
+  rejectionType: undefined,
+  opinion: undefined,
+  designatedDealer: undefined,
+  deadline: undefined
+};
+
+const auditForm = ref<QcTaskAuditForm>({ ...initAuditForm });
+
+// 审核表单验证规则
+const auditRules = computed(() => ({
+  result: [{ required: true, message: '请选择审核结果', trigger: 'change' }],
+  rejectionType: auditForm.value.result === 2 ? [{ required: true, message: '请选择问题类型', trigger: 'change' }] : [],
+  opinion: auditForm.value.result === 2 ? [{ required: true, message: '请输入意见', trigger: 'blur' }] : [],
+  designatedDealer: auditForm.value.result === 2 ? [{ required: true, message: '请选择指定处理人', trigger: 'change' }] : [],
+  deadline: auditForm.value.result === 2 ? [{ required: true, message: '请选择截止日期', trigger: 'change' }] : []
+}));
+
+// 处理人搜索相关
+const dealerSearchLoading = ref(false);
+const dealerOptions = ref<MemberNotInCenterVO[]>([]);
+let dealerSearchTimer: NodeJS.Timeout | null = null;
+
 /**
  * 获取任务列表
  */
@@ -133,6 +309,142 @@ const resetQuery = () => {
   handleQuery();
 };
 
+/**
+ * 处理递交
+ */
+const handleSubmit = (row: QcTaskVO) => {
+  currentTask.value = row;
+  submitForm.value = {
+    document: ''
+  };
+  submitDialog.visible = true;
+  submitDialog.title = '递交文档';
+  nextTick(() => {
+    submitFormRef.value?.clearValidate();
+  });
+};
+
+/**
+ * 取消递交
+ */
+const cancelSubmit = () => {
+  submitDialog.visible = false;
+  submitForm.value = {
+    document: ''
+  };
+  currentTask.value = null;
+};
+
+/**
+ * 提交递交表单
+ */
+const submitSubmitForm = () => {
+  submitFormRef.value?.validate(async (valid: boolean) => {
+    if (valid && currentTask.value) {
+      submitButtonLoading.value = true;
+      try {
+        const submitData: QcTaskSubmitForm = {
+          detailId: currentTask.value.id,
+          document: Number(submitForm.value.document)
+        };
+        await submitQcTask(submitData);
+        ElMessage.success('递交成功');
+        submitDialog.visible = false;
+        await getTaskList();
+      } catch (error) {
+        console.error('递交失败:', error);
+        ElMessage.error('递交失败');
+      } finally {
+        submitButtonLoading.value = false;
+      }
+    }
+  });
+};
+
+/** 审核任务 */
+const handleAudit = (row: QcTaskVO) => {
+  currentTask.value = row;
+  auditForm.value = {
+    ...initAuditForm,
+    taskId: row.taskId || 0,
+    detailId: row.id
+  };
+  dealerOptions.value = [];
+  auditDialog.visible = true;
+};
+
+/** 搜索处理人 */
+const searchDealers = async (query: string) => {
+  if (!query || query.trim() === '') {
+    dealerOptions.value = [];
+    return;
+  }
+
+  if (dealerSearchTimer) {
+    clearTimeout(dealerSearchTimer);
+  }
+
+  dealerSearchTimer = setTimeout(async () => {
+    dealerSearchLoading.value = true;
+    try {
+      const queryParams: MemberNotInCenterQuery = {
+        pageNum: 1,
+        pageSize: 10,
+        projectId: currentTask.value?.projectId || 0,
+        folderId: 0,
+        name: query
+      };
+      const res = await queryMemberNotInCenter(queryParams);
+      dealerOptions.value = res.rows || [];
+    } catch (error) {
+      console.error('搜索处理人失败:', error);
+      ElMessage.error('搜索处理人失败');
+    } finally {
+      dealerSearchLoading.value = false;
+    }
+  }, 300);
+};
+
+/** 取消审核 */
+const cancelAudit = () => {
+  auditDialog.visible = false;
+  auditFormRef.value?.resetFields();
+  currentTask.value = null;
+};
+
+/** 提交审核 */
+const submitAudit = () => {
+  auditFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      auditLoading.value = true;
+      try {
+        const submitData: QcTaskAuditForm = {
+          taskId: auditForm.value.taskId,
+          detailId: auditForm.value.detailId,
+          result: auditForm.value.result
+        };
+
+        // 驳回时添加额外字段
+        if (auditForm.value.result === 2) {
+          submitData.rejectionType = auditForm.value.rejectionType;
+          submitData.opinion = auditForm.value.opinion;
+          submitData.designatedDealer = auditForm.value.designatedDealer;
+          submitData.deadline = auditForm.value.deadline;
+        }
+
+        await auditQcTask(submitData);
+        ElMessage.success('审核成功');
+        auditDialog.visible = false;
+        await getTaskList();
+      } catch (error) {
+        console.error('审核失败:', error);
+      } finally {
+        auditLoading.value = false;
+      }
+    }
+  });
+};
+
 onMounted(() => {
   getTaskList();
 });
@@ -142,6 +454,26 @@ onMounted(() => {
 .qc-task-container {
   padding: 20px;
 
+  .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;
+  }
+
   .search-form {
     margin-bottom: 15px;
   }

+ 1 - 1
src/views/login.vue

@@ -153,7 +153,7 @@ const handleLogin = () => {
       const [err] = await to(userStore.login(loginForm.value));
       if (!err) {
         // const redirectUrl = redirect.value || '/';
-        const redirectUrl = redirect.value || '/home/workbench';
+        const redirectUrl = redirect.value || '/home/dashboard';
         await router.push(redirectUrl);
         loading.value = false;
       } else {