mirror of
				https://github.com/continew-org/continew-admin-ui.git
				synced 2025-11-04 10:57:08 +08:00 
			
		
		
		
	feat: 新增用户批量导入功能 (#23)
This commit is contained in:
		@@ -25,6 +25,15 @@ export type UserDetailResp = UserResp & {
 | 
			
		||||
  pwdResetTime?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface UserImportResp {
 | 
			
		||||
  importKey: string
 | 
			
		||||
  totalRows: number
 | 
			
		||||
  validRows: number
 | 
			
		||||
  duplicateUserRows: number
 | 
			
		||||
  duplicateEmailRows: number
 | 
			
		||||
  duplicatePhoneRows: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface UserQuery {
 | 
			
		||||
  description?: string
 | 
			
		||||
  status?: number
 | 
			
		||||
 
 | 
			
		||||
@@ -30,10 +30,25 @@ export function deleteUser(ids: string | Array<string>) {
 | 
			
		||||
 | 
			
		||||
/** @desc 导出用户 */
 | 
			
		||||
export function exportUser(query: System.UserQuery) {
 | 
			
		||||
  return http.download<any>(`${BASE_URL}/export`, query)
 | 
			
		||||
  return http.download(`${BASE_URL}/export`, query)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @desc 重置密码 */
 | 
			
		||||
export function resetUserPwd(data: any, id: string) {
 | 
			
		||||
  return http.patch(`${BASE_URL}/${id}/password`, data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @desc 下载用户导入模板 */
 | 
			
		||||
export function downloadImportUserTemplate() {
 | 
			
		||||
  return http.download(`${BASE_URL}/downloadImportUserTemplate`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @desc 解析用户导入数据 */
 | 
			
		||||
export function parseImportUser(data: FormData) {
 | 
			
		||||
  return http.post(`${BASE_URL}/parseImportUser`, data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @desc 导入用户 */
 | 
			
		||||
export function importUser(data: any) {
 | 
			
		||||
  return http.post(`${BASE_URL}/import`, data)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										196
									
								
								src/views/system/user/UserImportModal.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								src/views/system/user/UserImportModal.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,196 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <a-drawer
 | 
			
		||||
      v-model:visible="visible"
 | 
			
		||||
      title="导入用户"
 | 
			
		||||
      :mask-closable="false"
 | 
			
		||||
      :esc-to-close="false"
 | 
			
		||||
      :width="width >= 600 ? 600 : '100%'"
 | 
			
		||||
      ok-text="确认导入"
 | 
			
		||||
      cancel-text="取消导入"
 | 
			
		||||
      @before-ok="save"
 | 
			
		||||
      @close="reset"
 | 
			
		||||
  >
 | 
			
		||||
    <a-form ref="formRef" :model="form" size="large" auto-label-width>
 | 
			
		||||
      <a-alert v-if="!form.disabled" :show-icon="false" style="margin-bottom: 15px">
 | 
			
		||||
        数据导入请严格按照模板填写,格式要求和新增一致!
 | 
			
		||||
        <template #action>
 | 
			
		||||
          <a-button size="small" type="primary" @click="downloadTemplate">下载模板</a-button>
 | 
			
		||||
        </template>
 | 
			
		||||
      </a-alert>
 | 
			
		||||
      <fieldset>
 | 
			
		||||
        <legend>1.上传解析文件</legend>
 | 
			
		||||
        <div class="file-box">
 | 
			
		||||
          <a-upload draggable
 | 
			
		||||
                    :custom-request="handleUpload"
 | 
			
		||||
                    :limit="1"
 | 
			
		||||
                    :show-retry-butto="false"
 | 
			
		||||
                    :show-cancel-button="false" tip="仅支持xls、xlsx格式"
 | 
			
		||||
                    :file-list="uploadFile"
 | 
			
		||||
                    accept=".xls, .xlsx, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-if="dataResult.importKey">
 | 
			
		||||
          <div class="file-box">
 | 
			
		||||
            <a-space size="large">
 | 
			
		||||
              <a-statistic title="总计行数" :value="dataResult.totalRows" />
 | 
			
		||||
              <a-statistic title="正常行数" :value="dataResult.validRows" />
 | 
			
		||||
            </a-space>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="file-box">
 | 
			
		||||
            <a-space size="large">
 | 
			
		||||
              <a-statistic title="已存在用户" :value="dataResult.duplicateUserRows" />
 | 
			
		||||
              <a-statistic title="已存在邮箱" :value="dataResult.duplicateEmailRows" />
 | 
			
		||||
              <a-statistic title="已存在手机" :value="dataResult.duplicatePhoneRows" />
 | 
			
		||||
            </a-space>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </fieldset>
 | 
			
		||||
      <fieldset>
 | 
			
		||||
        <legend>2.导入策略</legend>
 | 
			
		||||
        <a-form-item label="用户已存在" field="duplicateUser">
 | 
			
		||||
          <a-radio-group v-model="form.duplicateUser" type="button">
 | 
			
		||||
            <a-radio :value="1">跳过该行</a-radio>
 | 
			
		||||
            <a-radio :value="3">停止导入</a-radio>
 | 
			
		||||
            <a-radio :value="2">修改数据</a-radio>
 | 
			
		||||
          </a-radio-group>
 | 
			
		||||
        </a-form-item>
 | 
			
		||||
        <a-form-item label="邮箱已存在" field="duplicateEmail">
 | 
			
		||||
          <a-radio-group v-model="form.duplicateEmail" type="button">
 | 
			
		||||
            <a-radio :value="1">跳过该行</a-radio>
 | 
			
		||||
            <a-radio :value="3">停止导入</a-radio>
 | 
			
		||||
          </a-radio-group>
 | 
			
		||||
        </a-form-item>
 | 
			
		||||
        <a-form-item label="手机已存在" field="duplicatePhone">
 | 
			
		||||
          <a-radio-group v-model="form.duplicatePhone" type="button">
 | 
			
		||||
            <a-radio :value="1">跳过该行</a-radio>
 | 
			
		||||
            <a-radio :value="3">停止导入</a-radio>
 | 
			
		||||
          </a-radio-group>
 | 
			
		||||
        </a-form-item>
 | 
			
		||||
        <a-form-item label="默认状态" field="defaultStatus">
 | 
			
		||||
          <a-switch
 | 
			
		||||
              v-model="form.defaultStatus"
 | 
			
		||||
              :checked-value="1"
 | 
			
		||||
              :unchecked-value="2"
 | 
			
		||||
              checked-text="启用"
 | 
			
		||||
              unchecked-text="禁用"
 | 
			
		||||
              type="round"
 | 
			
		||||
          />
 | 
			
		||||
        </a-form-item>
 | 
			
		||||
      </fieldset>
 | 
			
		||||
    </a-form>
 | 
			
		||||
  </a-drawer>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { type FormInstance, Message, type RequestOption } from '@arco-design/web-vue'
 | 
			
		||||
import { useWindowSize } from '@vueuse/core'
 | 
			
		||||
import { type UserImportResp, downloadImportUserTemplate, importUser, parseImportUser } from '@/apis'
 | 
			
		||||
import { useDownload, useForm } from '@/hooks'
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
  (e: 'save-success'): void
 | 
			
		||||
}>()
 | 
			
		||||
const { width } = useWindowSize()
 | 
			
		||||
 | 
			
		||||
const formRef = ref<FormInstance>()
 | 
			
		||||
const uploadFile = ref([])
 | 
			
		||||
 | 
			
		||||
const { form, resetForm } = useForm({
 | 
			
		||||
  errorPolicy: 1,
 | 
			
		||||
  duplicateUser: 1,
 | 
			
		||||
  duplicateEmail: 1,
 | 
			
		||||
  duplicatePhone: 1,
 | 
			
		||||
  defaultStatus: 1
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const dataResult = ref<UserImportResp>({
 | 
			
		||||
  importKey: '',
 | 
			
		||||
  totalRows: 0,
 | 
			
		||||
  validRows: 0,
 | 
			
		||||
  duplicateUserRows: 0,
 | 
			
		||||
  duplicateEmailRows: 0,
 | 
			
		||||
  duplicatePhoneRows: 0
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 重置
 | 
			
		||||
const reset = () => {
 | 
			
		||||
  formRef.value?.resetFields()
 | 
			
		||||
  dataResult.value.importKey = ''
 | 
			
		||||
  uploadFile.value = []
 | 
			
		||||
  resetForm()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const visible = ref(false)
 | 
			
		||||
const onImport = () => {
 | 
			
		||||
  reset()
 | 
			
		||||
  visible.value = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 下载模板
 | 
			
		||||
const downloadTemplate = () => {
 | 
			
		||||
  useDownload(() => downloadImportUserTemplate())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 上传解析导入数据
 | 
			
		||||
const handleUpload = (options: RequestOption) => {
 | 
			
		||||
  const controller = new AbortController();
 | 
			
		||||
  (async function requestWrap() {
 | 
			
		||||
    const { onProgress, onError, onSuccess, fileItem, name = 'file' } = options
 | 
			
		||||
    onProgress(20)
 | 
			
		||||
    const formData = new FormData()
 | 
			
		||||
    formData.append(name as string, fileItem.file as Blob)
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await parseImportUser(formData)
 | 
			
		||||
      dataResult.value = res.data
 | 
			
		||||
      Message.success('上传解析成功')
 | 
			
		||||
      onSuccess(res)
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      onError(error)
 | 
			
		||||
    }
 | 
			
		||||
  })()
 | 
			
		||||
  return {
 | 
			
		||||
    abort() {
 | 
			
		||||
      controller.abort()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 执行导入
 | 
			
		||||
const save = async () => {
 | 
			
		||||
  try {
 | 
			
		||||
    if (!dataResult.value.importKey) {
 | 
			
		||||
      return false
 | 
			
		||||
    }
 | 
			
		||||
    form.importKey = dataResult.value.importKey
 | 
			
		||||
    const res = await importUser(form)
 | 
			
		||||
    Message.success(`导入成功,新增${res.data.insertRows},修改${res.data.updateRows}`)
 | 
			
		||||
    emit('save-success')
 | 
			
		||||
    return true
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
defineExpose({ onImport })
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
fieldset {
 | 
			
		||||
  padding: 15px 15px 0 15px;
 | 
			
		||||
  margin-bottom: 15px;
 | 
			
		||||
  border: 1px solid var(--color-neutral-3);
 | 
			
		||||
  border-radius: 3px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fieldset legend {
 | 
			
		||||
  color: rgb(var(--gray-10));
 | 
			
		||||
  padding: 2px 5px 2px 5px;
 | 
			
		||||
  border: 1px solid var(--color-neutral-3);
 | 
			
		||||
  border-radius: 3px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.file-box {
 | 
			
		||||
  margin-bottom: 20px;
 | 
			
		||||
  margin-left: 10px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -13,21 +13,31 @@
 | 
			
		||||
      </a-col>
 | 
			
		||||
      <a-col :xs="24" :sm="16" :md="17" :lg="18" :xl="19" :xxl="20" flex="1" class="h-full ov-hidden">
 | 
			
		||||
        <GiTable row-key="id" :data="dataList" :columns="columns" :loading="loading"
 | 
			
		||||
          :scroll="{ x: '100%', y: '100%', minWidth: 1500 }" :pagination="pagination" :disabled-tools="['size']"
 | 
			
		||||
          :disabled-column-keys="['username']" @refresh="search">
 | 
			
		||||
                 :scroll="{ x: '100%', y: '100%', minWidth: 1500 }" :pagination="pagination" :disabled-tools="['size']"
 | 
			
		||||
                 :disabled-column-keys="['username']" @refresh="search">
 | 
			
		||||
          <template #custom-left>
 | 
			
		||||
            <a-input v-model="queryForm.description" placeholder="请输入关键词" allow-clear @change="search">
 | 
			
		||||
              <template #prefix><icon-search /></template>
 | 
			
		||||
              <template #prefix>
 | 
			
		||||
                <icon-search />
 | 
			
		||||
              </template>
 | 
			
		||||
            </a-input>
 | 
			
		||||
            <a-select v-model="queryForm.status" :options="DisEnableStatusList" placeholder="请选择状态" allow-clear
 | 
			
		||||
              style="width: 150px" @change="search" />
 | 
			
		||||
                      style="width: 150px" @change="search" />
 | 
			
		||||
            <a-button @click="reset">重置</a-button>
 | 
			
		||||
          </template>
 | 
			
		||||
          <template #custom-right>
 | 
			
		||||
            <a-button v-permission="['system:user:add']" type="primary" @click="onAdd">
 | 
			
		||||
              <template #icon><icon-plus /></template>
 | 
			
		||||
              <template #icon>
 | 
			
		||||
                <icon-plus />
 | 
			
		||||
              </template>
 | 
			
		||||
              <span>新增</span>
 | 
			
		||||
            </a-button>
 | 
			
		||||
            <a-button v-permission="['system:user:import']" @click="onImport">
 | 
			
		||||
              <template #icon>
 | 
			
		||||
                <icon-upload />
 | 
			
		||||
              </template>
 | 
			
		||||
              <span>导入</span>
 | 
			
		||||
            </a-button>
 | 
			
		||||
            <a-tooltip content="导出">
 | 
			
		||||
              <a-button v-permission="['system:user:export']" class="gi_hover_btn-border" @click="onExport">
 | 
			
		||||
                <template #icon>
 | 
			
		||||
@@ -38,7 +48,7 @@
 | 
			
		||||
          </template>
 | 
			
		||||
          <template #username="{ record }">
 | 
			
		||||
            <GiCellAvatar :avatar="getAvatar(record.avatar, record.gender)" :name="record.username" is-link
 | 
			
		||||
              @click="onDetail(record)" />
 | 
			
		||||
                          @click="onDetail(record)" />
 | 
			
		||||
          </template>
 | 
			
		||||
          <template #gender="{ record }">
 | 
			
		||||
            <GiCellGender :gender="record.gender" />
 | 
			
		||||
@@ -57,7 +67,8 @@
 | 
			
		||||
            <a-space>
 | 
			
		||||
              <a-link v-permission="['system:user:update']" @click="onUpdate(record)">修改</a-link>
 | 
			
		||||
              <a-link v-permission="['system:user:delete']" status="danger"
 | 
			
		||||
                :title="record.isSystem ? '系统内置数据不能删除' : '删除'" :disabled="record.disabled" @click="onDelete(record)">
 | 
			
		||||
                      :title="record.isSystem ? '系统内置数据不能删除' : '删除'" :disabled="record.disabled"
 | 
			
		||||
                      @click="onDelete(record)">
 | 
			
		||||
                删除
 | 
			
		||||
              </a-link>
 | 
			
		||||
              <a-dropdown>
 | 
			
		||||
@@ -73,6 +84,7 @@
 | 
			
		||||
    </a-row>
 | 
			
		||||
 | 
			
		||||
    <UserAddModal ref="UserAddModalRef" @save-success="search" />
 | 
			
		||||
    <UserImportModal ref="UserImportModalRef" @save-success="search" />
 | 
			
		||||
    <UserDetailDrawer ref="UserDetailDrawerRef" />
 | 
			
		||||
    <UserResetPwdModal ref="UserResetPwdModalRef" />
 | 
			
		||||
  </div>
 | 
			
		||||
@@ -81,6 +93,7 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import DeptTree from './dept/index.vue'
 | 
			
		||||
import UserAddModal from './UserAddModal.vue'
 | 
			
		||||
import UserImportModal from './UserImportModal.vue'
 | 
			
		||||
import UserDetailDrawer from './UserDetailDrawer.vue'
 | 
			
		||||
import UserResetPwdModal from './UserResetPwdModal.vue'
 | 
			
		||||
import { type UserQuery, type UserResp, deleteUser, exportUser, listUser } from '@/apis'
 | 
			
		||||
@@ -176,6 +189,12 @@ const onAdd = () => {
 | 
			
		||||
  UserAddModalRef.value?.onAdd()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const UserImportModalRef = ref<InstanceType<typeof UserImportModal>>()
 | 
			
		||||
// 导入
 | 
			
		||||
const onImport = () => {
 | 
			
		||||
  UserImportModalRef.value?.onImport()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 修改
 | 
			
		||||
const onUpdate = (item: UserResp) => {
 | 
			
		||||
  UserAddModalRef.value?.onUpdate(item.id)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user