From 5e7c6199bfb5e0944e235ba85dc8246de8e9a91c Mon Sep 17 00:00:00 2001
From: dagongren <53748875+wang-liang0615@users.noreply.github.com>
Date: Thu, 7 Nov 2024 11:52:55 +0800
Subject: [PATCH] feat:add employee work_region (#634)

* feat:add employee work_region

* env
---
 cmdb-ui/src/style/global.less                 |  16 +-
 .../setting/companyStructure/BatchUpload.vue  | 393 ------------------
 .../companyStructure/EmployeeModal.vue        | 218 +++++++---
 .../views/setting/companyStructure/index.vue  | 298 +++++++++----
 .../views/setting/components/BatchUpload.vue  | 243 +++++++++++
 .../setting/components/employeeTable.vue      |  40 +-
 cmdb-ui/src/views/setting/lang/en.js          |  40 +-
 cmdb-ui/src/views/setting/lang/zh.js          |  34 +-
 8 files changed, 700 insertions(+), 582 deletions(-)
 delete mode 100644 cmdb-ui/src/views/setting/companyStructure/BatchUpload.vue
 create mode 100644 cmdb-ui/src/views/setting/components/BatchUpload.vue

diff --git a/cmdb-ui/src/style/global.less b/cmdb-ui/src/style/global.less
index 7d86eeb..729bcf2 100644
--- a/cmdb-ui/src/style/global.less
+++ b/cmdb-ui/src/style/global.less
@@ -860,20 +860,30 @@ body {
   .vue-treeselect__control {
     border-radius: 2px !important;
     height: 32px;
+    border-color: #e4e7ed;
+    .vue-treeselect__value-container{
+      height: 30px;
+    }
+    .vue-treeselect__input-container{
+      display: flex;
+      align-items: center;
+      height: 30px;
+    }
   }
   .vue-treeselect__placeholder,
   .vue-treeselect__single-value {
-    line-height: 32px !important;
+    line-height: 28px !important;
   }
   .vue-treeselect__input {
-    height: 32px !important;
-    line-height: 32px !important;
+    height: 28px !important;
+    line-height: 28px !important;
   }
 }
 // vue-treeselect 多选样式
 .ops-setting-treeselect.vue-treeselect--multi {
   .vue-treeselect__control {
     border-radius: 2px !important;
+    border-color: #e4e7ed;
   }
 }
 
diff --git a/cmdb-ui/src/views/setting/companyStructure/BatchUpload.vue b/cmdb-ui/src/views/setting/companyStructure/BatchUpload.vue
deleted file mode 100644
index 93c163e..0000000
--- a/cmdb-ui/src/views/setting/companyStructure/BatchUpload.vue
+++ /dev/null
@@ -1,393 +0,0 @@
-<template>
-  <a-modal
-    :visible="visible"
-    :title="$t('cs.companyStructure.batchImport')"
-    dialogClass="ops-modal setting-structure-upload"
-    :width="800"
-    @cancel="close"
-  >
-    <div class="setting-structure-upload-steps">
-      <div
-        :class="{ 'setting-structure-upload-step': true, selected: index + 1 <= currentStep }"
-        v-for="(step, index) in stepList"
-        :key="step.value"
-      >
-        <div :class="{ 'setting-structure-upload-step-icon': true }">
-          <ops-icon :type="step.icon" />
-        </div>
-        <span>{{ step.label }}</span>
-      </div>
-    </div>
-    <template v-if="currentStep === 1">
-      <a-upload :multiple="false" :customRequest="customRequest" accept=".xlsx" :showUploadList="false">
-        <a-button :style="{ marginBottom: '20px' }" type="primary"> <a-icon type="upload" />{{ $t('cs.companyStructure.selectFile') }}</a-button>
-      </a-upload>
-      <p><a @click="download">{{ $t('cs.companyStructure.clickDownloadImportTemplate') }}</a></p>
-    </template>
-    <div
-      :style="{
-        height: '60px',
-        display: 'flex',
-        justifyContent: 'center',
-        alignItems: 'center',
-        whiteSpace: 'pre-wrap',
-      }"
-      v-if="currentStep === 3"
-    >
-      {{ $t('cs.companyStructure.importSuccess', { allCount: allCount })
-      }}<span :style="{ color: '#2362FB' }"> {{ allCount - errorCount }} </span>{{ $t('cs.companyStructure.count') }},
-      {{ $t('cs.companyStructure.importFailed') }}<span :style="{ color: '#D81E06' }"> {{ errorCount }} </span
-      >{{ $t('cs.companyStructure.count') }}
-    </div>
-    <vxe-table
-      v-if="currentStep === 2 || has_error"
-      ref="employeeTable"
-      stripe
-      :data="importData"
-      show-overflow
-      show-header-overflow
-      highlight-hover-row
-      size="small"
-      class="ops-stripe-table"
-      :max-height="400"
-      :column-config="{ resizable: true }"
-    >
-      <vxe-column field="email" :title="$t('cs.companyStructure.email')" min-width="120" fixed="left"></vxe-column>
-      <vxe-column field="username" :title="$t('cs.companyStructure.username')" min-width="80" ></vxe-column>
-      <vxe-column field="nickname" :title="$t('cs.companyStructure.nickname')" min-width="80"></vxe-column>
-      <vxe-column field="password" :title="$t('cs.companyStructure.password')" min-width="80"></vxe-column>
-      <vxe-column field="sex" :title="$t('cs.companyStructure.sex')" min-width="60"></vxe-column>
-      <vxe-column field="mobile" :title="$t('cs.companyStructure.mobile')" min-width="80"></vxe-column>
-      <vxe-column field="position_name" :title="$t('cs.companyStructure.positionName')" min-width="80"></vxe-column>
-      <vxe-column field="department_name" :title="$t('cs.companyStructure.departmentName')" min-width="80"></vxe-column>
-      <vxe-column v-if="has_error" field="err" :title="$t('cs.companyStructure.importFailedReason')" min-width="120" fixed="right">
-        <template #default="{ row }">
-          <span :style="{ color: '#D81E06' }">{{ row.err }}</span>
-        </template>
-      </vxe-column>
-    </vxe-table>
-    <a-space slot="footer">
-      <a-button size="small" type="primary" ghost @click="close">{{ $t('cancel') }}</a-button>
-      <a-button v-if="currentStep !== 1" size="small" type="primary" ghost @click="goPre">{{ $t('cs.companyStructure.prevStep') }}</a-button>
-      <a-button v-if="currentStep !== 3" size="small" type="primary" @click="goNext">{{ $t('cs.companyStructure.nextStep') }}</a-button>
-      <a-button v-else size="small" type="primary" @click="close">{{ $t('cs.companyStructure.done') }}</a-button>
-    </a-space>
-  </a-modal>
-</template>
-
-<script>
-import { downloadExcel, excel2Array } from '@/utils/download'
-import { importEmployee } from '@/api/employee'
-export default {
-  name: 'BatchUpload',
-  data() {
-    const stepList = [
-      {
-        value: 1,
-        label: this.$t('cs.companyStructure.uploadFile'),
-        icon: 'icon-shidi-tianjia',
-      },
-      {
-        value: 2,
-        label: this.$t('cs.companyStructure.confirmData'),
-        icon: 'icon-shidi-yunshangchuan',
-      },
-      {
-        value: 3,
-        label: this.$t('cs.companyStructure.uploadDone'),
-        icon: 'icon-shidi-queren',
-      },
-    ]
-    const common_importParamsList = [
-      'email',
-      'username',
-      'nickname',
-      'password',
-      'sex',
-      'mobile',
-      'position_name',
-      'department_name',
-      'entry_date',
-      'is_internship',
-      'leave_date',
-      'id_card',
-      'nation',
-      'id_place',
-      'party',
-      'household_registration_type',
-      'hometown',
-      'marry',
-      'max_degree',
-      'emergency_person',
-      'emergency_phone',
-      'bank_card_number',
-      'bank_card_name',
-      'opening_bank',
-      'account_opening_location',
-      'school',
-      'major',
-      'education',
-      'graduation_year',
-    ]
-    return {
-      stepList,
-      common_importParamsList,
-      visible: false,
-      currentStep: 1,
-      importData: [],
-      has_error: false,
-      allCount: 0,
-      errorCount: 0,
-    }
-  },
-  methods: {
-    open() {
-      this.importData = []
-      this.has_error = false
-      this.errorCount = 0
-      this.visible = true
-    },
-    close() {
-      this.currentStep = 1
-      this.visible = false
-    },
-    async goNext() {
-      if (this.currentStep === 2) {
-        // 此处调用后端接口
-        this.allCount = this.importData.length
-        const importData = this.importData.map((item) => {
-          const { _X_ROW_KEY, ...rest } = item
-          const keyArr = Object.keys(rest)
-          keyArr.forEach((key) => {
-            if (rest[key]) {
-              rest[key] = rest[key] + ''
-            }
-          })
-          rest.educational_experience = [
-            {
-              school: rest.school,
-              major: rest.major,
-              education: rest.education,
-              graduation_year: rest.graduation_year,
-            },
-          ]
-          delete rest.school
-          delete rest.major
-          delete rest.education
-          delete rest.graduation_year
-          return rest
-        })
-        const res = await importEmployee({ employee_list: importData })
-        if (res.length) {
-          const errData = res.filter((item) => {
-            return item.err.length
-          })
-          console.log('err', errData)
-          this.has_error = true
-          this.errorCount = errData.length
-          this.currentStep += 1
-          this.importData = errData
-          this.$message.error(this.$t('cs.companyStructure.dataErr'))
-        } else {
-          this.currentStep += 1
-          this.$message.success(this.$t('cs.companyStructure.opSuccess'))
-        }
-        this.$emit('refresh')
-      }
-    },
-    goPre() {
-      this.has_error = false
-      this.errorCount = 0
-      this.currentStep -= 1
-    },
-    download() {
-      const data = [
-        [
-          {
-            v: '1、表头标“*”的红色字体为必填项\n2、邮箱、用户名不允许重复\n3、登录密码:密码由6-20位字母、数字组成\n4、部门:上下级部门间用"/"隔开,且从最上级部门开始,例如“深圳分公司/IT部/IT二部”。如出现相同的部门,则默认导入组织架构中顺序靠前的部门',
-            t: 's',
-            s: {
-              alignment: {
-                wrapText: true,
-                vertical: 'center',
-              },
-            },
-          },
-        ],
-        [
-          {
-            v: '*邮箱',
-            t: 's',
-            s: {
-              font: {
-                color: {
-                  rgb: 'FF0000',
-                },
-              },
-            },
-          },
-          {
-            v: '*用户名',
-            t: 's',
-            s: {
-              font: {
-                color: {
-                  rgb: 'FF0000',
-                },
-              },
-            },
-          },
-          {
-            v: '*姓名',
-            t: 's',
-            s: {
-              font: {
-                color: {
-                  rgb: 'FF0000',
-                },
-              },
-            },
-          },
-          {
-            v: '*密码',
-            t: 's',
-            s: {
-              font: {
-                color: {
-                  rgb: 'FF0000',
-                },
-              },
-            },
-          },
-          {
-            v: '性别',
-            t: 's',
-          },
-          {
-            v: '手机号',
-            t: 's',
-          },
-          {
-            v: '岗位',
-            t: 's',
-          },
-          {
-            v: '部门',
-            t: 's',
-          },
-        ],
-      ]
-      data[1] = data[1].filter((item) => item['v'] !== '目前所属主体')
-      data[1] = data[1].filter((item) => item['v'] !== '初始入职日期')
-      downloadExcel(data, this.$t('cs.companyStructure.downloadTemplateName'))
-    },
-    customRequest(data) {
-      this.fileList = [data.file]
-      excel2Array(data.file).then((res) => {
-        res = res.filter((item) => item.length)
-        this.importData = res.slice(2).map((item) => {
-          const obj = {}
-          // 格式化日期字段
-          item[8] = this.formatDate(item[8]) // 目前主体入职日期
-          item[10] = this.formatDate(item[10]) // 离职日期
-          item[28] = this.formatDate(item[28]) // 毕业年份
-          item.forEach((ele, index) => {
-            obj[this.common_importParamsList[index]] = ele
-          })
-          return obj
-        })
-        this.currentStep = 2
-      })
-    },
-    formatDate(numb) {
-      if (numb) {
-        const time = new Date((numb - 1) * 24 * 3600000 + 1)
-        time.setYear(time.getFullYear() - 70)
-        time.setMonth(time.getMonth())
-        time.setHours(time.getHours() - 8)
-        time.setMinutes(time.getMinutes())
-        time.setMilliseconds(time.getMilliseconds())
-        // return time.valueOf()
-        // 日期格式
-        const format = 'Y-m-d'
-        const year = time.getFullYear()
-        // 由于 getMonth 返回值会比正常月份小 1
-        let month = time.getMonth() + 1
-        let day = time.getDate()
-        month = month > 9 ? month : `0${month}`
-        day = day > 9 ? day : `0${day}`
-        const hash = {
-          Y: year,
-          m: month,
-          d: day,
-        }
-        return format.replace(/\w/g, (o) => {
-          return hash[o]
-        })
-      } else {
-        return null
-      }
-    },
-  },
-}
-</script>
-
-<style lang="less">
-.setting-structure-upload {
-  .ant-modal-body {
-    padding: 24px 48px;
-  }
-  .setting-structure-upload-steps {
-    display: flex;
-    flex-direction: row;
-    justify-content: space-between;
-    margin-bottom: 20px;
-    .setting-structure-upload-step {
-      display: inline-block;
-      text-align: center;
-      position: relative;
-      .setting-structure-upload-step-icon {
-        width: 86px;
-        height: 86px;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        background-image: url('../../../assets/icon-bg.png');
-        margin-bottom: 20px;
-        > i {
-          font-size: 40px;
-          color: #fff;
-        }
-      }
-      > span {
-        font-size: 16px;
-        font-weight: 600;
-        color: rgba(0, 0, 0, 0.5);
-      }
-    }
-    .setting-structure-upload-step:not(:first-child)::before {
-      content: '';
-      height: 2px;
-      width: 223px;
-      position: absolute;
-      background-color: #e7ecf3;
-      left: -223px;
-      top: 43px;
-      z-index: 0;
-    }
-    .selected.setting-structure-upload-step {
-      &:not(:first-child)::before {
-        background-color: #7eb0ff;
-      }
-    }
-    .selected {
-      .setting-structure-upload-step-icon {
-        background-image: url('../../../assets/icon-bg-selected.png');
-      }
-      > span {
-        color: rgba(0, 0, 0, 0.8);
-      }
-    }
-  }
-}
-</style>
diff --git a/cmdb-ui/src/views/setting/companyStructure/EmployeeModal.vue b/cmdb-ui/src/views/setting/companyStructure/EmployeeModal.vue
index 1ebea1a..afd9b10 100644
--- a/cmdb-ui/src/views/setting/companyStructure/EmployeeModal.vue
+++ b/cmdb-ui/src/views/setting/companyStructure/EmployeeModal.vue
@@ -10,10 +10,22 @@
     :body-style="{ height: `${windowHeight - 320}px`, overflow: 'hidden', overflowY: 'scroll' }"
   >
     <a-form-model ref="employeeFormData" :model="employeeFormData" :rules="rules" :colon="false">
