浏览代码

资源保障接口调试时

zhangjie 1 年之前
父节点
当前提交
a5a7a0d807

+ 41 - 22
src/api/resource-guard.js

@@ -1,34 +1,37 @@
-import { request } from '@/utils/request.js';
+import { request, paramsSerializer } from '@/utils/request.js';
 
 
 // person-files
 // person-files
 export const personFilesListApi = (data) =>
 export const personFilesListApi = (data) =>
   request({
   request({
-    url: '/api/system/person-files/list',
+    url: '/api/admin/user/archives/page',
+    params: data,
+  });
+export const personFilesStatisticsApi = (data) =>
+  request({
+    url: '/api/admin/user/archives/sub_total',
+    params: data,
+  });
+export const personFilesCodeApi = (data) =>
+  request({
+    url: '/api/admin/user/archives/create_code',
+    data,
+  });
+export const personFilesEditApi = (data) =>
+  request({
+    url: '/api/admin/user/archives/edit',
     data,
     data,
   });
   });
-export const personFilesEditApi = (data) => {
-  if (data.id) {
-    return request({
-      url: '/api/system/person-files/edit',
-      data,
-    });
-  } else {
-    return request({
-      url: '/api/system/person-files/add',
-      data,
-    });
-  }
-};
 export const personFilesDestroyApi = (ids) =>
 export const personFilesDestroyApi = (ids) =>
   request({
   request({
     url: '/api/system/person-files/destroy',
     url: '/api/system/person-files/destroy',
-    data: { ids },
+    params: { ids },
+    paramsSerializer,
   });
   });
 
 
 export const personFilesExportApi = (ids) =>
 export const personFilesExportApi = (ids) =>
   request({
   request({
-    url: '/api/system/person-files/export',
-    data: { ids },
+    url: '/api/admin/user/archives/export',
+    params: data,
     download: true,
     download: true,
   });
   });
 
 
@@ -36,22 +39,38 @@ export const personFilesExportApi = (ids) =>
 export const personAllocateListApi = (data) =>
 export const personAllocateListApi = (data) =>
   request({
   request({
     url: '/api/system/person-allocate/list',
     url: '/api/system/person-allocate/list',
-    data,
+    params: data,
   });
   });
 export const personAllocateStatisticsApi = (data) =>
 export const personAllocateStatisticsApi = (data) =>
   request({
   request({
     url: '/api/system/person-allocate/statistics',
     url: '/api/system/person-allocate/statistics',
+    params: data,
+  });
+export const personAllocateBatchDeployApi = (ids) =>
+  request({
+    url: '/api/system/person-allocate/batch-deploy',
+    params: { ids },
+    paramsSerializer,
+  });
+export const personAllocateDeployApi = (data) =>
+  request({
+    url: '/api/system/person-allocate/deploy',
     data,
     data,
   });
   });
-export const personAllocateSubmitSopApi = (data) =>
+export const personAllocateSubmitSopApi = (id) =>
   request({
   request({
     url: '/api/system/person-allocate/submit-sop',
     url: '/api/system/person-allocate/submit-sop',
-    data,
+    param: { id },
+  });
+export const personAllocateCancelSubmitSopApi = (id) =>
+  request({
+    url: '/api/system/person-allocate/cancel-submit-sop',
+    param: { id },
   });
   });
 
 
 // registration-query
 // registration-query
 export const registrationQueryListApi = (data) =>
 export const registrationQueryListApi = (data) =>
   request({
   request({
     url: '/api/system/registration-query/list',
     url: '/api/system/registration-query/list',
-    data,
+    params: data,
   });
   });

+ 5 - 0
src/api/user.js

@@ -119,3 +119,8 @@ export const getAllUserList = (roleType) =>
   request({
   request({
     url: '/api/admin/common/get_user_list',
     url: '/api/admin/common/get_user_list',
   });
   });
+export const fileUploadApi = (data) =>
+  request({
+    url: '/api/admin/common/file/upload',
+    data,
+  });

+ 1 - 1
src/components/common/select-customer/index.vue

@@ -16,7 +16,7 @@
 </template>
 </template>
 
 
 <script setup name="SelectCustomer">
 <script setup name="SelectCustomer">
-import { onMounted, ref, useAttrs, watch } from 'vue';
+import { onMounted, ref, useAttrs, watch, computed } from 'vue';
 import { customerListApi } from '@/api/system';
 import { customerListApi } from '@/api/system';
 
 
 let optionList = ref([]);
 let optionList = ref([]);

+ 1 - 1
src/components/common/select-product/index.vue

@@ -16,7 +16,7 @@
 </template>
 </template>
 
 
 <script setup name="SelectProduct">
 <script setup name="SelectProduct">
-import { onMounted, ref, useAttrs, watch } from 'vue';
+import { onMounted, ref, useAttrs, watch, computed } from 'vue';
 import { productListApi } from '@/api/service-unit';
 import { productListApi } from '@/api/service-unit';
 
 
 let optionList = ref([]);
 let optionList = ref([]);

+ 1 - 1
src/components/common/select-role/index.vue

@@ -16,7 +16,7 @@
 </template>
 </template>
 
 
 <script setup name="SelectSupplier">
 <script setup name="SelectSupplier">
-import { onMounted, ref, useAttrs, watch } from 'vue';
+import { onMounted, ref, useAttrs, watch, computed } from 'vue';
 import { getAllRoleList } from '@/api/user';
 import { getAllRoleList } from '@/api/user';
 
 
 let optionList = ref([]);
 let optionList = ref([]);

+ 1 - 1
src/components/common/select-service-level/index.vue

@@ -16,7 +16,7 @@
 </template>
 </template>
 
 
 <script setup name="SelectServiceLevel">
 <script setup name="SelectServiceLevel">
-import { onMounted, ref, useAttrs, watch } from 'vue';
+import { onMounted, ref, useAttrs, watch, computed } from 'vue';
 import { serviceLevelListApi } from '@/api/system';
 import { serviceLevelListApi } from '@/api/system';
 
 
 let optionList = ref([]);
 let optionList = ref([]);

+ 1 - 1
src/components/common/select-supplier/index.vue

@@ -16,7 +16,7 @@
 </template>
 </template>
 
 
 <script setup name="SelectSupplier">
 <script setup name="SelectSupplier">
-import { onMounted, ref, useAttrs, watch } from 'vue';
+import { onMounted, ref, useAttrs, watch, computed } from 'vue';
 import { supplierListApi } from '@/api/system';
 import { supplierListApi } from '@/api/system';
 
 
 let optionList = ref([]);
 let optionList = ref([]);

+ 1 - 1
src/components/common/select-type-user/index.vue

@@ -16,7 +16,7 @@
 </template>
 </template>
 
 
 <script setup name="SelectTypeUser">
 <script setup name="SelectTypeUser">
-import { onMounted, ref, useAttrs, watch } from 'vue';
+import { onMounted, ref, useAttrs, watch, computed } from 'vue';
 import { getRoleUserList } from '@/api/user';
 import { getRoleUserList } from '@/api/user';
 
 
 let optionList = ref([]);
 let optionList = ref([]);

+ 5 - 1
src/components/common/upload-button/index.vue

@@ -61,6 +61,10 @@ const props = defineProps({
     type: Boolean,
     type: Boolean,
     default: false,
     default: false,
   },
   },
+  paramFileName: {
+    type: String,
+    default: 'file',
+  },
 });
 });
 
 
 const emit = defineEmits(['on-fail', 'on-success', 'on-validate-error']);
 const emit = defineEmits(['on-fail', 'on-success', 'on-validate-error']);
@@ -97,7 +101,7 @@ const upload = async (file) => {
   Object.entries(props.uploadData).forEach(([k, v]) => {
   Object.entries(props.uploadData).forEach(([k, v]) => {
     formData.append(k, v);
     formData.append(k, v);
   });
   });
-  formData.append('file', file.raw);
+  formData.append(props.paramFileName, file.raw);
 
 
   const md5 = await getFileMD5(file.raw);
   const md5 = await getFileMD5(file.raw);
   // console.log(md5);
   // console.log(md5);

+ 26 - 0
src/config/constants.js

@@ -11,6 +11,16 @@ export const ROLE_TYPE = {
   CUSTOM: '技术客服',
   CUSTOM: '技术客服',
   DEFINED: '自定义',
   DEFINED: '自定义',
 };
 };
