mirror of
				https://github.com/continew-org/continew-admin-ui.git
				synced 2025-10-31 10:57:10 +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
	 kils
					kils