-      <a-form-model-item ref="email" :label="$t('cs.companyStructure.email')" prop="email" :style="formModalItemStyle" v-if="attributes.findIndex(v=>v=='email')!==-1">
-        <a-input v-model="employeeFormData.email" :placeholder="$t('cs.companyStructure.emailPlaceholder')"/>
+      <a-form-model-item
+        ref="email"
+        :label="$t('cs.companyStructure.email')"
+        prop="email"
+        :style="formModalItemStyle"
+        v-if="attributes.findIndex((v) => v == 'email') !== -1"
+      >
+        <a-input v-model="employeeFormData.email" :placeholder="$t('cs.companyStructure.emailPlaceholder')" />
       </a-form-model-item>
-      <a-form-model-item ref="username" :label="$t('cs.companyStructure.username')" prop="username" :style="formModalItemStyle" v-if="attributes.findIndex(v=>v=='username')!==-1">
+      <a-form-model-item
+        ref="username"
+        :label="$t('cs.companyStructure.username')"
+        prop="username"
+        :style="formModalItemStyle"
+        v-if="attributes.findIndex((v) => v == 'username') !== -1"
+      >
         <a-input v-model="employeeFormData.username" :placeholder="$t('cs.companyStructure.usernamePlaceholder')" />
       </a-form-model-item>
       <a-form-model-item
@@ -23,31 +35,82 @@
         prop="password"
         :style="formModalItemStyle"
       >
-        <a-input-password v-model="employeeFormData.password" :placeholder="$t('cs.companyStructure.passwordPlaceholder')" />
+        <a-input-password
+          v-model="employeeFormData.password"
+          :placeholder="$t('cs.companyStructure.passwordPlaceholder')"
+        />
       </a-form-model-item>