+export const GENDER_TYPE = {
+  MAN: '男',
+  WOMAN: '女',
+};
+export const EDUCATION_TYPE = {
+  BACHELOR_DEGREE: '本科及以上',
+  JUNIOR_COLLEGE: '大专',
+  HIGH_SCHOOL: '高中',
+  MIDDLE_SCHOOL: '初中',
+};
 // 审核结果
 // 审核结果
 export const AUDITING_RESULT = {
 export const AUDITING_RESULT = {
   NOT_PASS: '未通过',
   NOT_PASS: '未通过',
@@ -80,6 +90,22 @@ export const ATTENDANCE_RESULT = {
   NORMAL: '正常',
   NORMAL: '正常',
   EXCEPTION: '异常',
   EXCEPTION: '异常',
 };
 };
+// 资源保障
+// 人员档案管理
+export const AUTHENTICATION_STATUS = {
+  true: '有效',
+  false: '无效',
+};
+export const AUTHENTICATION_ROLE = {
+  REGION_COORDINATOR: '区域协调人',
+  EFFECT_ENGINEER: '实施工程师',
+  ASSISTANT_ENGINEER: '助理工程师',
+};
+// 出入库
+export const INOUT_TYPE = {
+  IN: '入库',
+  OUT: '出库',
+};
 
 
 // SOP
 // SOP
 export const FLOW_STATUS = {
 export const FLOW_STATUS = {

+ 25 - 0
src/utils/filter.js

@@ -11,6 +11,11 @@ import {
   AUDITING_RESULT,
   AUDITING_RESULT,
   ATTENDANCE_TYPE,
   ATTENDANCE_TYPE,
   ATTENDANCE_RESULT,
   ATTENDANCE_RESULT,
+  AUTHENTICATION_STATUS,
+  AUTHENTICATION_ROLE,
+  INOUT_TYPE,
+  GENDER_TYPE,
+  EDUCATION_TYPE,
 } from '@/config/constants';
 } from '@/config/constants';
 import { dateFormat } from './tool';
 import { dateFormat } from './tool';
 
 
@@ -23,9 +28,19 @@ export function enableFilter(val) {
 export function auditingResultFilter(val) {
 export function auditingResultFilter(val) {
   return AUDITING_RESULT[val] || DEFAULT_FIELD;
   return AUDITING_RESULT[val] || DEFAULT_FIELD;
 }
 }
+export function genderTypeFilter(val) {
+  return GENDER_TYPE[val] || DEFAULT_FIELD;
+}
+export function educationTypeFilter(val) {
+  return EDUCATION_TYPE[val] || DEFAULT_FIELD;
+}
 export function timestampFilter(val) {
 export function timestampFilter(val) {
   return val ? dateFormat(val) : DEFAULT_FIELD;
   return val ? dateFormat(val) : DEFAULT_FIELD;
 }
 }
+export function dayCountFilter(val) {
+  console.log(val);
+  return Math.ceil(val / (24 * 60 * 60 * 1000));
+}
 // 系统管理
 // 系统管理
 export function customerTypeFilter(val) {
 export function customerTypeFilter(val) {
   return CUSTOMER_TYPE[val] || DEFAULT_FIELD;
   return CUSTOMER_TYPE[val] || DEFAULT_FIELD;
@@ -60,6 +75,16 @@ export function attendanceTypeFilter(val) {
 export function attendanceResultFilter(val) {
 export function attendanceResultFilter(val) {
   return ATTENDANCE_RESULT[val] || DEFAULT_FIELD;
   return ATTENDANCE_RESULT[val] || DEFAULT_FIELD;
 }
 }
+// 资源保障
+export function authenticationStatusFilter(val) {
+  return AUTHENTICATION_STATUS[val + ''] || DEFAULT_FIELD;
+}
+export function authenticationRoleFilter(val) {
+  return AUTHENTICATION_ROLE[val] || DEFAULT_FIELD;
+}
+export function inoutTypeFilter(val) {
+  return INOUT_TYPE[val] || DEFAULT_FIELD;
+}
 // sop
 // sop
 export function flowStatusFilter(val) {
 export function flowStatusFilter(val) {
   return FLOW_STATUS[val] || DEFAULT_FIELD;
   return FLOW_STATUS[val] || DEFAULT_FIELD;

+ 2 - 1
src/utils/tool.js

@@ -349,6 +349,7 @@ export const toSecondFloorArray = (arr) => {
  */
  */
 export const dictToOptionList = (data) => {
 export const dictToOptionList = (data) => {
   return Object.keys(data).map((k) => {
   return Object.keys(data).map((k) => {
-    return { value: k, label: data[k] };
+    const kstr = typeof k === 'number' ? k : k + '';
+    return { value: kstr, label: data[k] };
   });
   });
 };
 };

+ 76 - 28
src/views/resource-guard/device-guard/registration-query/index.vue

@@ -1,6 +1,16 @@
 <template>
 <template>
   <div class="registration-query flex flex-col h-full">
   <div class="registration-query flex flex-col h-full">
-    <SearchForm :fields="fields" :params="params"></SearchForm>
+    <SearchForm :fields="fields" :params="params">
+      <template #service="{ item, params }">
+        <select-service-unit v-model="params[item.prop]"></select-service-unit>
+      </template>
+      <template #supplier="{ item, params }">
+        <select-supplier
+          v-model="params[item.prop]"
+          type="DEVICE"
+        ></select-supplier>
+      </template>
+    </SearchForm>
     <div class="flex-1 page-wrap">
     <div class="flex-1 page-wrap">
       <t-table
       <t-table
         size="small"
         size="small"
@@ -16,57 +26,62 @@
           current: pagination.pageNumber,
           current: pagination.pageNumber,
         }"
         }"
       >
       >
+        <template #create-time="{ col, row }">
+          {{ timestampFilter(row[col.colKey]) }}
+        </template>
       </t-table>
       </t-table>
     </div>
     </div>
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup lang="jsx" name="RegistrationQuery">
 <script setup lang="jsx" name="RegistrationQuery">
-import { ref, reactive } from 'vue';
+import { ref, reactive, computed } from 'vue';
+import { omit } from 'lodash';
+
 import { registrationQueryListApi } from '@/api/resource-guard';
 import { registrationQueryListApi } from '@/api/resource-guard';
 import useFetchTable from '@/hooks/useFetchTable';
 import useFetchTable from '@/hooks/useFetchTable';
-
-const columns = [
-  { colKey: 'a', title: '服务单元名称' },
-  { colKey: 'b', title: '出/入库时间' },
-  { colKey: 'c', title: '登记人' },
-  { colKey: 'd', title: '客户名称' },
-  { colKey: 'e', title: '出库/入库' },
-  { colKey: 'f', title: '设备编号' },
-  { colKey: 'g', title: '供应商' },
-  { colKey: 'h', title: '发往地' },
-];
-const { pagination, tableData, search, onChange } = useFetchTable(
-  registrationQueryListApi
-);
+import { dictToOptionList } from '@/utils/tool';
+import { INOUT_TYPE } from '@/config/constants';
+import { timestampFilter } from '@/utils/filter';
 
 
 const fields = ref([
 const fields = ref([
   {
   {
-    prop: 'a',
+    prop: 'serviceUnitId',
     label: '服务单元',
     label: '服务单元',
     type: 'select',
     type: 'select',
     labelWidth: 100,
     labelWidth: 100,
     colSpan: 5,
     colSpan: 5,
+    cell: 'service',
   },
   },
   {
   {
-    prop: 'b',
+    prop: 'inout',
     label: '出库/入库',
     label: '出库/入库',
     type: 'select',
     type: 'select',
     labelWidth: 100,
     labelWidth: 100,
     colSpan: 5,
     colSpan: 5,
+    options: dictToOptionList(INOUT_TYPE),
+    attrs: {
+      clearable: true,
+    },
   },
   },
   {
   {
-    prop: 'c',
+    prop: 'deviceNo',
     label: '设备编号',
     label: '设备编号',
     labelWidth: 100,
     labelWidth: 100,
     colSpan: 5,
     colSpan: 5,
+    attrs: {
+      clearable: true,
+    },
   },
   },
   {
   {
-    prop: 'd',
+    prop: 'createTime',
     label: '出/入库时间',
     label: '出/入库时间',
     type: 'daterange',
     type: 'daterange',
     labelWidth: 100,
     labelWidth: 100,
     colSpan: 7,
     colSpan: 7,
+    attrs: {
+      clearable: true,
+    },
   },
   },
   {
   {
     type: 'buttons',
     type: 'buttons',
@@ -82,25 +97,58 @@ const fields = ref([
     ],
     ],
   },
   },
   {
   {
-    prop: 'e',
+    prop: 'supplierId',
     label: '供应商',
     label: '供应商',
     type: 'select',
     type: 'select',
     labelWidth: 100,
     labelWidth: 100,
     colSpan: 5,
     colSpan: 5,
+    cell: 'supplier',
   },
   },
   {
   {
-    prop: 'e',
+    prop: 'custom',
     label: '客户名称',
     label: '客户名称',
     labelWidth: 100,
     labelWidth: 100,
     colSpan: 5,
     colSpan: 5,
+    attrs: {
+      clearable: true,
+    },
   },
   },
 ]);
 ]);
 const params = reactive({
 const params = reactive({
-  a: '',
-  b: '',
-  c: '',
-  d: [],
-  e: '',
-  f: '',
+  serviceUnitId: '',
+  inout: '',
+  deviceNo: '',
+  createTime: [],
+  supplierId: '',
+  custom: '',
+});
+
+const computedParams = computed(() => {
+  let data = omit(params, ['createTime']);
+  data.startTime = params.createTime[0];
+  data.endTime = params.createTime[1];
+  return data;
 });
 });
+
+const columns = [
+  { colKey: 'serviceUnitName', title: '服务单元名称' },
+  {
+    colKey: 'createTime',
+    title: '出/入库时间',
+    cell: 'create-time',
+    width: 170,
+  },
+  { colKey: 'creator', title: '登记人' },
+  { colKey: 'custom', title: '客户名称' },
+  { colKey: 'inout', title: '出库/入库', width: 100 },
+  { colKey: 'deviceNo', title: '设备编号' },
+  { colKey: 'supplier', title: '供应商' },
+  { colKey: 'toArea', title: '发往地' },
+];
+const { pagination, tableData, search, onChange } = useFetchTable(
+  registrationQueryListApi,
+  {
+    params: computedParams,
+  }
+);
 </script>
 </script>

+ 146 - 75
src/views/resource-guard/person-guard/person-allocate/index.vue

@@ -1,6 +1,13 @@
 <template>
 <template>
   <div class="person-allocate flex flex-col h-full">
   <div class="person-allocate flex flex-col h-full">
-    <SearchForm :fields="fields" :params="params"></SearchForm>
+    <SearchForm :fields="fields" :params="params">
+      <template #service="{ item, params }">
+        <select-service-unit v-model="params[item.prop]"></select-service-unit>
+      </template>
+      <template #city>
+        <select-area v-model="areaInfo" @change="areaChange"></select-area>
+      </template>
+    </SearchForm>
     <div class="flex-1 page-wrap">
     <div class="flex-1 page-wrap">
       <div class="flex justify-between items-center">
       <div class="flex justify-between items-center">
         <t-space>
         <t-space>
@@ -22,7 +29,7 @@
           <t-button
           <t-button
             theme="success"
             theme="success"
             :disabled="!selectedRowKeys.length"
             :disabled="!selectedRowKeys.length"
-            @click="multDeploy"
+            @click="handleBatchDeploy"
             >批量调配</t-button
             >批量调配</t-button
           >
           >
         </div>
         </div>
@@ -44,8 +51,35 @@
         select-on-row-click
         select-on-row-click
         @select-change="selectChange"
         @select-change="selectChange"
       >
       >
+        <template #custom-type="col, row">
+          {{ customerTypeFilter(row[col.colKey]) }}
+        </template>
+        <template #operate="{ row }">
+          <div class="table-operations">
+            <t-link theme="primary" hover="color" @click="handleDeploy(row)">
+              调配
+            </t-link>
+            <t-link theme="primary" hover="color" @click="handleSubmitSop(row)">
+              发布SOP
+            </t-link>
+            <t-link
+              theme="danger"
+              hover="color"
+              @click="handleCancelSubmitSop(row)"
+            >
+              撤销发布
+            </t-link>
+          </div>
+        </template>
       </t-table>
       </t-table>
     </div>
     </div>
+
+    <!-- PersonDeployDialog -->
+    <person-deploy-dialog
+      v-model:visible="showPersonDeployDialog"
+      :curRow="curRow"
+      @success="personDeploySuccess"
+    ></person-deploy-dialog>
   </div>
   </div>
 </template>
 </template>
 
 
@@ -56,74 +90,21 @@ import useFetchTable from '@/hooks/useFetchTable';
 import {
 import {
   personAllocateListApi,
   personAllocateListApi,
   personAllocateStatisticsApi,
   personAllocateStatisticsApi,
+  personAllocateBatchDeployApi,
   personAllocateSubmitSopApi,
   personAllocateSubmitSopApi,
+  personAllocateCancelSubmitSopApi,
 } from '@/api/resource-guard';
 } from '@/api/resource-guard';
+import { customerTypeFilter } from '@/utils/filter';
+import PersonDeployDialog from './person-deploy-dialog.vue';
 
 
-const columns = [
-  {
-    colKey: 'row-select',
-    type: 'multiple',
-    width: 50,
-    fixed: 'left',
-  },
-  { colKey: 'a', title: '服务单元' },
-  { colKey: 'b', title: '项目单号' },
-  { colKey: 'c', title: '客户名称' },
-  { colKey: 'd', title: '客户类型' },
-  { colKey: 'e', title: '省份' },
-  { colKey: 'f', title: '城市' },
-  { colKey: 'g', title: '区县' },
-  { colKey: 'h', title: '服务挡位' },
-  { colKey: 'i', title: '项目角色配置', minWidth: 140 },
-  { colKey: 'j', title: '配额(人)' },
-  { colKey: 'k', title: '已分配(人)' },
-  { colKey: 'l', title: '分配差额(人)' },
-  {
-    title: '操作',
-    colKey: 'operate',
-    fixed: 'right',
-    width: 120,
-    align: 'center',
-    cell: (h, { row }) => {
-      return (
-        <div class="table-operations">
-          <t-link
-            theme="primary"
-            hover="color"
-            onClick={(e) => {
-              e.stopPropagation();
-              handleDeploy(row);
-            }}
-          >
-            调配
-          </t-link>
-          <t-link
-            theme="primary"
-            hover="color"
-            onClick={(e) => {
-              e.stopPropagation();
-              handleSubmitSop(row);
-            }}
-          >
-            发布SOP
-          </t-link>
-        </div>
-      );
-    },
-  },
-];
-const { pagination, tableData, fetchData, search, onChange } = useFetchTable(
-  personAllocateListApi,
-  {
-    fetchDataHandle: () => {
-      selectedRowKeys.value = [];
-    },
-  }
-);
-let statisticsInfo = reactive({ a: 1, b: 2, c: 3, d: 4, e: 5 });
+const curRow = ref(null);
+const showPersonDeployDialog = ref(false);
+
+let areaInfo = ref(['', '', '']);
+let statisticsInfo = ref({});
 const getStatisticsInfo = async () => {
 const getStatisticsInfo = async () => {
   const res = await personAllocateStatisticsApi(params);
   const res = await personAllocateStatisticsApi(params);
-  statisticsInfo = res.data || {};
+  statisticsInfo = res || {};
 };
 };
 
 
 const selectedRowKeys = ref([]);
 const selectedRowKeys = ref([]);
@@ -133,30 +114,33 @@ const selectChange = (value) => {
 
 
 const fields = ref([
 const fields = ref([
   {
   {
-    prop: 'a',
+    prop: 'serviceUnitId',
     label: '服务单元',
     label: '服务单元',
     type: 'select',
     type: 'select',
     labelWidth: 100,
     labelWidth: 100,
     colSpan: 4,
     colSpan: 4,
+    cell: 'service',
   },
   },
   {
   {
-    prop: 'b',
+    prop: 'province',
     label: '服务区域',
     label: '服务区域',
     type: 'select',
     type: 'select',
     labelWidth: 100,
     labelWidth: 100,
     colSpan: 8,
     colSpan: 8,
+    cell: 'area',
   },
   },
   {
   {
-    prop: 'c',
+    prop: 'costomer',
     label: '客户名称',
     label: '客户名称',
     labelWidth: 100,
     labelWidth: 100,
     colSpan: 4,
     colSpan: 4,
   },
   },
   {
   {
-    prop: 'd',
+    prop: 'count',
     label: '分配差额≤',
     label: '分配差额≤',
     labelWidth: 100,
     labelWidth: 100,
     colSpan: 4,
     colSpan: 4,
+    type: 'number',
   },
   },
   {
   {
     type: 'buttons',
     type: 'buttons',
@@ -175,15 +159,85 @@ const fields = ref([
   },
   },
 ]);
 ]);
 const params = reactive({
 const params = reactive({
-  a: '',
-  b: '',
-  c: '',
-  d: '',
+  serviceUnitId: '',
+  province: '',
+  city: '',
+  area: '',
+  costomer: '',
+  count: '',
 });
 });
 
 
-const multDeploy = () => {};
+const columns = [
+  {
+    colKey: 'row-select',
+    type: 'multiple',
+    width: 50,
+    fixed: 'left',
+  },
+  { colKey: 'serviceUnitName', title: '服务单元' },
+  { colKey: 'crmNo', title: '项目单号' },
+  { colKey: 'custom', title: '客户名称' },
+  { colKey: 'customType', title: '客户类型', cell: 'custom-type', width: 100 },
+  { colKey: 'province', title: '省份' },
+  { colKey: 'city', title: '城市' },
+  { colKey: 'area', title: '区县' },
+  { colKey: 'level', title: '服务挡位' },
+  { colKey: 'roleList', title: '项目角色配置', minWidth: 140 },
+  { colKey: 'j', title: '配额(人)', width: 80 },
+  { colKey: 'k', title: '已分配(人)', width: 100 },
+  { colKey: 'l', title: '分配差额(人)', width: 120 },
+  {
+    title: '操作',
+    colKey: 'operate',
+    fixed: 'right',
+    width: 120,
+    align: 'center',
+    cell: 'operate',
+  },
+];
+const { pagination, tableData, fetchData, search, onChange } = useFetchTable(
+  personAllocateListApi,
+  {
+    fetchDataHandle: () => {
+      selectedRowKeys.value = [];
+    },
+    params,
+  }
+);
+
+const personDeploySuccess = () => {
+  fetchData();
+  getStatisticsInfo();
+};
+
+const areaChange = (data) => {
+  params.provice = data[0];
+  params.city = data[1];
+  params.are = data[2];
+};
+
+const handleBatchDeploy = () => {
+  if (!selectedRowKeys.value.length) {
+    MessagePlugin.error('请选择记录');
+    return;
+  }
+  const confirmDia = DialogPlugin({
+    header: '批量调配提示',
+    body: `批量调配会对派单中可能已调配人员的结果进行重新调配,您还要继续该操作吗?`,
+    confirmBtn: '确定',
+    cancelBtn: '取消',
+    onConfirm: async () => {
+      confirmDia.hide();
+      const res = await personAllocateBatchDeployApi(row.id).catch(() => {});
+      if (!res) return;
+      MessagePlugin.success('操作成功');
+      fetchData();
+    },
+  });
+};
 const handleDeploy = (row) => {
 const handleDeploy = (row) => {
-  console.log(row);
+  curRow.value = row;
+  showPersonDeployDialog.value = true;
 };
 };
 const handleSubmitSop = (row) => {
 const handleSubmitSop = (row) => {
   const confirmDia = DialogPlugin({
   const confirmDia = DialogPlugin({
@@ -200,4 +254,21 @@ const handleSubmitSop = (row) => {
     },
     },
   });
   });
 };
 };
+const handleCancelSubmitSop = (row) => {
+  const confirmDia = DialogPlugin({
+    header: '撤销SOP提示',
+    body: `您确定要撤销当前派单的SOP吗?若该SOP已开始填报,则撤销后该SOP填报的所有信息将会全部清空,请谨慎操作!`,
+    confirmBtn: '确定',
+    cancelBtn: '取消',
+    onConfirm: async () => {
+      confirmDia.hide();
+      const res = await personAllocateCancelSubmitSopApi(row.id).catch(
+        () => {}
+      );
+      if (!res) return;
+      MessagePlugin.success('操作成功');
+      fetchData();
+    },
+  });
+};
 </script>
 </script>

+ 213 - 0
src/views/resource-guard/person-guard/person-allocate/person-deploy-dialog.vue

@@ -0,0 +1,213 @@
+<template>
+  <my-dialog
+    :visible="visible"
+    header="调配"
+    :width="900"
+    attach="body"
+    :closeOnOverlayClick="false"
+    @close="emit('update:visible', false)"
+    @opened="dialogOpened"
+  >
+    <t-form ref="formRef" :labelWidth="120">
+      <t-row :gutter="[0, 0]">
+        <t-col :span="4">
+          <t-form-item label="服务单元"
+            >{{ formData.serviceUnitName }}
+          </t-form-item>
+        </t-col>
+        <t-col :span="4">
+          <t-form-item label="项目单号">{{ formData.crmNo }} </t-form-item>
+        </t-col>
+        <t-col :span="4">
+          <t-form-item label="客户类型">{{ formData.customType }} </t-form-item>
+        </t-col>
+        <t-col :span="8">
+          <t-form-item label="客户名称">{{ formData.custom }} </t-form-item>
+        </t-col>
+        <t-col :span="4">
+          <t-form-item label="服务档位">{{ formData.level }} </t-form-item>
+        </t-col>
+        <t-col :span="12">
+          <t-form-item label="客户地址">
+            <t-space>
+              <span>{{ formData.privince }}</span>
+              <span>{{ formData.city }}</span>
+              <span>{{ formData.area }}</span>
+              <span>{{ formData.address }}</span>
+            </t-space>
+          </t-form-item>
+        </t-col>
+        <t-col :span="12">
+          <t-form-item label="人员调配">
+            <div>
+              <t-table
+                size="small"
+                row-key="roleId"
+                :columns="roleColumns"
+                :data="formData.roleList"
+              >
+                <template #num="{ row }">
+                  <span>{{ row.users.length }}</span>
+                  <span>/</span>
+                  <span>{{ row.quota }}</span>
+                </template>
+                <template #users="{ row }">
+                  <select-type-user
+                    :type="row.roleType"
+                    :multiple="row.roleType !== 'REGION_COORDINATOR'"
+                  ></select-type-user>
+                </template>
+                <template #operate="{ row }">
+                  <div
+                    v-if="row.roleType !== 'REGION_COORDINATOR'"
+                    class="table-operations"
+                  >
+                    <t-button
+                      shape="circle"
+                      theme="danger"
+                      @click="handleDelete(row)"
+                    >
+                      <template #icon> <MinusCircleFilledIcon /></template>
+                    </t-button>
+                  </div>
+                </template>
+              </t-table>
+            </div>
+          </t-form-item>
+          <t-form-item>
+            <div class="flex justify-between items-center" style="width: 100%">
+              <select-role
+                v-model="selectedRoleId"
+                @change="roleChange"
+              ></select-role>
+              <t-button
+                class="m-l-10px"
+                theme="primary"
+                :disabled="!selectedRole"
+                @click="toAddRole"
+                >添加</t-button
+              >
+            </div>
+          </t-form-item>
+        </t-col>
+      </t-row>
+    </t-form>
+    <template #foot>
+      <t-button theme="default" @click="emit('update:visible', false)"
+        >取消</t-button
+      >
+      <t-button theme="primary" @click="save">保存</t-button>
+    </template>
+  </my-dialog>
+</template>
+<script setup name="PersonDeployDialog">
+import { ref } from 'vue';
+import { MessagePlugin, MinusCircleFilledIcon } from 'tdesign-vue-next';
+import useClearDialog from '@/hooks/useClearDialog';
+import { personAllocateDeployApi } from '@/api/resource-guard';
+
+const emit = defineEmits(['update:visible', 'success']);
+const props = defineProps({
+  visible: Boolean,
+  curRow: Object,
+});
+
+const formRef = ref(null);
+let selectedRole = ref(null);
+let selectedRoleId = ref(null);
+
+const { formData } = useClearDialog(
+  {
+    id: null,
+    level: '',
+    type: '',
+    devices: null,
+    enable: true,
+    roleList: [],
+  },
+  props,
+  formRef,
+  () => {
+    for (let key in formData) {
+      formData[key] = props.curRow[key];
+    }
+  }
+);
+const rules = {
+  roleList: [
+    {
+      validator: (val) => {
+        if (!val || !val.length)
+          return { result: false, message: '请填写至少一个角色' };
+
+        if (val.some((item) => !item.quota))
+          return { result: false, message: '有角色未设置配额' };
+
+        return { result: true, type: 'success' };
+      },
+      type: 'error',
+      trigger: 'change',
+    },
+  ],
+};
+
+const roleColumns = [
+  { colKey: 'roleName', title: '项目角色', width: 120 },
+  { colKey: 'quota', title: '配额', cell: 'num', width: 80, align: 'center' },
+  { colKey: 'users', title: '人员', cell: 'users' },
+  { colKey: 'operate', title: '', cell: 'operate', width: 60 },
+];
+
+const dialogOpened = () => {
+  selectedRole.value = null;
+  selectedRoleId.value = null;
+};
+
+const roleChange = (role) => {
+  selectedRole.value = role;
+};
+const toAddRole = () => {
+  if (formData.roleList.some((item) => item.roleId === selectedRole.value.id)) {
+    MessagePlugin.error('当前角色已经被选过!');
+    return;
+  }
+
+  formData.roleList.push({
+    quota: 1,
+    users: [],
+    roleId: selectedRole.value.id,
+    roleName: selectedRole.value.name,
+    roleType: selectedRole.value.type,
+  });
+};
+const handleDelete = (row) => {
+  formData.roleList = formData.roleList.filter(
+    (item) => item.roleId !== row.roleId
+  );
+};
+
+const checkRoleList = () => {
+  const val = formData.roleList;
+  if (!val || !val.length)
+    return { result: false, message: '至少配置一个角色' };
+
+  if (val.some((item) => !item.users.length))
+    return { result: false, message: '有角色未设置人员' };
+
+  return { result: true, type: 'success' };
+};
+const save = async () => {
+  const checkResult = checkRoleList();
+  if (!checkResult.result) {
+    MessagePlugin.error(checkResult.message);
+    return;
+  }
+
+  const res = await personAllocateDeployApi(formData).catch(() => {});
+  if (!res) return;
+
+  MessagePlugin.success('保存成功');
+  emit('update:visible', false);
+  emit('success');
+};
+</script>

+ 179 - 78
src/views/resource-guard/person-guard/person-files/add-person-file-dialog.vue

@@ -2,57 +2,77 @@
   <my-dialog
   <my-dialog
     :visible="visible"
     :visible="visible"
     :header="title"
     :header="title"
-    :width="900"
+    :width="1000"
     :closeOnOverlayClick="false"
     :closeOnOverlayClick="false"
     @close="emit('update:visible', false)"
     @close="emit('update:visible', false)"
+    @opened="dialogOpened"
   >
   >
     <t-form ref="formRef" :model="formData" :rules="rules" labelWidth="120px">
     <t-form ref="formRef" :model="formData" :rules="rules" labelWidth="120px">
       <t-row :gutter="[0, 20]">
       <t-row :gutter="[0, 20]">
         <t-col :span="4">
         <t-col :span="4">
           <t-form-item label="人员档案编号">
           <t-form-item label="人员档案编号">
-            <span>123456*****</span>
+            <span>{{ formData.code }}</span>
           </t-form-item>
           </t-form-item>
         </t-col>
         </t-col>
         <t-col :span="4">
         <t-col :span="4">
-          <t-form-item label="姓名" name="a">
-            <t-input v-model="formData.a"></t-input>
+          <t-form-item label="姓名" name="name">
+            <t-input v-model="formData.name"></t-input>
           </t-form-item>
           </t-form-item>
         </t-col>
         </t-col>
         <t-col :span="4">
         <t-col :span="4">
-          <t-form-item label="常住区域" name="b">
-            <t-select v-model="formData.b"></t-select>
+          <t-form-item label="常住区域" name="city">
+            <select-area
+              v-model="areaInfo"
+              :level="2"
+              value-type="full"
+              @change="areaChange"
+            ></select-area>
           </t-form-item>
           </t-form-item>
         </t-col>
         </t-col>
         <t-col :span="4">
         <t-col :span="4">
-          <t-form-item label="身份证号" name="c">
-            <t-input v-model="formData.c"></t-input>
+          <t-form-item label="身份证号" name="identity">
+            <t-input v-model="formData.identity"></t-input>
           </t-form-item>
           </t-form-item>
         </t-col>
         </t-col>
         <t-col :span="4">
         <t-col :span="4">
-          <t-form-item label="性别" name="d">
-            <t-select v-model="formData.d"></t-select>
+          <t-form-item label="性别" name="gender">
+            <t-select v-model="formData.gender">
+              <t-option
+                v-for="(val, key) in GENDER_TYPE"
+                :key="key"
+                :label="val"
+                :value="key"
+              ></t-option>
+            </t-select>
           </t-form-item>
           </t-form-item>
         </t-col>
         </t-col>
         <t-col :span="4">
         <t-col :span="4">
-          <t-form-item label="学历" name="e">
-            <t-select v-model="formData.e"></t-select>
+          <t-form-item label="学历" name="education">
+            <t-select v-model="formData.education">
+              <t-option
+                v-for="(val, key) in EDUCATION_TYPE"
+                :key="key"
+                :label="val"
+                :value="key"
+              ></t-option>
+            </t-select>
           </t-form-item>
           </t-form-item>
         </t-col>
         </t-col>
         <t-col :span="4">
         <t-col :span="4">
-          <t-form-item label="手机号" name="f">
+          <t-form-item label="手机号" name="mobileNumber">
             <t-input v-model="formData.f"></t-input>
             <t-input v-model="formData.f"></t-input>
           </t-form-item>
           </t-form-item>
         </t-col>
         </t-col>
         <t-col :span="8">
         <t-col :span="8">
-          <t-form-item label="电子邮箱" name="g">
-            <t-input v-model="formData.g"></t-input>
+          <t-form-item label="电子邮箱" name="email">
+            <t-input v-model="formData.email"></t-input>
           </t-form-item>
           </t-form-item>
         </t-col>
         </t-col>
         <t-col :span="12">
         <t-col :span="12">
           <t-form-item label="上传照片">
           <t-form-item label="上传照片">
             <t-upload
             <t-upload
               ref="uploadRef"
               ref="uploadRef"
-              v-model="file"
+              v-model="photos"
               theme="image"
               theme="image"
               tips="请上传近期清晰免冠一寸登记照,图片格式:JPEG、JPG、PNG、GIF格式,文件大小不超过2M,推荐尺寸大小:295*412,上传不符合要求的照片将影响现场服务人员正常的打卡签到。"
               tips="请上传近期清晰免冠一寸登记照,图片格式:JPEG、JPG、PNG、GIF格式,文件大小不超过2M,推荐尺寸大小:295*412,上传不符合要求的照片将影响现场服务人员正常的打卡签到。"
               accept="image/*"
               accept="image/*"
@@ -64,50 +84,69 @@
               :size-limit="{ size: 2, unit: 'MB' }"
               :size-limit="{ size: 2, unit: 'MB' }"
               :request-method="requestMethod"
               :request-method="requestMethod"
             >
             >
-              <!-- custom UI -->
-              <!-- <template #fileListDisplay="{ files }">
-          <div>{{ JSON.stringify(files) }}</div>
-        </template> -->
             </t-upload>
             </t-upload>
           </t-form-item>
           </t-form-item>
         </t-col>
         </t-col>
         <t-col :span="4">
         <t-col :span="4">
-          <t-form-item label="所属供应商" name="i">
-            <t-select v-model="formData.i"></t-select>
+          <t-form-item label="所属供应商" name="supplierId">
+            <select-supplier v-model="formData.supplierId"></select-supplier>
           </t-form-item>
           </t-form-item>
         </t-col>
         </t-col>
         <t-col :span="4">
         <t-col :span="4">
-          <t-form-item label="入档时间" name="j">
-            <t-date-picker v-model="formData.j" />
+          <t-form-item label="入档时间" name="archivesTime">
+            <t-date-picker
+              v-model="formData.archivesTime"
+              value-type="time-stamp"
+            />
           </t-form-item>
           </t-form-item>
         </t-col>
         </t-col>
         <t-col :span="12">
         <t-col :span="12">
-          <t-form-item label="认证项目角色" name="k">
-            <t-checkbox-group v-model="formData.k">
-              <t-checkbox>实施工程师</t-checkbox>
-              <t-checkbox>区域协调人</t-checkbox>
-              <t-checkbox>助理工程师</t-checkbox>
+          <t-form-item label="认证项目角色" name="roleIds">
+            <t-checkbox-group v-model="formData.roleIds" :max="1">
+              <t-checkbox
+                v-for="(val, key) in AUTHENTICATION_ROLE"
+                :key="key"
+                :value="key"
+                :label="val"
+              ></t-checkbox>
             </t-checkbox-group>
             </t-checkbox-group>
           </t-form-item>
           </t-form-item>
         </t-col>
         </t-col>
         <t-col :span="4">
         <t-col :span="4">
           <t-form-item label="认证时间">
           <t-form-item label="认证时间">
-            <t-date-picker v-model="formData.l" />
+            <t-date-picker
+              v-model="formData.authenticationTime"
+              value-type="time-stamp"
+              :disable-date="{ after: formData.examEndTime || undefined }"
+            />
           </t-form-item>
           </t-form-item>
         </t-col>
         </t-col>
         <t-col :span="4">
         <t-col :span="4">
           <t-form-item label="认证成绩">
           <t-form-item label="认证成绩">
-            <t-input v-model="formData.m"></t-input>
+            <t-input-number
+              v-model="formData.authenticationScore"
+              theme="column"
+              :decimalPlaces="1"
+              :max="1000"
+              :min="0"
+              style="width: 100px"
+            ></t-input-number>
           </t-form-item>
           </t-form-item>
         </t-col>
         </t-col>
         <t-col :span="4">
         <t-col :span="4">
           <t-form-item label="认证有效期">
           <t-form-item label="认证有效期">
-            <t-date-picker v-model="formData.n" />
+            <t-date-picker
+              v-model="formData.authenticationValidTime"
+              value-type="time-stamp"
+              :disable-date="{
+                before: formData.authenticationTime || undefined,
+              }"
+            />
           </t-form-item>
           </t-form-item>
         </t-col>
         </t-col>
         <t-col :span="12">
         <t-col :span="12">
           <t-form-item label="备注">
           <t-form-item label="备注">
-            <t-textarea v-model="formData.o" :maxlength="100"></t-textarea>
+            <t-textarea v-model="formData.remark" :maxlength="100"></t-textarea>
           </t-form-item>
           </t-form-item>
         </t-col>
         </t-col>
       </t-row>
       </t-row>
@@ -124,62 +163,68 @@
 import { ref, computed } from 'vue';
 import { ref, computed } from 'vue';
 import { MessagePlugin } from 'tdesign-vue-next';
 import { MessagePlugin } from 'tdesign-vue-next';
 import useClearDialog from '@/hooks/useClearDialog';
 import useClearDialog from '@/hooks/useClearDialog';
-import { personfilesEditApi } from '@/api/resource-guard';
+import { personFilesEditApi, personFilesCodeApi } from '@/api/resource-guard';
+import { fileUploadApi } from '@/api/user';
+import {
+  GENDER_TYPE,
+  AUTHENTICATION_ROLE,
+  EDUCATION_TYPE,
+} from '@/config/constants';
+import { getFileMD5 } from '@/utils/crypto';
 
 
 const emit = defineEmits(['update:visible']);
 const emit = defineEmits(['update:visible']);
-const formRef = ref(null);
-const file = ref([]);
 const props = defineProps({
 const props = defineProps({
   visible: Boolean,
   visible: Boolean,
   curRow: Object,
   curRow: Object,
 });
 });
 
 
+const formRef = ref(null);
 const title = computed(() => {
 const title = computed(() => {
   return (isEdit.value ? '编辑' : '新增') + '人员档案';
   return (isEdit.value ? '编辑' : '新增') + '人员档案';
 });
 });
+let areaInfo = ref(['', '']);
+let photos = ref([]);
 
 
-const requestMethod = (f) => {
-  return new Promise((rs) => {
-    rs({
-      status: 'success',
-      response: {
-        //测试上传成功后的服务器图片地址,后期根据实际情况替换
-        url: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw1%2Fae175b96-62b4-4c48-ae36-a278a8a27e2c%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1693365377&t=363c6f09798e07b6ce44e61150763a72',
-      },
-    });
-  });
-};
-const getDetail = async () => {
-  //编辑状态下获取回显数据的接口请求业务,如果curRow里的字段够用,就直接把curRow里的字段赋值给formData
-  alert('获取详情中...');
-};
 const { formData, isEdit } = useClearDialog(
 const { formData, isEdit } = useClearDialog(
   {
   {
-    a: '',
-    b: '',
-    c: '',
-    d: '',
-    e: '',
-    f: '',
-    g: '',
-    h: '',
-    i: '',
-    j: '',
-    k: [],
-    l: '',
-    m: '',
-    n: '',
-    o: '',
+    id: '',
+    name: '',
+    province: '',
+    city: '',
+    identity: '',
+    gender: '',
+    education: '',
+    mobileNumber: '',
+    email: '',
+    basePhotoPath: '',
+    supplierId: '',
+    archivesTime: '',
+    roleIds: [],
+    authenticationTime: '',
+    authenticationScore: '',
+    authenticationValidTime: '',
+    remark: '',
   },
   },
   props,
   props,
-  getDetail
+  formRef,
+  () => {
+    for (let key in formData) {
+      formData[key] = props.curRow[key];
+    }
+  }
 );
 );
 
 
 const rules = {
 const rules = {
-  a: [
+  name: [
     { required: true, message: '姓名必填', type: 'error', trigger: 'change' },
     { required: true, message: '姓名必填', type: 'error', trigger: 'change' },
+    {
+      max: 50,
+      message: '至多50个字',
+      type: 'error',
+      trigger: 'change',
+    },
   ],
   ],
-  b: [
+  city: [
     {
     {
       required: true,
       required: true,
       message: '常住区域必选',
       message: '常住区域必选',
@@ -187,15 +232,21 @@ const rules = {
       trigger: 'change',
       trigger: 'change',
     },
     },
   ],
   ],
-  c: [
+  identity: [
     {
     {
       required: true,
       required: true,
       message: '身份证号必填',
       message: '身份证号必填',
       type: 'error',
       type: 'error',
       trigger: 'change',
       trigger: 'change',
     },
     },
+    {
+      idcard: true,
+      message: '请输入正确的身份证号码',
+      type: 'error',
+      trigger: 'change',
+    },
   ],
   ],
-  d: [
+  gender: [
     {
     {
       required: true,
       required: true,
       message: '性别必选',
       message: '性别必选',
@@ -203,7 +254,7 @@ const rules = {
       trigger: 'change',
       trigger: 'change',
     },
     },
   ],
   ],
-  e: [
+  education: [
     {
     {
       required: true,
       required: true,
       message: '学历必选',
       message: '学历必选',
@@ -211,23 +262,42 @@ const rules = {
       trigger: 'change',
       trigger: 'change',
     },
     },
   ],
   ],
-  f: [
+  mobileNumber: [
     {
     {
       required: true,
       required: true,
       message: '手机号必填',
       message: '手机号必填',
       type: 'error',
       type: 'error',
       trigger: 'change',
       trigger: 'change',
     },
     },
+    {
+      pattern: /^1\d{10}$/,
+      message: '请输入合适的手机号码',
+      trigger: 'change',
+    },
   ],
   ],
-  g: [
+  email: [
     {
     {
       required: true,
       required: true,
       message: '电子邮箱必填',
       message: '电子邮箱必填',
       type: 'error',
       type: 'error',
       trigger: 'change',
       trigger: 'change',
     },
     },
+    {
+      email: true,
+      message: '请输入正确的邮箱地址',
+      type: 'error',
+      trigger: 'change',
+    },
   ],
   ],
-  i: [
+  basePhotoPath: [
+    {
+      required: true,
+      message: '照片必传',
+      type: 'error',
+      trigger: 'change',
+    },
+  ],
+  supplierId: [
     {
     {
       required: true,
       required: true,
       message: '所属供应商必选',
       message: '所属供应商必选',
@@ -235,7 +305,7 @@ const rules = {
       trigger: 'change',
       trigger: 'change',
     },
     },
   ],
   ],
-  j: [
+  archivesTime: [
     {
     {
       required: true,
       required: true,
       message: '入档时间必填',
       message: '入档时间必填',
@@ -243,7 +313,7 @@ const rules = {
       trigger: 'change',
       trigger: 'change',
     },
     },
   ],
   ],
-  k: [
+  roleIds: [
     {
     {
       required: true,
       required: true,
       message: '认证项目角色必选',
       message: '认证项目角色必选',
@@ -253,15 +323,46 @@ const rules = {
   ],
   ],
 };
 };
 
 
+const requestMethod = async (file) => {
+  const md5Str = await getFileMD5(file);
+
+  let data = new FormData();
+  data.append('file', file);
+  data.append('md5', md5Str);
+  data.append('type', 'FILE');
+
+  const res = await fileUploadApi(data).catch(() => {});
+  if (!res) return { status: 'fail', error: '上传失败' };
+
+  formData.basePhotoPath = res.url;
+  return { status: 'success' };
+};
+
+const areaChange = (data) => {
+  formData.province = data[0];
+  formData.city = data[1];
+};
+
 const save = async () => {
 const save = async () => {
   const valid = await formRef.value.validate();
   const valid = await formRef.value.validate();
   if (valid !== true) return;
   if (valid !== true) return;
 
 
-  const res = await personfilesEditApi(formData).catch(() => {});
+  const res = await personFilesEditApi(formData).catch(() => {});
   if (!res) return;
   if (!res) return;
 
 
   MessagePlugin.success('保存成功');
   MessagePlugin.success('保存成功');
   emit('update:visible', false);
   emit('update:visible', false);
   emit('success');
   emit('success');
 };
 };
+
+const dialogOpened = async () => {
+  if (!props.curRow) {
+    const res = await personFilesCodeApi();
+    formData.code = res || '';
+    areaInfo.value = ['', ''];
+  } else {
+    areaInfo.value = [formData.province, formData.city];
+    photos.value = [formData.basePhotoPath];
+  }
+};
 </script>
 </script>

+ 218 - 112
src/views/resource-guard/person-guard/person-files/index.vue

@@ -1,32 +1,53 @@
 <template>
 <template>
   <div class="person-files flex flex-col h-full">
   <div class="person-files flex flex-col h-full">
-    <SearchForm :fields="fields" :params="params"></SearchForm>
+    <SearchForm :fields="fields" :params="params">
+      <template #city="{ item, params }">
+        <select-area v-model="params[item.prop]" :level="2"></select-area>
+      </template>
+      <template #supplier="{ item, params }">
+        <select-supplier v-model="params[item.prop]"></select-supplier>
+      </template>
+    </SearchForm>
     <div class="flex-1 page-wrap">
     <div class="flex-1 page-wrap">
-      <t-space size="small">
-        <t-button theme="success" @click="handleAdd">新增</t-button>
-        <upload-button
-          upload-url="/api/upload"
-          :button-props="{
-            content: '批量导入',
-            theme: 'success',
-          }"
-        ></upload-button>
-        <t-button
-          theme="success"
-          @click="multExport"
-          :disabled="!selectedRowKeys.length"
-          >批量导出</t-button
-        >
-        <t-button
-          theme="success"
-          :disabled="!selectedRowKeys.length"
-          @click="handleDestroy"
-          >作废</t-button
-        >
-      </t-space>
+      <div class="flex justify-between items-center">
+        <t-space>
+          <span>在册:{{ statisticsInfo.totalCount }}人</span>
+          <span>无效:{{ statisticsInfo.validCount }}人</span>
+          <span>有效:{{ statisticsInfo.invalidCount }}人</span>
+          <span
+            >实施工程师有效:{{ statisticsInfo.effectEngineerCount }}人</span
+          >
+          <span
+            >区域负责人有效:{{ statisticsInfo.regionCoordinatorCount }}人</span
+          >
+          <span
+            >助理实施工程师有效:{{
+              statisticsInfo.assistantEngineerCount
+            }}人</span
+          >
+        </t-space>
+        <t-space size="small">
+          <t-button theme="success" @click="handleAdd">新增</t-button>
+          <upload-button
+            upload-url="/api/admin/user/archives/import"
+            :button-props="{
+              content: '批量导入',
+              theme: 'success',
+            }"
+            param-file-name="MultipartFile"
+          ></upload-button>
+          <t-button theme="success" @click="multExport">批量导出</t-button>
+          <t-button
+            theme="success"
+            :disabled="!selectedRowKeys.length"
+            @click="handleDestroy"
+            >作废</t-button
+          >
+        </t-space>
+      </div>
       <t-table
       <t-table
         size="small"
         size="small"
-        row-key="id"
+        row-key="userArchivesId"
         :columns="columns"
         :columns="columns"
         :data="tableData"
         :data="tableData"
         bordered
         bordered
@@ -41,6 +62,49 @@
         select-on-row-click
         select-on-row-click
         @select-change="selectChange"
         @select-change="selectChange"
       >
       >
+        <template #archives-time="{ col, row }">
+          {{ timestampFilter(row[col.colKey]) }}
+        </template>
+        <template #valid-time="{ col, row }">
+          {{ timestampFilter(row[col.colKey]) }}
+        </template>
+        <template #gender="{ col, row }">
+          {{ genderTypeFilter(row[col.colKey]) }}
+        </template>
+        <template #education="{ col, row }">
+          {{ educationTypeFilter(row[col.colKey]) }}
+        </template>
+        <template #status="{ col, row }">
+          {{ authenticationStatusFilter(row[col.colKey]) }}
+        </template>
+        <template #remain-day="{ row }">
+          <span>
+            {{
+              dayCountFilter(row.authenticationValidTime - row.archivesTime)
+            }}天
+          </span>
+        </template>
+        <template #photo="{ col, row }">
+          <t-image
+            :src="row[col.colKey]"
+            fit="contain"
+            :style="{ width: '100px', height: '100px' }"
+          ></t-image>
+        </template>
+        <template #roles="{ row }">
+          {{
+            row.roleTypeList
+              .map((item) => authenticationRoleFilter(item))
+              .join(',')
+          }}
+        </template>
+        <template #operate="{ row }">
+          <div class="table-operations" @click.stop>
+            <t-link theme="primary" hover="color" @click="handleEdit(row)">
+              修改
+            </t-link>
+          </div>
+        </template>
       </t-table>
       </t-table>
     </div>
     </div>
 
 
@@ -53,111 +117,68 @@
 </template>
 </template>
 
 
 <script setup lang="jsx" name="PersonFiles">
 <script setup lang="jsx" name="PersonFiles">
-import { ref, reactive } from 'vue';
+import { ref, reactive, computed, onMounted } from 'vue';
+import { omit } from 'lodash';
+
 import { DialogPlugin, MessagePlugin } from 'tdesign-vue-next';
 import { DialogPlugin, MessagePlugin } from 'tdesign-vue-next';
 import useFetchTable from '@/hooks/useFetchTable';
 import useFetchTable from '@/hooks/useFetchTable';
 import AddPersonFileDialog from './add-person-file-dialog';
 import AddPersonFileDialog from './add-person-file-dialog';
 import {
 import {
   personFilesListApi,
   personFilesListApi,
+  personFilesStatisticsApi,
   personFilesDestroyApi,
   personFilesDestroyApi,
   personFilesExportApi,
   personFilesExportApi,
 } from '@/api/resource-guard';
 } from '@/api/resource-guard';
+import { dictToOptionList } from '@/utils/tool';
+import { AUTHENTICATION_ROLE, AUTHENTICATION_STATUS } from '@/config/constants';
+import {
+  authenticationRoleFilter,
+  authenticationStatusFilter,
+  timestampFilter,
+  dayCountFilter,
+  genderTypeFilter,
+  educationTypeFilter,
+} from '@/utils/filter';
 
 
 const curRow = ref(null);
 const curRow = ref(null);
 const showAddPersonFileDialog = ref(false);
 const showAddPersonFileDialog = ref(false);
 
 
-const columns = [
-  {
-    colKey: 'row-select',
-    type: 'multiple',
-    width: 50,
-    fixed: 'left',
-  },
-  { colKey: 'a', title: '档案流水号' },
-  { colKey: 'b', title: '姓名' },
-  { colKey: 'c', title: '省份' },
-  { colKey: 'd', title: '城市' },
-  { colKey: 'e', title: '区县' },
-  { colKey: 'f', title: '性别' },
-  { colKey: 'g', title: '年龄' },
-  { colKey: 'h', title: '身份证号' },
-  { colKey: 'i', title: '学历' },
-  { colKey: 'j', title: '手机号' },
-  { colKey: 'k', title: '电子邮箱' },
-  { colKey: 'l', title: '底照' },
-  { colKey: 'm', title: '供应商' },
-  { colKey: 'n', title: '入档时间' },
-  { colKey: 'o', title: '认证项目角色', width: 110 },
-  { colKey: 'p', title: '认证分数' },
-  { colKey: 'q', title: '认证有效期' },
-  { colKey: 'r', title: '剩余有效天数', width: 110 },
-  { colKey: 's', title: '认证状态' },
-  { colKey: 't', title: '备注' },
-  {
-    title: '操作',
-    colKey: 'operate',
-    fixed: 'right',
-    width: 80,
-    align: 'center',
-    cell: (h, { row }) => {
-      return (
-        <div class="table-operations">
-          <t-link
-            theme="primary"
-            hover="color"
-            onClick={(e) => {
-              e.stopPropagation();
-              handleEdit(row);
-            }}
-          >
-            修改
-          </t-link>
-        </div>
-      );
-    },
-  },
-];
-const { pagination, tableData, fetchData, search, onChange } = useFetchTable(
-  personFilesListApi,
-  {
-    fetchDataHandle: () => {
-      selectedRowKeys.value = [];
-    },
-  }
-);
-
-const selectedRowKeys = ref([]);
-const selectChange = (value, { selectedRowData }) => {
-  selectedRowKeys.value = value;
-};
-
 const fields = ref([
 const fields = ref([
   {
   {
-    prop: 'a',
+    prop: 'city',
     label: '区域',
     label: '区域',
     labelWidth: 100,
     labelWidth: 100,
     type: 'select',
     type: 'select',
     colSpan: 5,
     colSpan: 5,
+    cell: 'city',
   },
   },
   {
   {
-    prop: 'b',
+    prop: 'supplierId',
     label: '供应商',
     label: '供应商',
     labelWidth: 100,
     labelWidth: 100,
     type: 'select',
     type: 'select',
     colSpan: 5,
     colSpan: 5,
+    cell: 'supplier',
   },
   },
   {
   {
-    prop: 'c',
+    prop: 'archivesName',
     label: '姓名',
     label: '姓名',
     labelWidth: 100,
     labelWidth: 100,
     colSpan: 5,
     colSpan: 5,
+    attrs: {
+      clearable: true,
+    },
   },
   },
   {
   {
-    prop: 'd',
+    prop: 'roleType',
     label: '认证角色',
     label: '认证角色',
     labelWidth: 140,
     labelWidth: 140,
     type: 'select',
     type: 'select',
     colSpan: 6,
     colSpan: 6,
+    options: dictToOptionList(AUTHENTICATION_ROLE),
+    attrs: {
+      clearable: true,
+    },
   },
   },
   {
   {
     type: 'buttons',
     type: 'buttons',
@@ -169,40 +190,127 @@ const fields = ref([
         text: '查询',
         text: '查询',
         onClick: () => {
         onClick: () => {
           search();
           search();
+          getStatisticsInfo();
         },
         },
       },
       },
     ],
     ],
   },
   },
   {
   {
-    prop: 'e',
+    prop: 'authenticationStatus',
     label: '认证状态',
     label: '认证状态',
     labelWidth: 100,
     labelWidth: 100,
     type: 'select',
     type: 'select',
     colSpan: 5,
     colSpan: 5,
+    options: dictToOptionList(AUTHENTICATION_STATUS),
+    attrs: {
+      clearable: true,
+    },
   },
   },
   {
   {
-    prop: 'f',
+    prop: 'archivesTime',
     label: '入档时间',
     label: '入档时间',
     labelWidth: 100,
     labelWidth: 100,
     type: 'daterange',
     type: 'daterange',
     colSpan: 10,
     colSpan: 10,
+    attrs: {
+      clearable: true,
+    },
   },
   },
   {
   {
-    prop: 'g',
+    prop: 'remainValidDay',
     label: '剩余有效天数:≥',
     label: '剩余有效天数:≥',
     labelWidth: 140,
     labelWidth: 140,
     colSpan: 6,
     colSpan: 6,
+    type: 'number',
   },
   },
 ]);
 ]);
 const params = reactive({
 const params = reactive({
-  a: '',
-  b: '',
-  c: '',
-  d: '',
-  e: '',
-  f: [],
-  g: '',
+  city: '',
+  supplierId: '',
+  archivesName: '',
+  roleType: '',
+  authenticationStatus: '',
+  archivesTime: [],
+  remainValidDay: null,
 });
 });
+const computedParams = computed(() => {
+  let data = omit(params, ['archivesTime']);
+  data.archivesTimeStart = params.archivesTime[0];
+  data.archivesTimeEnd = params.archivesTime[1];
+  return data;
+});
+
+const columns = [
+  {
+    colKey: 'row-select',
+    type: 'multiple',
+    width: 50,
+    fixed: 'left',
+  },
+  { colKey: 'userArchivesId', title: '档案流水号' },
+  { colKey: 'name', title: '姓名' },
+  { colKey: 'province', title: '省份' },
+  { colKey: 'city', title: '城市' },
+  { colKey: 'area', title: '区县' },
+  { colKey: 'gender', title: '性别', cell: 'gender', width: 60 },
+  { colKey: 'age', title: '年龄', width: 60 },
+  { colKey: 'identity', title: '身份证号', width: 160 },
+  { colKey: 'education', title: '学历', cell: 'education' },
+  { colKey: 'mobileNumber', title: '手机号', width: 120 },
+  { colKey: 'email', title: '电子邮箱' },
+  { colKey: 'basePhotoPath', title: '底照', cell: 'photo', width: 120 },
+  { colKey: 'supplierName', title: '供应商' },
+  {
+    colKey: 'archivesTime',
+    title: '入档时间',
+    cell: 'archives-time',
+    width: 170,
+  },
+  {
+    colKey: 'roleTypeList',
+    title: '认证项目角色',
+    cell: 'roles',
+    minWidth: 110,
+  },
+  { colKey: 'authenticationScore', title: '认证分数' },
+  {
+    colKey: 'authenticationValidTime',
+    title: '认证有效期',
+    cell: 'valid-time',
+    width: 170,
+  },
+  { colKey: 'r', title: '剩余有效天数', cell: 'remainDay', width: 110 },
+  { colKey: 'authenticationStatus', title: '认证状态', cell: 'status' },
+  { colKey: 'remark', title: '备注' },
+  {
+    title: '操作',
+    colKey: 'operate',
+    cell: 'operate',
+    fixed: 'right',
+    width: 80,
+    align: 'center',
+  },
+];
+const { pagination, tableData, fetchData, search, onChange } = useFetchTable(
+  personFilesListApi,
+  {
+    fetchDataHandle: () => {
+      selectedRowKeys.value = [];
+    },
+    params: computedParams,
+  }
+);
+
+let statisticsInfo = ref({});
+const getStatisticsInfo = async () => {
+  const res = await personFilesStatisticsApi(params);
+  statisticsInfo.value = res || {};
+};
+
+const selectedRowKeys = ref([]);
+const selectChange = (value) => {
+  selectedRowKeys.value = value;
+};
 
 
 const handleAdd = () => {
 const handleAdd = () => {
   curRow.value = null;
   curRow.value = null;
@@ -235,23 +343,21 @@ const handleDestroy = () => {
   });
   });
 };
 };
 const multExport = () => {
 const multExport = () => {
-  if (!selectedRowKeys.value.length) {
-    MessagePlugin.error('请选择要作废的记录');
-    return;
-  }
   const confirmDia = DialogPlugin({
   const confirmDia = DialogPlugin({
     header: '操作提示',
     header: '操作提示',
-    body: `确定要导出选择的所有记录吗?`,
+    body: `确定要导出所有记录吗?`,
     confirmBtn: '确定',
     confirmBtn: '确定',
     cancelBtn: '取消',
     cancelBtn: '取消',
     onConfirm: async () => {
     onConfirm: async () => {
       confirmDia.hide();
       confirmDia.hide();
-      const res = await personFilesExportApi(selectedRowKeys.value).catch(
-        () => {}
-      );
+      const res = await personFilesExportApi(computedParams).catch(() => {});
       if (!res) return;
       if (!res) return;
       MessagePlugin.success('开始下载');
       MessagePlugin.success('开始下载');
     },
     },
   });
   });
 };
 };
+
+onMounted(() => {
+  getStatisticsInfo();
+});
 </script>
 </script>

+ 18 - 1
src/views/system/config-manage/service-level-manage/edit-service-level-dialog.vue

@@ -47,6 +47,17 @@
                     style="width: 100px"
                     style="width: 100px"
                   ></t-input-number>
                   ></t-input-number>
                 </template>
                 </template>
+                <template #operate="{ row }">
+                  <div class="table-operations">
+                    <t-button
+                      shape="circle"
+                      theme="danger"
+                      @click="handleDelete(row)"
+                    >
+                      <template #icon> <MinusCircleFilledIcon /></template>
+                    </t-button>
+                  </div>
+                </template>
               </t-table>
               </t-table>
             </div>
             </div>
           </t-form-item>
           </t-form-item>
@@ -90,7 +101,7 @@
 </template>
 </template>
 <script setup name="EditServiceLevelDialog">
 <script setup name="EditServiceLevelDialog">
 import { ref, computed } from 'vue';
 import { ref, computed } from 'vue';
-import { MessagePlugin } from 'tdesign-vue-next';
+import { MessagePlugin, MinusCircleFilledIcon } from 'tdesign-vue-next';
 import useClearDialog from '@/hooks/useClearDialog';
 import useClearDialog from '@/hooks/useClearDialog';
 import { CUSTOMER_TYPE } from '@/config/constants';
 import { CUSTOMER_TYPE } from '@/config/constants';
 import { serviceLevelEditApi } from '@/api/system';
 import { serviceLevelEditApi } from '@/api/system';
@@ -170,6 +181,7 @@ const rules = {
 const roleColumns = [
 const roleColumns = [
   { colKey: 'roleName', title: '项目角色' },
   { colKey: 'roleName', title: '项目角色' },
   { colKey: 'quota', title: '配额', cell: 'quota', width: 130 },
   { colKey: 'quota', title: '配额', cell: 'quota', width: 130 },
+  { colKey: 'operate', title: '', cell: 'operate', width: 60 },
 ];
 ];
 
 
 const dialogOpened = () => {
 const dialogOpened = () => {
@@ -192,6 +204,11 @@ const toAddRole = () => {
     roleName: selectedRole.value.name,
     roleName: selectedRole.value.name,
   });
   });
 };
 };
+const handleDelete = (row) => {
+  formData.roleList = formData.roleList.filter(
+    (item) => item.roleId !== row.roleId
+  );
+};
 
 
 const save = async () => {
 const save = async () => {
   const valid = await formRef.value.validate();
   const valid = await formRef.value.validate();

+ 2 - 0
src/views/work-hours/work-hours-manage/abnormal-check/done-check.vue

@@ -43,6 +43,8 @@
 
 
 <script setup lang="jsx" name="DoneCheck">
 <script setup lang="jsx" name="DoneCheck">
 import { reactive, ref, computed } from 'vue';
 import { reactive, ref, computed } from 'vue';
+import { omit } from 'lodash';
+
 import useFetchTable from '@/hooks/useFetchTable';
 import useFetchTable from '@/hooks/useFetchTable';
 import { workHoursDoneCheckListApi } from '@/api/work-hours';
 import { workHoursDoneCheckListApi } from '@/api/work-hours';
 import {
 import {

+ 2 - 0
src/views/work-hours/work-hours-manage/abnormal-check/wait-check.vue

@@ -72,6 +72,8 @@
 
 
 <script setup lang="jsx" name="WaitCheck">
 <script setup lang="jsx" name="WaitCheck">
 import { reactive, ref, computed } from 'vue';
 import { reactive, ref, computed } from 'vue';
+import { omit } from 'lodash';
+
 import { DialogPlugin, MessagePlugin } from 'tdesign-vue-next';
 import { DialogPlugin, MessagePlugin } from 'tdesign-vue-next';
 import useFetchTable from '@/hooks/useFetchTable';
 import useFetchTable from '@/hooks/useFetchTable';
 import {
 import {

+ 2 - 0
src/views/work-hours/work-hours-manage/work-attendance-detail/index.vue

@@ -52,6 +52,8 @@
 
 
 <script setup name="WorkAttendanceDetail">
 <script setup name="WorkAttendanceDetail">
 import { reactive, ref } from 'vue';
 import { reactive, ref } from 'vue';
+import { omit } from 'lodash';
+
 import { DialogPlugin, MessagePlugin } from 'tdesign-vue-next';
 import { DialogPlugin, MessagePlugin } from 'tdesign-vue-next';
 import useFetchTable from '@/hooks/useFetchTable';
 import useFetchTable from '@/hooks/useFetchTable';
 import {
 import {