-      <a-form-model-item ref="nickname" :label="$t('cs.companyStructure.nickname')" prop="nickname" :style="formModalItemStyle" v-if="attributes.findIndex(v=>v=='nickname')!==-1">
+      <a-form-model-item
+        ref="nickname"
+        :label="$t('cs.companyStructure.nickname')"
+        prop="nickname"
+        :style="formModalItemStyle"
+        v-if="attributes.findIndex((v) => v == 'nickname') !== -1"
+      >
         <a-input v-model="employeeFormData.nickname" :placeholder="$t('cs.companyStructure.nicknamePlaceholder')" />
       </a-form-model-item>
-      <a-form-model-item :label="$t('cs.companyStructure.sex')" prop="sex" :style="formModalItemStyle" v-if="attributes.findIndex(v=>v=='sex')!==-1">
+      <a-form-model-item
+        :label="$t('cs.companyStructure.sex')"
+        prop="sex"
+        :style="formModalItemStyle"
+        v-if="attributes.findIndex((v) => v == 'sex') !== -1"
+      >
         <a-select v-model="employeeFormData.sex" :placeholder="$t('cs.companyStructure.sexPlaceholder')">
           <a-select-option value="男"> {{ $t('cs.companyStructure.male') }} </a-select-option>
           <a-select-option value="女"> {{ $t('cs.companyStructure.female') }} </a-select-option>
         </a-select>
       </a-form-model-item>
-      <a-form-model-item ref="mobile" :label="$t('cs.companyStructure.mobile')" prop="mobile" :style="formModalItemStyle" v-if="attributes.findIndex(v=>v=='mobile')!==-1">
+      <a-form-model-item
+        ref="mobile"
+        :label="$t('cs.companyStructure.mobile')"
+        prop="mobile"
+        :style="formModalItemStyle"
+        v-if="attributes.findIndex((v) => v == 'mobile') !== -1"
+      >
         <a-input v-model="employeeFormData.mobile" :placeholder="$t('cs.companyStructure.mobilePlaceholder')" />
       </a-form-model-item>
-      <div :style="{ width: '361px', display: 'inline-block', margin: '0 7px' }" v-if="attributes.findIndex(v=>v=='department_id')!==-1">
-        <div :style="{ height: '41px', lineHeight: '40px' }">{{ $t('cs.companyStructure.departmentName') }}</div>
-        <DepartmentTreeSelect v-model="employeeFormData.department_id" />
-      </div>
-      <a-form-model-item ref="position_name" :label="$t('cs.companyStructure.positionName')" prop="position_name" :style="formModalItemStyle" v-if="attributes.findIndex(v=>v=='position_name')!==-1">
-        <a-input v-model="employeeFormData.position_name" :placeholder="$t('cs.companyStructure.positionNamePlaceholder')" />
+      <a-form-model-item
+        ref="department_id"
+        :label="$t('cs.companyStructure.departmentName')"
+        prop="department_id"
+        :style="formModalItemStyle"
+        v-if="attributes.findIndex((v) => v == 'department_id') !== -1"
+      >
+        <DepartmentTreeSelect style="margin-top: 4px" v-model="employeeFormData.department_id" />
+      </a-form-model-item>
+      <a-form-model-item
+        ref="position_name"
+        :label="$t('cs.companyStructure.positionName')"
+        prop="position_name"
+        :style="formModalItemStyle"
+        v-if="attributes.findIndex((v) => v == 'position_name') !== -1"
+      >
+        <a-input
+          v-model="employeeFormData.position_name"
+          :placeholder="$t('cs.companyStructure.positionNamePlaceholder')"
+        />
+      </a-form-model-item>
+      <a-form-model-item
+        ref="direct_supervisor_id"
+        :label="$t('cs.companyStructure.selectDirectSupervisor')"
+        prop="direct_supervisor_id"
+        :style="formModalItemStyle"
+        v-if="attributes.findIndex((v) => v == 'direct_supervisor_id') !== -1"
+      >
+        <EmployeeTreeSelect style="margin-top: 4px" v-model="employeeFormData.direct_supervisor_id" />
+      </a-form-model-item>
+      <a-form-model-item
+        ref="work_region"
+        :label="$t('cs.companyStructure.work_region')"
+        prop="work_region"
+        :style="formModalItemStyle"
+        v-if="attributes.findIndex((v) => v == 'work_region') !== -1"
+      >
+        <a-select v-model="employeeFormData.work_region" :placeholder="$t('cs.companyStructure.workRegionPlaceholder')">
+          <a-select-option value="china_mainland"> {{ $t('cs.companyStructure.china_mainland') }} </a-select-option>
+          <a-select-option value="china_hk"> {{ $t('cs.companyStructure.china_hk') }} </a-select-option>
+        </a-select>
       </a-form-model-item>
-      <div :style="{ width: '361px', display: 'inline-block', margin: '0 7px' }" v-if="attributes.findIndex(v=>v=='direct_supervisor_id')!==-1">
-        <div :style="{ height: '41px', lineHeight: '40px' }">{{ $t('cs.companyStructure.selectDirectSupervisor') }}</div>
-        <EmployeeTreeSelect v-model="employeeFormData.direct_supervisor_id" />
-      </div>
     </a-form-model>
     <template slot="footer">
       <a-button key="back" @click="close"> {{ $t('cancel') }} </a-button>
@@ -75,7 +138,7 @@ export default {
       educational_experience: [],
       children_information: [],
       file_is_show: true,
-      attributes: []
+      attributes: [],
     }
   },
   created() {
@@ -85,7 +148,7 @@ export default {
   },
   inject: ['provide_allTreeDepartment', 'provide_allFlatEmployees'],
   computed: {
-        ...mapState({
+    ...mapState({
       windowHeight: (state) => state.windowHeight,
     }),
     departemntTreeSelectOption() {
@@ -103,7 +166,12 @@ export default {
     rules() {
       return {
         email: [
-          { required: true, whitespace: true, message: this.$t('cs.companyStructure.emailPlaceholder'), trigger: 'blur' },
+          {
+            required: true,
+            whitespace: true,
+            message: this.$t('cs.companyStructure.emailPlaceholder'),
+            trigger: 'blur',
+          },
           {
             pattern: /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$/,
             message: this.$t('cs.companyStructure.emailFormatErr'),
@@ -112,12 +180,29 @@ export default {
           { max: 50, message: this.$t('cs.person.inputStrCountLimit', { limit: 50 }) },
         ],
         username: [
-          { required: true, whitespace: true, message: this.$t('cs.companyStructure.usernamePlaceholder'), trigger: 'blur' },
+          {
+            required: true,
+            whitespace: true,
+            message: this.$t('cs.companyStructure.usernamePlaceholder'),
+            trigger: 'blur',
+          },
           { max: 20, message: this.$t('cs.person.inputStrCountLimit', { limit: 20 }) },
         ],
-        password: [{ required: true, whitespace: true, message: this.$t('cs.companyStructure.passwordPlaceholder'), trigger: 'blur' }],
+        password: [
+          {
+            required: true,
+            whitespace: true,
+            message: this.$t('cs.companyStructure.passwordPlaceholder'),
+            trigger: 'blur',
+          },
+        ],
         nickname: [
-          { required: true, whitespace: true, message: this.$t('cs.companyStructure.nicknamePlaceholder'), trigger: 'blur' },
+          {
+            required: true,
+            whitespace: true,
+            message: this.$t('cs.companyStructure.nicknamePlaceholder'),
+            trigger: 'blur',
+          },
           { max: 20, message: this.$t('cs.person.inputStrCountLimit', { limit: 20 }) },
         ],
         mobile: [
@@ -128,7 +213,7 @@ export default {
           },
         ],
       }
-    }
+    },
   },
   beforeDestroy() {
     Bus.$off('getAttributes')
@@ -136,7 +221,8 @@ export default {
   methods: {
     async open(getData, type) {
       // 提交时去掉school, major, education, graduation_year, name, gender, birthday, parental_leave_left
-      const { school, major, education, graduation_year, name, gender, birthday, parental_leave_left, ...newGetData } = getData
+      const { school, major, education, graduation_year, name, gender, birthday, parental_leave_left, ...newGetData } =
+        getData
       const _getData = _.cloneDeep(newGetData)
       const { direct_supervisor_id } = newGetData
       if (direct_supervisor_id) {
@@ -149,46 +235,54 @@ export default {
       // if (type !== 'add' && this.employeeFormData.educational_experience.length !== 0) {
       //   this.educational_experience = this.employeeFormData.educational_experience
       // }
-      this.children_information = this.formatChildrenInformationList() || [{
-        id: uuidv4(),
-        name: '',
-        gender: undefined,
-        birthday: null,
-        parental_leave_left: 0
-      }]
-      this.educational_experience = this.formatEducationalExperienceList() || [{
-        id: uuidv4(),
-        school: '',
-        major: '',
-        education: undefined,
-        graduation_year: null
-      }]
+      this.children_information = this.formatChildrenInformationList() || [
+        {
+          id: uuidv4(),
+          name: '',
+          gender: undefined,
+          birthday: null,
+          parental_leave_left: 0,
+        },
+      ]
+      this.educational_experience = this.formatEducationalExperienceList() || [
+        {
+          id: uuidv4(),
+          school: '',
+          major: '',
+          education: undefined,
+          graduation_year: null,
+        },
+      ]
 
       this.type = type
       this.visible = true
     },
     close() {
       this.$refs.employeeFormData.resetFields()
-      this.educational_experience = [{
-        school: '',
-        major: '',
-        education: undefined,
-        graduation_year: null
-      }]
-      this.children_information = [{
-        id: uuidv4(),
-        name: '',
-        gender: undefined,
-        birthday: null,
-        parental_leave_left: 0
-      }]
+      this.educational_experience = [
+        {
+          school: '',
+          major: '',
+          education: undefined,
+          graduation_year: null,
+        },
+      ]
+      this.children_information = [
+        {
+          id: uuidv4(),
+          name: '',
+          gender: undefined,
+          birthday: null,
+          parental_leave_left: 0,
+        },
+      ]
       this.visible = false
     },
     formatChildrenInformationList() {
       let arr = []
       arr = this.employeeFormData.children_information ? this.employeeFormData.children_information : undefined
       if (arr && arr.length) {
-        arr.forEach(item => {
+        arr.forEach((item) => {
           item.id = uuidv4()
         })
         return arr
@@ -199,7 +293,7 @@ export default {
       let arr = []
       arr = this.employeeFormData.educational_experience ? this.employeeFormData.educational_experience : undefined
       if (arr && arr.length) {
-        arr.forEach(item => {
+        arr.forEach((item) => {
           item.id = uuidv4()
         })
         return arr
@@ -212,12 +306,12 @@ export default {
         school: '',
         major: '',
         education: undefined,
-        graduation_year: null
+        graduation_year: null,
       }
       this.educational_experience.push(newEducational_experience)
     },
     removeEducation(removeId) {
-      const _idx = this.educational_experience.findIndex(item => item.id === removeId)
+      const _idx = this.educational_experience.findIndex((item) => item.id === removeId)
       if (_idx !== -1) {
         this.educational_experience.splice(_idx, 1)
       }
@@ -228,12 +322,12 @@ export default {
         name: '',
         gender: undefined,
         birthday: null,
-        parental_leave_left: 0
+        parental_leave_left: 0,
       }
       this.children_information.push(newChildrenInfo)
     },
     removeChildren(removeId) {
-      const _idx = this.children_information.findIndex(item => item.id === removeId)
+      const _idx = this.children_information.findIndex((item) => item.id === removeId)
       if (_idx !== -1) {
         this.children_information.splice(_idx, 1)
       }
@@ -254,10 +348,10 @@ export default {
       // }
       if (date !== null) {
         if (param === 'graduation_year') {
-          const _idx = this.educational_experience.findIndex(item => item.id === id)
+          const _idx = this.educational_experience.findIndex((item) => item.id === id)
           this.educational_experience[_idx].graduation_year = moment(date).format('YYYY-MM')
         } else if (param === 'birthday') {
-          const _idx = this.children_information.findIndex(item => item.id === id)
+          const _idx = this.children_information.findIndex((item) => item.id === id)
           this.children_information[_idx].birthday = moment(date).format('YYYY-MM-DD')
         } else {
           this.employeeFormData[param] = moment(date).format('YYYY-MM-DD')
@@ -303,7 +397,7 @@ export default {
 </script>
 <style lang="less" scoped>
 .el-date-picker {
-    width: 100%;
-    height: 36px;
+  width: 100%;
+  height: 36px;
 }
 </style>
diff --git a/cmdb-ui/src/views/setting/companyStructure/index.vue b/cmdb-ui/src/views/setting/companyStructure/index.vue
index 290e40a..b154142 100644
--- a/cmdb-ui/src/views/setting/companyStructure/index.vue
+++ b/cmdb-ui/src/views/setting/companyStructure/index.vue
@@ -16,17 +16,17 @@
             :key="group.id"
           >
             <div
-              class="ops-setting-structure-sidebar-group-header"
-              :class="{ 'group-selected': groupIndex === activeGroupIndex }"
+              :class="{
+                'ops-setting-structure-sidebar-group-header': true,
+                'group-selected': groupIndex === activeGroupIndex,
+              }"
             >
               <div class="ops-setting-structure-sidebar-group-header-avatar">
-                <a-icon :type="group.icon"/>
+                <a-icon :type="group.icon" />
               </div>
               <span
                 class="ops-setting-structure-sidebar-group-header-title"
-                @click="
-                  clickSelectGroup(groupIndex)
-                "
+                @click="clickSelectGroup(groupIndex)"
                 :id="[group.id === 0 ? 'employee' : 'department']"
               >
                 {{ group.title }}
@@ -100,7 +100,7 @@
                   />
                 </div>
                 <!-- 筛选框 -->
-                <div class="Screening-box" v-if="activeGroupIndex === 1" style="background-color: rgb(240, 245, 255) ;">
+                <div class="Screening-box" v-if="activeGroupIndex === 1" style="background-color: rgb(240, 245, 255)">
                   <a-popover
                     @visibleChange="visibleChange"
                     trigger="click"
@@ -122,16 +122,17 @@
                       </div>
                     </template>
                     <span :style="{ whiteSpace: 'nowrap' }">
-                      <a-icon class="screening-box-scene-icon" type="filter"/>
+                      <a-icon class="screening-box-scene-icon" type="filter" />
                       {{ getCurrentSceneLabel() }}
-                      <a-icon class="screening-box-scene-icon" :type="displayTimeIcon"/>
+                      <a-icon class="screening-box-scene-icon" :type="displayTimeIcon" />
                     </span>
                   </a-popover>
                 </div>
                 <SearchForm
                   ref="search"
                   :canSearchPreferenceAttrList="canSearchPreferenceAttrList"
-                  @refresh="handleSearch"/>
+                  @refresh="handleSearch"
+                />
               </div>
               <div>
                 <a-space v-if="isEditable">
@@ -168,23 +169,24 @@
               <div>
                 <div :style="{ marginTop: '8px' }" class="ops-list-batch-action" v-show="!!selectedRowKeys.length">
                   <span @click="downloadEmployeeAll">{{ $t('cs.companyStructure.downloadAll') }}</span>
-                  <a-divider type="vertical"/>
+                  <a-divider type="vertical" />
                   <span @click="exportSelectEvent">{{ $t('cs.companyStructure.downloadSelected') }}</span>
-                  <a-divider type="vertical"/>
+                  <a-divider type="vertical" />
                   <span @click="openBatchModal('department_id')">{{ $t('cs.companyStructure.editDepartment') }}</span>
-                  <a-divider type="vertical"/>
-                  <span
-                    @click="openBatchModal('direct_supervisor_id')">{{ $t('cs.companyStructure.editDirectSupervisor') }}</span>
-                  <a-divider type="vertical"/>
+                  <a-divider type="vertical" />
+                  <span @click="openBatchModal('direct_supervisor_id')">{{
+                    $t('cs.companyStructure.editDirectSupervisor')
+                  }}</span>
+                  <a-divider type="vertical" />
                   <span @click="openBatchModal('position_name')">{{ $t('cs.companyStructure.editPosition') }}</span>
-                  <a-divider type="vertical"/>
+                  <a-divider type="vertical" />
                   <span @click="openBatchModal('password')">{{ $t('cs.companyStructure.resetPassword') }}</span>
-                  <a-divider type="vertical"/>
+                  <a-divider type="vertical" />
                   <span @click="openBatchModal('block', null, 1)">{{ $t('cs.companyStructure.block') }}</span>
-                  <a-divider type="vertical"/>
+                  <a-divider type="vertical" />
                   <span @click="openBatchModal('block', null, 0)">{{ $t('cs.companyStructure.recover') }}</span>
-                  <a-divider type="vertical"/>
-                  <span>{{ $t('selectRows', {rows: selectedRowKeys.length}) }}</span>
+                  <a-divider type="vertical" />
+                  <span>{{ $t('selectRows', { rows: selectedRowKeys.length }) }}</span>
                 </div>
               </div>
               <!-- <div>
@@ -195,11 +197,11 @@
             </div>
           </div>
           <!-- 批量操作对话框 -->
-          <BatchModal ref="BatchModal" @refresh="updateAll"/>
+          <BatchModal ref="BatchModal" @refresh="updateAll" />
           <!-- 部门表单对话框 -->
-          <DepartmentModal ref="DepartmentModal" @refresh="clickSelectGroup(1)"/>
+          <DepartmentModal ref="DepartmentModal" @refresh="clickSelectGroup(1)" />
           <!-- 员工表单对话框 -->
-          <EmployeeModal ref="EmployeeModal" @refresh="updateAll"/>
+          <EmployeeModal ref="EmployeeModal" @refresh="updateAll" />
 
           <!-- 表格展示 -->
           <EmployeeTable
@@ -211,7 +213,7 @@
             @onSelectChange="onSelectChange"
             @openEmployeeModal="openEmployeeModal"
             @openBatchModal="openBatchModal"
-            @tranferAttributes="getAttributes"
+            @transferAttributes="getAttributes"
             :isEditable="isEditable"
             :loading="loading"
           >
@@ -225,7 +227,9 @@
               :page-size-options="pageSizeOptions"
               :current="tablePage.currentPage"
               :total="tablePage.totalResult"
-              :show-total="(total, range) => $t('pagination.total', { range0: range[0], range1: range[1], total:total })"
+              :show-total="
+                (total, range) => $t('pagination.total', { range0: range[0], range1: range[1], total: total })
+              "
               :page-size="tablePage.pageSize"
               :default-current="1"
               @change="pageOrSizeChange"
@@ -252,6 +256,9 @@
           clickSelectGroup(1)
         }
       "
+      @downloadTemplate="downloadTemplate"
+      @customRequest="customRequest"
+      @import="importEmployee"
     />
   </div>
 </template>
@@ -262,15 +269,22 @@ import SplitPane from '@/components/SplitPane'
 import CollapseTransition from '@/components/CollapseTransition'
 import Bus from './eventBus/bus'
 import CategroyTree from './CategoryTree'
-import BatchUpload from './BatchUpload'
+import BatchUpload from '../components/BatchUpload.vue'
 import BatchModal from './BatchModal.vue'
 import EmployeeModal from './EmployeeModal.vue'
 import DepartmentModal from './DepartmentModal.vue'
 import EmployeeTable from '../components/employeeTable.vue'
 import { getDepartmentList, deleteDepartmentById, getAllDepartmentList, getAllDepAndEmployee } from '@/api/company'
-import { getEmployeeList, getEmployeeCount, downloadAllEmployee, getEmployeeListByFilter } from '@/api/employee'
+import {
+  getEmployeeList,
+  getEmployeeCount,
+  downloadAllEmployee,
+  getEmployeeListByFilter,
+  importEmployee,
+} from '@/api/employee'
 import { mixinPermissions } from '@/utils/mixin'
 import SearchForm from '../components/SearchForm.vue'
+import { downloadExcel, excel2Array } from '@/utils/download'
 
 export default {
   name: 'CompanyStructure',
@@ -284,10 +298,22 @@ export default {
     EmployeeModal,
     DepartmentModal,
     EmployeeTable,
-    SearchForm
+    SearchForm,
   },
   data() {
+    const common_importParamsList = [
+      'email',
+      'username',
+      'nickname',
+      'password',
+      'sex',
+      'mobile',
+      'position_name',
+      'department_name',
+      'work_region',
+    ]
     return {
+      common_importParamsList,
       isActive: '',
       visible: true,
       localStorageKey: 'itsm-company-strcutre',
@@ -322,7 +348,7 @@ export default {
       attributes: [],
       pageSizeOptions: ['50', '100', '200', '9999'],
       expression: [],
-      loading: false
+      loading: false,
     }
   },
   // created() {
@@ -363,16 +389,26 @@ export default {
           value: 'sex',
           is_choice: true,
           choice_value: [
-              { label: this.$t('cs.companyStructure.male'), value: '男' },
-            { label: this.$t('cs.companyStructure.female'), value: '女' }]
+            { label: this.$t('cs.companyStructure.male'), value: '男' },
+            { label: this.$t('cs.companyStructure.female'), value: '女' },
+          ],
         },
         { label: this.$t('cs.companyStructure.mobile'), value: 'mobile' },
         { label: this.$t('cs.companyStructure.departmentName'), value: 'department_name' },
         { label: this.$t('cs.companyStructure.positionName'), value: 'position_name' },
         { label: this.$t('cs.companyStructure.supervisor'), value: 'direct_supervisor_id' },
+        {
+          label: this.$t('cs.companyStructure.work_region'),
+          value: 'work_region',
+          is_choice: true,
+          choice_value: [
+            { label: this.$t('cs.companyStructure.china_mainland'), value: 'china_mainland' },
+            { label: this.$t('cs.companyStructure.china_hk'), value: 'china_hk' },
+          ],
+        },
       ]
     },
-    sceneList () {
+    sceneList() {
       return [
         {
           label: this.$t('all'),
@@ -388,7 +424,7 @@ export default {
         },
       ]
     },
-    groupData () {
+    groupData() {
       return [
         {
           id: 0,
@@ -396,7 +432,7 @@ export default {
           icon: 'user',
         },
       ]
-    }
+    },
   },
   provide() {
     return {
@@ -426,11 +462,8 @@ export default {
     } else {
       this.currentScene = 0
     }
-    // console.log(this.currentScene)
-    // this.init()
-    this.clickSelectGroup(0).then(val => {
-      this.clickSelectItem(0)
-    })
+    this.updateCount()
+    this.clickSelectItem(0)
     Bus.$on('updataAllIncludeEmployees', () => {
       this.getAllFlatEmployees()
       this.getAllDepAndEmployee()
@@ -492,7 +525,7 @@ export default {
     setSearchPreferenceAttrList() {
       this.canSearchPreferenceAttrList.forEach((item) => {
         if (!this.attributes.includes(item.value)) {
-          this.canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter(v => v.value !== item.value)
+          this.canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((v) => v.value !== item.value)
         }
       })
     },
@@ -504,9 +537,6 @@ export default {
         this.$refs.ScreeningBoxScenePopover.$refs.tooltip.onVisibleChange(false)
       }
       document.getElementById('department').click()
-      // this.currentPage = 1
-      // this.updateTableData(1)
-      // this.departmentList = this.reqDepartmentList(-1)
     },
     clickHandler(event) {
       this.isActive = event.target.innerText
@@ -544,37 +574,6 @@ export default {
       this.activeEmployeeCount = res1.employee_count
       this.deactiveEmployeeCount = res2.employee_count
     },
-    async updateTableData(currentPage = 1, pageSize = this.tablePage.pageSize) {
-      this.selectedRowKeys = []
-      let reqEmployeeData = null
-      if (this.activeGroupIndex === 0) {
-        reqEmployeeData = await getEmployeeList({
-          ...this.tableFilterData,
-          block_status: this.block_status,
-          page: currentPage,
-          page_size: pageSize,
-          search: this.filterName,
-          order: this.tableSortData || 'direct_supervisor_id',
-        })
-      } else if (this.activeGroupIndex === 1) {
-        reqEmployeeData = await getEmployeeList({
-          ...this.tableFilterData,
-          block_status: this.currentScene,
-          department_id: this.selectDepartment.id,
-          page: currentPage,
-          page_size: pageSize,
-          search: this.filterName,
-          order: this.tableSortData || 'direct_supervisor_id',
-        })
-      }
-      this.tableData = this.FilterTableData(reqEmployeeData)
-      this.tablePage = {
-        ...this.tablePage,
-        currentPage: reqEmployeeData.page,
-        pageSize: reqEmployeeData.page_size,
-        totalResult: reqEmployeeData.total,
-      }
-    },
     async updateTableDataByFilter(currentPage = 1, pageSize = this.tablePage.pageSize) {
       this.loading = true
       this.selectedRowKeys = []
@@ -623,7 +622,8 @@ export default {
                 let max_index = 0
                 educational_experience.forEach((item, index) => {
                   if (index < educational_experience.length - 1) {
-                    max_index = item.graduation_year > educational_experience[index + 1].graduation_year ? index : index + 1
+                    max_index =
+                      item.graduation_year > educational_experience[index + 1].graduation_year ? index : index + 1
                   }
                 })
                 tableData[index].school = educational_experience[max_index].school
@@ -708,7 +708,7 @@ export default {
       } else {
         block_status = this.currentScene
       }
-      downloadAllEmployee({ block_status: block_status }).then(res => {
+      downloadAllEmployee({ block_status: block_status }).then((res) => {
         const content = res
         const blob = new Blob([content], { type: 'application/vnd.ms-excel' })
         const url = window.URL.createObjectURL(blob)
@@ -787,7 +787,8 @@ export default {
     },
     // 请求部门数据
     async reqDepartmentList(departmentId) {
-      const res = (await getDepartmentList({ department_parent_id: departmentId, block: this.currentScene })).departments
+      const res = (await getDepartmentList({ department_parent_id: departmentId, block: this.currentScene }))
+        .departments
       return this.transformDepartmentData(res)
     },
     openDepartmentModal(type) {
@@ -828,10 +829,10 @@ export default {
     },
     sortChangeEvent({ sortList }) {
       this.tableSortData = sortList
-          .map((item) => {
-            return `${item.order === 'asc' ? '' : '-'}${item.property}`
-          })
-          .join(',')
+        .map((item) => {
+          return `${item.order === 'asc' ? '' : '-'}${item.property}`
+        })
+        .join(',')
       this.updateTableDataByFilter()
     },
     filterChangeEvent({ column, property, values, datas, filterList, $event }) {
@@ -852,6 +853,137 @@ export default {
     exportSelectEvent() {
       Bus.$emit('reqExportSelectEvent')
     },
+    downloadTemplate() {
+      const data = [
+        [
+          {
+            v: '1、表头标“*”的红色字体为必填项\n2、邮箱、用户名不允许重复\n3、登录密码:密码由6-20位字母、数字组成\n4、部门:上下级部门间用"/"隔开,且从最上级部门开始,例如“深圳分公司/IT部/IT二部”。如出现相同的部门,则默认导入组织架构中顺序靠前的部门',
+            t: 's',
+            s: {
+              alignment: {
+                wrapText: true,
+                vertical: 'center',
+              },
+            },
+          },
+        ],
+        [
+          {
+            v: '*邮箱',
+            t: 's',
+            s: {
+              font: {
+                color: {
+                  rgb: 'FF0000',
+                },
+              },
+            },
+          },
+          {
+            v: '*用户名',
+            t: 's',
+            s: {
+              font: {
+                color: {
+                  rgb: 'FF0000',
+                },
+              },
+            },
+          },
+          {
+            v: '*姓名',
+            t: 's',
+            s: {
+              font: {
+                color: {
+                  rgb: 'FF0000',
+                },
+              },
+            },
+          },
+          {
+            v: '*密码',
+            t: 's',
+            s: {
+              font: {
+                color: {
+                  rgb: 'FF0000',
+                },
+              },
+            },
+          },
+          {
+            v: '性别',
+            t: 's',
+          },
+          {
+            v: '手机号',
+            t: 's',
+          },
+          {
+            v: '部门',
+            t: 's',
+          },
+          {
+            v: '岗位',
+            t: 's',
+          },
+          {
+            v: '工作地区',
+            t: 's',
+          },
+        ],
+      ]
+      downloadExcel(data, this.$t('cs.companyStructure.downloadTemplateName'))
+    },
+    customRequest({ data }, callback) {
+      excel2Array(data.file).then((res) => {
+        res = res.filter((item) => item.length)
+        callback(
+          res.slice(2).map((item) => {
+            const obj = {}
+            item.forEach((ele, index) => {
+              obj[this.common_importParamsList[index]] = ele
+            })
+            return obj
+          })
+        )
+      })
+    },
+    async importEmployee({ importData }, callback) {
+      this.allCount = importData.length
+      const _importData = importData.map((item) => {
+        const { _X_ROW_KEY, ...rest } = item
+        const keyArr = Object.keys(rest)
+        keyArr.forEach((key) => {
+          if (rest[key]) {
+            rest[key] = rest[key] + ''
+          }
+        })
+        rest.educational_experience = [
+          {
+            school: rest.school,
+            major: rest.major,
+            education: rest.education,
+            graduation_year: rest.graduation_year,
+          },
+        ]
+        delete rest.school
+        delete rest.major
+        delete rest.education
+        delete rest.graduation_year
+        const regionMap = {
+          中国大陆: 'china_mainland',
+          中国香港: 'china_hk',
+          'Chinese Mainland': 'china_mainland',
+          'HK China': 'china_hk',
+        }
+        rest.work_region = regionMap[rest.work_region] ?? res.work_region
+        return rest
+      })
+      const res = await importEmployee({ employee_list: _importData })
+      callback(res)
+    },
   },
 }
 </script>
@@ -1051,7 +1183,6 @@ export default {
               color: @primary-color;
               font-size: 12px;
             }
-
           }
         }
       }
@@ -1068,7 +1199,6 @@ export default {
           }
         }
       }
-
     }
     .ops-setting-structure-main-pagination {
       width: 100%;
diff --git a/cmdb-ui/src/views/setting/components/BatchUpload.vue b/cmdb-ui/src/views/setting/components/BatchUpload.vue
new file mode 100644
index 0000000..809e1f9
--- /dev/null
+++ b/cmdb-ui/src/views/setting/components/BatchUpload.vue
@@ -0,0 +1,243 @@
+<template>
+  <a-modal
+    :visible="visible"
+    :title="$t('cs.companyStructure.batchImport')"
+    dialogClass="ops-modal setting-structure-upload"
+    :width="800"
+    @cancel="close"
+  >
+    <div class="setting-structure-upload-steps">
+      <div
+        :class="{ 'setting-structure-upload-step': true, selected: index + 1 <= currentStep }"
+        v-for="(step, index) in stepList"
+        :key="step.value"
+      >
+        <div :class="{ 'setting-structure-upload-step-icon': true }">
+          <ops-icon :type="step.icon" />
+        </div>
+        <span>{{ step.label }}</span>
+      </div>
+    </div>
+    <template v-if="currentStep === 1">
+      <a-upload :multiple="false" :customRequest="customRequest" accept=".xlsx" :showUploadList="false">
+        <a-button :style="{ marginBottom: '20px' }" type="primary">
+          <a-icon type="upload" />{{ $t('cs.companyStructure.selectFile') }}</a-button
+        >
+      </a-upload>
+      <p>
+        <a @click="download">
+          <slot name="downloadTemplateText">{{ $t('cs.companyStructure.clickDownloadImportTemplate') }}</slot>
+        </a>
+      </p>
+    </template>
+    <div
+      :style="{
+        height: '60px',
+        display: 'flex',
+        justifyContent: 'center',
+        alignItems: 'center',
+        whiteSpace: 'pre-wrap',
+      }"
+      v-if="currentStep === 3"
+    >
+      {{ $t('cs.companyStructure.importSuccess', { allCount: allCount })
+      }}<span :style="{ color: '#2362FB' }"> {{ allCount - errorCount }} </span>{{ $t('cs.companyStructure.count') }},
+      {{ $t('cs.companyStructure.importFailed') }}<span :style="{ color: '#D81E06' }"> {{ errorCount }} </span
+      >{{ $t('cs.companyStructure.count') }}
+    </div>
+    <slot>
+      <vxe-table
+        v-if="currentStep === 2 || has_error"
+        ref="employeeTable"
+        stripe
+        :data="importData"
+        show-overflow
+        show-header-overflow
+        highlight-hover-row
+        size="small"
+        class="ops-stripe-table"
+        :max-height="400"
+        :column-config="{ resizable: true }"
+      >
+        <vxe-column field="email" :title="$t('cs.companyStructure.email')" min-width="120" fixed="left"></vxe-column>
+        <vxe-column field="username" :title="$t('cs.companyStructure.username')" min-width="80"></vxe-column>
+        <vxe-column field="nickname" :title="$t('cs.companyStructure.nickname')" min-width="80"></vxe-column>
+        <vxe-column field="password" :title="$t('cs.companyStructure.password')" min-width="80"></vxe-column>
+        <vxe-column field="sex" :title="$t('cs.companyStructure.sex')" min-width="60"></vxe-column>
+        <vxe-column field="mobile" :title="$t('cs.companyStructure.mobile')" min-width="80"></vxe-column>
+        <vxe-column
+          field="department_name"
+          :title="$t('cs.companyStructure.departmentName')"
+          min-width="80"
+        ></vxe-column>
+        <vxe-column field="position_name" :title="$t('cs.companyStructure.positionName')" min-width="80"></vxe-column>
+        <vxe-column field="work_region" :title="$t('cs.companyStructure.work_region')" min-width="80"></vxe-column>
+        <vxe-column
+          v-if="has_error"
+          field="err"
+          :title="$t('cs.companyStructure.importFailedReason')"
+          min-width="120"
+          fixed="right"
+        >
+          <template #default="{ row }">
+            <span :style="{ color: '#D81E06' }">{{ row.err }}</span>
+          </template>
+        </vxe-column>
+      </vxe-table>
+    </slot>
+    <a-space slot="footer">
+      <a-button size="small" type="primary" ghost @click="close">{{ $t('cancel') }}</a-button>
+      <a-button v-if="currentStep !== 1" size="small" type="primary" ghost @click="goPre">{{
+        $t('cs.companyStructure.prevStep')
+      }}</a-button>
+      <a-button v-if="currentStep !== 3" size="small" type="primary" @click="goNext">{{
+        $t('cs.companyStructure.nextStep')
+      }}</a-button>
+      <a-button v-else size="small" type="primary" @click="close">{{ $t('cs.companyStructure.done') }}</a-button>
+    </a-space>
+  </a-modal>
+</template>
+
+<script>
+export default {
+  name: 'BatchUpload',
+  data() {
+    const stepList = [
+      {
+        value: 1,
+        label: this.$t('cs.companyStructure.uploadFile'),
+        icon: 'icon-shidi-tianjia',
+      },
+      {
+        value: 2,
+        label: this.$t('cs.companyStructure.confirmData'),
+        icon: 'icon-shidi-yunshangchuan',
+      },
+      {
+        value: 3,
+        label: this.$t('cs.companyStructure.uploadDone'),
+        icon: 'icon-shidi-queren',
+      },
+    ]
+
+    return {
+      stepList,
+      visible: false,
+      currentStep: 1,
+      importData: [],
+      has_error: false,
+      allCount: 0,
+      errorCount: 0,
+    }
+  },
+  methods: {
+    open() {
+      this.importData = []
+      this.has_error = false
+      this.errorCount = 0
+      this.visible = true
+    },
+    close() {
+      this.currentStep = 1
+      this.visible = false
+    },
+    async goNext() {
+      if (this.currentStep === 2) {
+        this.allCount = this.importData.length
+        this.$emit('import', { importData: this.importData }, (res) => {
+          if (res.length) {
+            const errData = res.filter((item) => {
+              return item.err.length
+            })
+            console.log('err', errData)
+            this.has_error = true
+            this.errorCount = errData.length
+            this.currentStep += 1
+            this.importData = errData
+            this.$message.error(this.$t('cs.companyStructure.dataErr'))
+          } else {
+            this.currentStep += 1
+            this.$message.success(this.$t('cs.companyStructure.opSuccess'))
+          }
+          this.$emit('refresh')
+        })
+      }
+    },
+    goPre() {
+      this.has_error = false
+      this.errorCount = 0
+      this.currentStep -= 1
+    },
+    download() {
+      this.$emit('downloadTemplate')
+    },
+    customRequest(data) {
+      this.fileList = [data.file]
+      this.$emit('customRequest', { data }, (importData) => {
+        this.importData = importData
+        this.currentStep = 2
+      })
+    },
+  },
+}
+</script>
+
+<style lang="less">
+.setting-structure-upload {
+  .ant-modal-body {
+    padding: 24px 48px;
+  }
+  .setting-structure-upload-steps {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    margin-bottom: 20px;
+    .setting-structure-upload-step {
+      display: inline-block;
+      text-align: center;
+      position: relative;
+      .setting-structure-upload-step-icon {
+        width: 86px;
+        height: 86px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        background-image: url('../../../assets/icon-bg.png');
+        margin-bottom: 20px;
+        > i {
+          font-size: 40px;
+          color: #fff;
+        }
+      }
+      > span {
+        font-size: 16px;
+        font-weight: 600;
+        color: rgba(0, 0, 0, 0.5);
+      }
+    }
+    .setting-structure-upload-step:not(:first-child)::before {
+      content: '';
+      height: 2px;
+      width: 223px;
+      position: absolute;
+      background-color: #e7ecf3;
+      left: -223px;
+      top: 43px;
+      z-index: 0;
+    }
+    .selected.setting-structure-upload-step {
+      &:not(:first-child)::before {
+        background-color: #7eb0ff;
+      }
+    }
+    .selected {
+      .setting-structure-upload-step-icon {
+        background-image: url('../../../assets/icon-bg-selected.png');
+      }
+      > span {
+        color: rgba(0, 0, 0, 0.8);
+      }
+    }
+  }
+}
+</style>
diff --git a/cmdb-ui/src/views/setting/components/employeeTable.vue b/cmdb-ui/src/views/setting/components/employeeTable.vue
index ad40758..184b21b 100644
--- a/cmdb-ui/src/views/setting/components/employeeTable.vue
+++ b/cmdb-ui/src/views/setting/components/employeeTable.vue
@@ -92,7 +92,7 @@
           <span>{{ $t('cs.companyStructure.sex') }}</span>
         </span>
       </template>
-      <template #default="{row}">
+      <template #default="{ row }">
         <span v-if="row.sex === '男'">{{ $t('cs.companyStructure.male') }}</span>
         <span v-if="row.sex === '女'">{{ $t('cs.companyStructure.female') }}</span>
       </template>
@@ -175,6 +175,26 @@
         }}</span>
       </template>
     </vxe-column>
+    <vxe-column
+      field="work_region"
+      v-if="
+        checkedCols.findIndex((v) => v == 'work_region') !== -1 &&
+          attributes.findIndex((v) => v == 'work_region') !== -1
+      "
+      :title="$t('cs.companyStructure.work_region')"
+      min-width="120px"
+      key="work_region"
+    >
+      <template #header>
+        <span class="vxe-handle">
+          <OpsMoveIcon class="move-icon" />
+          <span>{{ $t('cs.companyStructure.work_region') }}</span>
+        </span>
+      </template>
+      <template #default="{ row }">
+        {{ $t(`cs.companyStructure.${row.work_region}`) }}
+      </template>
+    </vxe-column>
     <vxe-column
       field="control"
       width="100px"
@@ -197,7 +217,7 @@
           >
             <template slot="content">
               <div :style="{ maxHeight: `${windowHeight - 320}px`, overflowY: 'auto', width: '160px' }">
-                <a-checkbox-group v-model="unsbmitCheckedCols" :options="options" style="display: grid;">
+                <a-checkbox-group v-model="unsbmitCheckedCols" :options="options" style="display: grid">
                 </a-checkbox-group>
               </div>
               <div
@@ -209,22 +229,24 @@
                   justifyContent: 'flex-end',
                 }"
               >
-                <a-button :style="{ marginRight: '10px' }" size="small" @click="handleCancel">{{ $t('cancel') }}</a-button>
+                <a-button :style="{ marginRight: '10px' }" size="small" @click="handleCancel">{{
+                  $t('cancel')
+                }}</a-button>
                 <a-button size="small" @click="handleSubmit" type="primary">{{ $t('confirm') }}</a-button>
               </div>
             </template>
-            <a-icon type="control" style="cursor: pointer;" />
+            <a-icon type="control" style="cursor: pointer" />
           </a-popover>
         </template>
       </template>
       <template #default="{ row }">
         <a-space v-if="tableType === 'structure'">
-          <a><a-icon type="edit" @click="openEmployeeModal(row, 'edit')"/></a>
+          <a><a-icon type="edit" @click="openEmployeeModal(row, 'edit')" /></a>
           <a-tooltip>
             <template slot="title">
               {{ $t('cs.companyStructure.resetPassword') }}
             </template>
-            <a><a-icon type="reload" @click="openBatchModal('password', row)"/></a>
+            <a><a-icon type="reload" @click="openBatchModal('password', row)" /></a>
           </a-tooltip>
           <a-tooltip v-if="!row.block">
             <template slot="title">
@@ -305,6 +327,7 @@ export default {
       { label: this.$t('cs.companyStructure.departmentName'), value: 'department_name' },
       { label: this.$t('cs.companyStructure.positionName'), value: 'position_name' },
       { label: this.$t('cs.companyStructure.supervisor'), value: 'direct_supervisor_id' },
+      { label: this.$t('cs.companyStructure.work_region'), value: 'work_region' },
     ]
     const checkedCols = JSON.parse(localStorage.getItem('setting-table-CheckedCols')) || [
       'nickname',
@@ -315,6 +338,7 @@ export default {
       'department_name',
       'position_name',
       'direct_supervisor_id',
+      'work_region',
     ]
     return {
       filterRoleList: [],
@@ -442,7 +466,7 @@ export default {
         }
       }
       Bus.$emit('getAttributes', this.attributes)
-      this.$emit('tranferAttributes', this.attributes)
+      this.$emit('transferAttributes', this.attributes)
     },
     getIsInterInship(is_internship) {
       return this.internMap.filter((item) => item.id === is_internship)[0]['label']
@@ -541,7 +565,7 @@ export default {
         useStyle: true, // 是否导出样式
         isFooter: false, // 是否导出表尾(比如合计)
         // 过滤那个字段导出
-        columnFilterMethod: function(column, $columnIndex) {
+        columnFilterMethod: function (column, $columnIndex) {
           return !(column.$columnIndex === 0)
           // 0是复选框 不导出
         },
diff --git a/cmdb-ui/src/views/setting/lang/en.js b/cmdb-ui/src/views/setting/lang/en.js
index c19a020..0785318 100644
--- a/cmdb-ui/src/views/setting/lang/en.js
+++ b/cmdb-ui/src/views/setting/lang/en.js
@@ -19,7 +19,7 @@ const cs_en = {
     app: 'APP Authority',
     basic: 'Basic Settings',
     theme: 'Theme Settings',
-    security: 'Security Settings'
+    security: 'Security Settings',
   },
   companyInfo: {
     spanCompany: 'Description',
@@ -201,7 +201,11 @@ const cs_en = {
     createEmployee: 'Create Employee',
     editEmployee: 'Edit Employee',
     role: 'Role',
-    selectDisplayColumn: 'Please select columns to display'
+    selectDisplayColumn: 'Please select columns to display',
+    work_region: 'Work Region',
+    workRegionPlaceholder: 'Please select work region',
+    china_mainland: 'Chinese Mainland',
+    china_hk: 'HK China',
   },
   auth: {
     basic: 'Basic',
@@ -220,7 +224,7 @@ const cs_en = {
       user: 'User',
       username: 'Username',
       userPlaceholder: 'Please enter username',
-      userHelp: 'User DN: cn={},ou=users,dc=xxx,dc=com   {} will be replaced by username'
+      userHelp: 'User DN: cn={},ou=users,dc=xxx,dc=com   {} will be replaced by username',
     },
     cas: {
       server: 'Server Address',
@@ -237,10 +241,11 @@ const cs_en = {
       validateRoutePlaceholder: 'Please enter validate route',
       afterLoginRoute: 'Redirect Route',
       afterLoginRoutePlaceholder: 'Please enter redirect route',
-      userMap: 'User Attribute Mapping'
+      userMap: 'User Attribute Mapping',
     },
     autoRedirectLogin: 'Auto Redirect to Third-party Login Page',
-    autoRedirectLoginHelp: 'If disabled, a confirmation will be displayed to redirect to third-party login page. Click the Cancel button will go to the built-in login page',
+    autoRedirectLoginHelp:
+      'If disabled, a confirmation will be displayed to redirect to third-party login page. Click the Cancel button will go to the built-in login page',
     usernameOrEmail: 'Username/Email',
     usernameOrEmailPlaceholder: 'Please enter username/email',
     password: 'Password',
@@ -257,7 +262,7 @@ const cs_en = {
       userInfo: 'User Info',
       scopes: 'Scopes',
       scopesPlaceholder: 'Please enter scopes',
-    }
+    },
   },
   duty: {
     basicSetting: 'Basic Settings',
@@ -274,13 +279,13 @@ const cs_en = {
     mainDutyPeople: 'Main Duty Person',
     deputyDutyPeople: 'Deputy Duty Person',
     dutyRule: 'Duty Rule',
-    '一': 'Mon',
-    '二': 'Tue',
-    '三': 'Wed',
-    '四': 'Thu',
-    '五': 'Fri',
-    '六': 'Sat',
-    '日': 'Sun',
+    一: 'Mon',
+    二: 'Tue',
+    三: 'Wed',
+    四: 'Thu',
+    五: 'Fri',
+    六: 'Sat',
+    日: 'Sun',
     searchPlaceholder: 'Please search',
     dutyTable: 'Duty Schedule',
     dutyMember: 'Duty Member',
@@ -304,7 +309,7 @@ const cs_en = {
     offDutyReceiverPlaceholder: 'Please select off-duty receiver',
     titleLimit: 'Please enter title (20 characters)',
     remarkLimit: 'Remark 150 characters max',
-    frequencyLimit: 'Please enter duty frequency (positive integer)'
+    frequencyLimit: 'Please enter duty frequency (positive integer)',
   },
   group: {
     groupName: 'User Group',
@@ -329,7 +334,7 @@ const cs_en = {
     moreThan: 'More Than',
     lessThan: 'Less Than',
     operatorInPlaceholder: 'Separate by ;',
-    selectEmployee: 'Select Employee'
+    selectEmployee: 'Select Employee',
   },
   notice: {
     corpid: 'Corp ID',
@@ -368,7 +373,8 @@ const cs_en = {
     disableCreationOfRequestsViaEmail: 'Disable Creation of Requests Via Email',
     specifyAllowedEmails: 'Specify Allowed Emails/Domains, Separate Multiple Values By Comma',
     specifyAllowedEmailsExample: 'E.g. user@domain.com,*@domain.com',
-    specifyAllowedEmailsLimit: 'Limit cannot apply to requests already in sessions, it will aggregate to its parent ticket',
+    specifyAllowedEmailsLimit:
+      'Limit cannot apply to requests already in sessions, it will aggregate to its parent ticket',
     messageConfig: 'Message Settings',
     moveWrongMessagesToFolder: 'Move Messages to Wrong Folder',
     knowMore: 'Learn More',
@@ -438,7 +444,7 @@ const cs_en = {
     myDepartmentAndSubordinateDepartments: 'My Department And Subordinate Departments',
     test: 'Test',
     selectApp: 'Select App',
-  }
+  },
 }
 
 export default cs_en
diff --git a/cmdb-ui/src/views/setting/lang/zh.js b/cmdb-ui/src/views/setting/lang/zh.js
index def1e92..e406848 100644
--- a/cmdb-ui/src/views/setting/lang/zh.js
+++ b/cmdb-ui/src/views/setting/lang/zh.js
@@ -19,7 +19,7 @@ const cs_zh = {
     app: '应用权限',
     basic: '基础设置',
     theme: '主题配置',
-    security: '安全配置'
+    security: '安全配置',
   },
   companyInfo: {
     spanCompany: '公司描述',
@@ -201,7 +201,11 @@ const cs_zh = {
     createEmployee: '新建员工',
     editEmployee: '编辑员工',
     role: '角色',
-    selectDisplayColumn: '请选择需要展示的列'
+    selectDisplayColumn: '请选择需要展示的列',
+    work_region: '工作地区',
+    workRegionPlaceholder: '请选择工作地区',
+    china_mainland: '中国大陆',
+    china_hk: '中国香港',
   },
   auth: {
     basic: '基本',
@@ -220,7 +224,7 @@ const cs_zh = {
       user: '用户',
       username: '用户名称',
       userPlaceholder: '请输入用户名称',
-      userHelp: '用户dn: cn={},ou=users,dc=xxx,dc=com   {}会替换成用户名'
+      userHelp: '用户dn: cn={},ou=users,dc=xxx,dc=com   {}会替换成用户名',
     },
     cas: {
       server: '服务端地址',
@@ -237,7 +241,7 @@ const cs_zh = {
       validateRoutePlaceholder: '请输入验证路由',
       afterLoginRoute: '重定向路由',
       afterLoginRoutePlaceholder: '请输入重定向路由',
-      userMap: '用户属性映射'
+      userMap: '用户属性映射',
     },
     autoRedirectLogin: '自动跳转到第三方登录页',
     autoRedirectLoginHelp: '如果关闭,则会弹出跳转到第三方登录页的确认,点取消按钮会进入系统内置的登录页',
@@ -257,7 +261,7 @@ const cs_zh = {
       userInfo: '用户信息',
       scopes: '授权范围',
       scopesPlaceholder: '请输入授权范围',
-    }
+    },
   },
   duty: {
     basicSetting: '基础设置',
@@ -274,13 +278,13 @@ const cs_zh = {
     mainDutyPeople: '主值班人',
     deputyDutyPeople: '副值班人',
     dutyRule: '排班规则',
-    '一': '一',
-    '二': '二',
-    '三': '三',
-    '四': '四',
-    '五': '五',
-    '六': '六',
-    '日': '日',
+    一: '一',
+    二: '二',
+    三: '三',
+    四: '四',
+    五: '五',
+    六: '六',
+    日: '日',
     searchPlaceholder: '请查找',
     dutyTable: '值班表',
     dutyMember: '值班人员',
@@ -304,7 +308,7 @@ const cs_zh = {
     offDutyReceiverPlaceholder: '请选择非值班时间接收人',
     titleLimit: '请输入标题(20个字符)',
     remarkLimit: '备注150个字符以内',
-    frequencyLimit: '请输入值班频次(正整数)'
+    frequencyLimit: '请输入值班频次(正整数)',
   },
   group: {
     groupName: '用户分组',
@@ -329,7 +333,7 @@ const cs_zh = {
     moreThan: '大于',
     lessThan: '小于',
     operatorInPlaceholder: '以 ; 分隔',
-    selectEmployee: '选择员工'
+    selectEmployee: '选择员工',
   },
   notice: {
     corpid: '企业ID',
@@ -438,6 +442,6 @@ const cs_zh = {
     myDepartmentAndSubordinateDepartments: '本部门及下属部门',
     test: '测试',
     selectApp: '选择应用',
-  }
+  },
 }
 export default cs_zh