Prechádzať zdrojové kódy

Merge remote-tracking branch 'origin/stable' into stable

liyuhui_ex 4 dní pred
rodič
commit
1e2d69ee61

+ 5 - 0
yudao-ui-admin-vue3/src/api/system/dept/index.ts

@@ -41,3 +41,8 @@ export const updateDept = async (params: DeptVO) => {
 export const deleteDept = async (id: number) => {
   return await request.delete({ url: '/system/dept/delete?id=' + id })
 }
+
+// 下载部门导入模板
+export const importDeptTemplate = () => {
+  return request.download({ url: '/system/dept/get-import-template' })
+}

+ 5 - 0
yudao-ui-admin-vue3/src/api/system/user/index.ts

@@ -61,6 +61,11 @@ export const importUserTemplate = () => {
   return request.download({ url: '/system/user/get-import-template' })
 }
 
+// 下载用户角色关联导入模板
+export const importUserRoleTemplate = () => {
+  return request.download({ url: '/system/user/get-import-role-template' })
+}
+
 // 用户密码重置
 export const resetUserPwd = (id: number, password: string) => {
   const data = {

+ 8 - 4
yudao-ui-admin-vue3/src/components/DynamicReport/SpreadViewer.vue

@@ -266,6 +266,8 @@ const initPreview = async () => {
                 // sheetData[i.colCode] = null;
               } else if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
                 try { sheetData[i.colCode] = JSON.parse(val); } catch { sheetData[i.colCode] = val; }
+              } else if (val == 'false' || val == 'true') {
+                sheetData[i.colCode] = val == 'true' ? 'true' : null;
               } else {
                 sheetData[i.colCode] = val;
               }
@@ -326,6 +328,8 @@ const initPreview = async () => {
               } catch {
                 sheetData[i.colCode] = val;
               }
+            } else if (val == 'false' || val == 'true') {
+              sheetData[i.colCode] = val == 'true' ? 'true' : null;
             } else {
               sheetData[i.colCode] = val;
             }
@@ -487,10 +491,10 @@ const selectWingdings = (item) => {
   
   const { sheet, row, col } = currentCell.value
   const cell = sheet.getCell(row, col)
-  if (!cell.allowEditInCell()){
-    wingdingsDialogVisible.value = false
-    return;
-  }
+  // if (!cell.allowEditInCell()){
+  //   wingdingsDialogVisible.value = false
+  //   return;
+  // }
   // 获取当前单元格的值
   const currentValue = cell.value() || ''
   

+ 4 - 4
yudao-ui-admin-vue3/src/views/pressure2/NonTaxBilling/detail.vue

@@ -101,13 +101,13 @@
             </el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item label="款人电话" prop="contactPhone" class="unit-form-item">
-              <el-input v-model="formData.contactPhone" placeholder="请输入款人电话" />
+            <el-form-item label="款人电话" prop="contactPhone" class="unit-form-item">
+              <el-input v-model="formData.contactPhone" placeholder="请输入款人电话" />
             </el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item label="款人邮箱" prop="email" class="unit-form-item">
-              <el-input v-model="formData.email" type="email" placeholder="请输入款人邮箱" />
+            <el-form-item label="款人邮箱" prop="email" class="unit-form-item">
+              <el-input v-model="formData.email" type="email" placeholder="请输入款人邮箱" />
             </el-form-item>
           </el-col>
         </el-row>

+ 29 - 9
yudao-ui-admin-vue3/src/views/pressure2/boilerchecker/components/StatusOperationPanel.vue

@@ -883,7 +883,7 @@ import RectificationUpload from '@/views/pressure2/components/RectificationUploa
 import { computed, nextTick, reactive, ref, watch } from 'vue'
 import { useRouter, useRoute } from 'vue-router'
 import { InfoFilled, View, Back, Right, WarningFilled, VideoPlay } from '@element-plus/icons-vue'
-import { dayjs, ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'
+import {dayjs, ElLoading, ElMessage, ElMessageBox, FormInstance, FormRules} from 'element-plus'
 const CustomDialog = defineAsyncComponent(() => import('@/components/CustomDialog/index.vue'))
 import VuePdfEmbed from 'vue-pdf-embed'
 import { getReportAllowedApplyModify, getReportCheckPdf } from '@/api/laboratory/functional/report'
@@ -1857,15 +1857,35 @@ const handleSubmitAudit = async () => {
     approvalUserVisible.value = true
 
   } else {
-    let res = await UserApi.getApprovalDetail({}) // 判断是否有审批信息
+    // 确定审核人:使用当前用户,否则使用默认审核人
+    // const approveId = userStore.user.id
+    const approveId = 'b5369aeb73954430eef53a9c8b7586ee'
 
-    if (res && res.approveUser) {
-      form.value.recheckUser = res.approveUser
-    }
-    
-    schemaFlag.value = 'audit'
-    isShowAuditDialog.value = true
-    // isShowReportAuditDialog.value = true
+    ElMessageBox.confirm('确定提交审核吗?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    }).then(async () => {
+      const loadingInstance = ElLoading.service({
+        fullscreen: true,
+        text: '正在提交审核...'
+      })
+      try {
+        const submitResult = await BoilerTaskOrderApi.submitReportAudit({
+          id: templateParams.value?.id,
+          approveId
+        })
+        if (submitResult) {
+          ElMessage.success('提交审核成功!')
+          selectNextItem([props.selectedItem])
+          emit('template-confirm')
+        }
+      } finally {
+        loadingInstance.close()
+      }
+    }).catch(() => {
+      console.log('用户取消提交审核')
+    })
   }
 }
 

+ 13 - 4
yudao-ui-admin-vue3/src/views/pressure2/boilerchecker/taskDetail.vue

@@ -13,10 +13,19 @@
       </div>
       <el-divider direction="vertical" class="mx-10px" />
       <div id="teleport-btn" ref="teleportBtnRef" class="teleport-btn"></div>
-      <el-button class="ml-10px" type="primary" size="small" plain @click="handleToggleFullscreen">
-        {{ isFullscreen ? '退出全屏' : '全屏' }}
-      </el-button>
-      <div class="detail-header-back"><el-button type="default" plain @click="() => handleBack()">返回</el-button></div>
+<!--      <el-button class="ml-10px" type="primary" size="small" plain @click="handleToggleFullscreen">-->
+<!--        {{ isFullscreen ? '退出全屏' : '全屏' }}-->
+<!--      </el-button>-->
+      <div class="detail-header-back">
+        <el-button circle type="default" plain @click="handleToggleFullscreen">
+          <Icon
+            :icon="isFullScreen ? 'radix-icons:exit-full-screen' : 'radix-icons:enter-full-screen'"
+            color="var(--el-color-info)"
+            hover-color="var(--el-color-primary)"
+          />
+        </el-button>
+        <el-button type="default" plain @click="() => handleBack()">返回</el-button>
+      </div>
     </div>
     <div class="task-detail-container flex">
       <!-- 左侧:检验项目列表 -->

+ 9 - 0
yudao-ui-admin-vue3/src/views/pressure2/boilertaskorder/components/ServiceRecordList.vue

@@ -509,6 +509,15 @@ const updateServiceOrderDialogFormData = async () => {
   }
 }
 const serviceOrderSubmitForm = async (type: string) => {
+
+  try {
+    await editSpreadRecordRef.value?.handleSave()
+  } catch (error) {
+    console.error('保存数据失败:', error)
+    ElMessage.error('保存数据失败,请重试')
+    return
+  }
+
   formType.value = type
   serviceOrderDialogVisible.value = true
 }

+ 5 - 5
yudao-ui-admin-vue3/src/views/pressure2/expenseReminderManagement/index.vue

@@ -329,7 +329,7 @@ const columns = ref<SmartTableColumn[]>([
     width: '50px',
     fieldProps: {
       align: 'center',
-      selectable: (row: Recordable) => row.paymentStatus === 0 && row.payerContact
+      selectable: (row: Recordable) => row.noTaxPaymentStatus === 0 && row.payerContact
     }
   },
   {
@@ -343,7 +343,7 @@ const columns = ref<SmartTableColumn[]>([
     render: (row, _value) => {
       return (
         <>
-          {row.paymentStatus === 0 && (
+          {row.noTaxPaymentStatus === 0 && (
             <>
               <el-button type="primary" link onClick={() => handleReminder(row)}>
                 催缴
@@ -361,7 +361,7 @@ const columns = ref<SmartTableColumn[]>([
     }
   },
   {
-    prop: 'paymentStatus',
+    prop: 'noTaxPaymentStatus',
     label: '结算状态',
     fieldProps: {
       align: 'center',
@@ -758,7 +758,7 @@ const notPaymentStatusNum = computed(() => {
   }, 0)
 })
 const getNotPaymentStatus = () => {
-  FetchApis.getList({ paymentStatus: 0, feeType: 100 }).then((res) => {
+  FetchApis.getList({ noTaxPaymentStatus: 0, feeType: 100 }).then((res) => {
     notPaymentStatusCount.value = res.total || 0
   })
 }
@@ -1112,7 +1112,7 @@ onMounted(() => {
   smartTableRef.value?.setSearchForm(searchFormData.value)
   const routerQuery = route.query || {}
   if(Object.entries(routerQuery).length){
-    smartTableRef.value?.setSearchForm({ paymentStatus: 0, ...routerQuery })
+    smartTableRef.value?.setSearchForm({ noTaxPaymentStatus: 0, ...routerQuery })
   }
   getList()
 })

+ 10 - 1
yudao-ui-admin-vue3/src/views/pressure2/orderConfirm/pipeDetail.vue

@@ -1509,10 +1509,19 @@ const handleRejectConfirm = async (type) => {
     return
   }
   const equipIds = selectedRows.value.map(e => e.id)
+  let detailEquipRows = selectedDetailRows.value
+  // 勾选了主表没有勾选子表,将子表全部加到detailEquipRows
+  for (let i = 0; i < equipIds.length; i++) {
+    const equipId = equipIds[i]
+    if (!detailEquipRows.find(item => item.equipPipeId === equipId)) {
+      const detailRows = await PipeAppointmentConfirmOrderApi.getPipeEquipmentDetailListByPipeEquipmentId(orderDetail.value.id, equipId)
+      detailEquipRows = detailEquipRows.concat(detailRows.list)
+    }
+  }
   const submitData = {
     equipIds,
     orderId: orderDetail.value?.id,
-    detailEquipRows: selectedDetailRows.value,
+    detailEquipRows,
     type,
     reasonDict: rejectForm.value.reasonDict,
     reason: rejectForm.value.reason,

+ 30 - 9
yudao-ui-admin-vue3/src/views/pressure2/pipechecker/components/StatusOperationPanel.vue

@@ -886,7 +886,7 @@ import RectificationUpload from '@/views/pressure2/components/RectificationUploa
 import {computed, defineAsyncComponent, nextTick, reactive, ref, watch} from 'vue'
 import {useRoute} from 'vue-router'
 import { InfoFilled, View, Back, Right, WarningFilled, VideoPlay } from '@element-plus/icons-vue'
-import {dayjs, ElMessage, ElMessageBox, FormInstance, FormRules} from 'element-plus'
+import {dayjs, ElLoading, ElMessage, ElMessageBox, FormInstance, FormRules} from 'element-plus'
 import * as UserApi from '@/api/system/user'
 import {
   PressureCheckerMyTaskStatus,
@@ -1826,15 +1826,36 @@ const handleSubmitAudit = async () => {
     approvalUserVisible.value = true
 
   }  else {
-    let res = await UserApi.getApprovalDetail({}) // 判断是否有审批信息
 
-    if (res && res.approveUser) {
-      form.value.recheckUser = res.approveUser
-    }
-    
-    schemaFlag.value = 'audit'
-    isShowAuditDialog.value = true
-    // isShowReportAuditDialog.value = true
+    // 确定审核人:使用当前用户,否则使用默认审核人
+    // const approveId = userStore.user.id
+    const approveId = 'b5369aeb73954430eef53a9c8b7586ee'
+
+    ElMessageBox.confirm('确定提交审核吗?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    }).then(async () => {
+      const loadingInstance = ElLoading.service({
+        fullscreen: true,
+        text: '正在提交审核...'
+      })
+      try {
+        const submitResult = await PipeTaskOrderApi.submitReportAudit({
+          id: templateParams.value?.id,
+          approveId
+        })
+        if (submitResult) {
+          ElMessage.success('提交审核成功!')
+          selectNextItem([props.selectedItem])
+          emit('template-confirm')
+        }
+      } finally {
+        loadingInstance.close()
+      }
+    }).catch(() => {
+      console.log('用户取消提交审核')
+    })
   }
 }
 

+ 11 - 3
yudao-ui-admin-vue3/src/views/pressure2/pipechecker/taskDetail.vue

@@ -11,10 +11,18 @@
       </div>
       <el-divider direction="vertical" class="mx-10px" />
       <div id="teleport-btn" ref="teleportBtnRef" class="teleport-btn"></div>
-      <el-button class="ml-10px" type="primary" size="small" plain @click="handleToggleFullscreen">
-        {{ isFullscreen ? '退出全屏' : '全屏' }}
+<!--      <el-button class="ml-10px" type="primary" size="small" plain @click="handleToggleFullscreen">-->
+<!--        {{ isFullscreen ? '退出全屏' : '全屏' }}-->
+<!--      </el-button>-->
+      <div class="detail-header-back">
+        <el-button circle type="default" plain @click="handleToggleFullscreen">
+        <Icon
+          :icon="isFullScreen ? 'radix-icons:exit-full-screen' : 'radix-icons:enter-full-screen'"
+          color="var(--el-color-info)"
+          hover-color="var(--el-color-primary)"
+        />
       </el-button>
-      <div class="detail-header-back"><el-button type="default" plain @click="() => handleBack()">返回</el-button></div>
+        <el-button type="default" plain @click="() => handleBack()">返回</el-button></div>
     </div>
     <div class="task-detail-container flex">
       <!-- 左侧:检验项目列表 -->

+ 9 - 0
yudao-ui-admin-vue3/src/views/pressure2/pipetaskorder/components/ServiceRecordList.vue

@@ -439,6 +439,15 @@ const updateServiceOrderDialogFormData = async () => {
   }
 }
 const serviceOrderSubmitForm = async (type: string) => {
+
+  try {
+    await editSpreadRecordRef.value?.handleSave()
+  } catch (error) {
+    console.error('保存数据失败:', error)
+    ElMessage.error('保存数据失败,请重试')
+    return
+  }
+
   formType.value = type
   serviceOrderDialogVisible.value = true
 }

+ 35 - 1
yudao-ui-admin-vue3/src/views/pressure2/schedule/index.vue

@@ -54,7 +54,11 @@
         <el-icon><Edit /></el-icon>
         <span>修改计划</span>
       </div>
-      <div class="context-menu-item" @click="handleCancelPlan">
+      <div 
+        class="context-menu-item" 
+        :class="{ 'disabled': (currentTask as BoilerTaskItem)?.status !== 100 }"
+        @click="handleCancelPlan"
+      >
         <el-icon><Delete /></el-icon>
         <span>取消计划</span>
       </div>
@@ -153,6 +157,7 @@
                         class="delete-btn"
                         type="danger"
                         link
+                        v-if="task.status === 100"
                         @click.stop="handleDeleteTask(task)"
                       >
                         <el-icon><Delete /></el-icon>
@@ -573,6 +578,11 @@ onUnmounted(() => {
 
 // 处理单个计划删除
 const handleDeleteTask = (task: BoilerTaskItem) => {
+  // 检查状态是否为已排期(100)
+  if (task.status !== 100) {
+    ElMessage.warning('仅允许删除"已排期"状态的记录')
+    return
+  }
   handleDeleteSelected([task])
 }
 
@@ -581,6 +591,13 @@ const handleDeleteSelected = async (tasksToDelete?: BoilerTaskItem[]) => {
   const tasks = tasksToDelete || selectedTasks.value
   if (tasks.length === 0) return
   
+  // 过滤出非已排期状态的任务
+  const nonScheduledTasks = tasks.filter(task => task.status !== 100)
+  if (nonScheduledTasks.length > 0) {
+    ElMessage.warning('仅允许删除"已排期"状态的记录')
+    return
+  }
+  
   try {
     await ElMessageBox.confirm(
       `确认删除选中的计划吗?`,
@@ -644,6 +661,13 @@ const handleEditPlan = () => {
 // 取消计划
 const handleCancelPlan = () => {
   if (currentTask.value) {
+    const task = currentTask.value as BoilerTaskItem
+    // 检查状态是否为已排期(100)
+    if (task.status !== 100) {
+      ElMessage.warning('仅允许删除"已排期"状态的记录')
+      contextMenuVisible.value = false
+      return
+    }
     handleDeleteSelected([currentTask.value])
     contextMenuVisible.value = false
   }
@@ -1053,6 +1077,16 @@ const handleSubmitPlan = async () => {
       background-color: var(--el-fill-color-light);
     }
     
+    &.disabled {
+      opacity: 0.5;
+      cursor: not-allowed;
+      color: var(--el-text-color-placeholder);
+      
+      &:hover {
+        background-color: transparent;
+      }
+    }
+    
     .el-icon {
       font-size: 16px;
     }

+ 35 - 1
yudao-ui-admin-vue3/src/views/pressure2/schedule/pipeindex.vue

@@ -128,7 +128,11 @@
         <el-icon><Edit /></el-icon>
         <span>修改计划</span>
       </div>
-      <div class="context-menu-item" @click="handleCancelPlan">
+      <div 
+        class="context-menu-item" 
+        :class="{ 'disabled': (currentTask as PipeTaskItem)?.status !== 100 }"
+        @click="handleCancelPlan"
+      >
         <el-icon><Delete /></el-icon>
         <span>取消计划</span>
       </div>
@@ -228,6 +232,7 @@
                         class="delete-btn"
                         type="danger"
                         link
+                        v-if="task.status === 100"
                         @click.stop="handleDeleteTask(task)"
                       >
                         <el-icon><Delete /></el-icon>
@@ -678,6 +683,11 @@ onUnmounted(() => {
 
 // 处理单个计划删除
 const handleDeleteTask = (task: PipeTaskItem) => {
+  // 检查状态是否为已排期(100)
+  if (task.status !== 100) {
+    ElMessage.warning('仅允许删除"已排期"状态的记录')
+    return
+  }
   handleDeleteSelected([task])
 }
 
@@ -686,6 +696,13 @@ const handleDeleteSelected = async (tasksToDelete?: PipeTaskItem[]) => {
   const tasks = tasksToDelete || selectedTasks.value
   if (tasks.length === 0) return
   
+  // 过滤出非已排期状态的任务
+  const nonScheduledTasks = tasks.filter(task => task.status !== 100)
+  if (nonScheduledTasks.length > 0) {
+    ElMessage.warning('仅允许删除"已排期"状态的记录')
+    return
+  }
+  
   try {
     await ElMessageBox.confirm(
       `确认删除选中的计划吗?`,
@@ -749,6 +766,13 @@ const handleEditPlan = () => {
 // 取消计划
 const handleCancelPlan = () => {
   if (currentTask.value) {
+    const task = currentTask.value as PipeTaskItem
+    // 检查状态是否为已排期(100)
+    if (task.status !== 100) {
+      ElMessage.warning('仅允许删除"已排期"状态的记录')
+      contextMenuVisible.value = false
+      return
+    }
     handleDeleteSelected([currentTask.value])
     contextMenuVisible.value = false
   }
@@ -1178,6 +1202,16 @@ watch(() => queryParams.value.type, (newType) => {
       background-color: var(--el-fill-color-light);
     }
     
+    &.disabled {
+      opacity: 0.5;
+      cursor: not-allowed;
+      color: var(--el-text-color-placeholder);
+      
+      &:hover {
+        background-color: transparent;
+      }
+    }
+    
     .el-icon {
       font-size: 16px;
     }

+ 123 - 0
yudao-ui-admin-vue3/src/views/system/dept/DeptImportForm.vue

@@ -0,0 +1,123 @@
+<template>
+  <Dialog v-model="dialogVisible" title="部门导入" width="400">
+    <el-upload
+      ref="uploadRef"
+      v-model:file-list="fileList"
+      :action="importUrl"
+      :auto-upload="false"
+      :disabled="formLoading"
+      :headers="uploadHeaders"
+      :limit="1"
+      :on-error="submitFormError"
+      :on-exceed="handleExceed"
+      :on-success="submitFormSuccess"
+      accept=".xlsx, .xls"
+      drag
+    >
+      <Icon icon="ep:upload" />
+      <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+      <template #tip>
+        <div class="el-upload__tip text-center">
+          <span>仅允许导入 xls、xlsx 格式文件。</span>
+          <el-link
+            :underline="false"
+            style="font-size: 12px; vertical-align: baseline"
+            type="primary"
+            @click="importTemplate"
+          >
+            下载模板
+          </el-link>
+        </div>
+      </template>
+    </el-upload>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import * as DeptApi from '@/api/system/dept'
+import { getAccessToken, getTenantId } from '@/utils/auth'
+import download from '@/utils/download'
+
+defineOptions({ name: 'SystemDeptImportForm' })
+
+const message = useMessage()
+
+const dialogVisible = ref(false)
+const formLoading = ref(false)
+const uploadRef = ref()
+const importUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/system/dept/import'
+const uploadHeaders = ref()
+const fileList = ref([])
+
+/** 打开弹窗 */
+const open = () => {
+  dialogVisible.value = true
+  fileList.value = []
+  resetForm()
+}
+defineExpose({ open })
+
+/** 提交表单 */
+const submitForm = async () => {
+  if (fileList.value.length == 0) {
+    message.error('请上传文件')
+    return
+  }
+  uploadHeaders.value = {
+    Authorization: 'Bearer ' + getAccessToken(),
+    'tenant-id': getTenantId()
+  }
+  formLoading.value = true
+  uploadRef.value!.submit()
+}
+
+/** 文件上传成功 */
+const emits = defineEmits(['success'])
+const submitFormSuccess = (response: any) => {
+  if (response.code !== 0) {
+    message.error(response.msg)
+    formLoading.value = false
+    return
+  }
+  const data = response.data
+  let text = '创建成功数量:' + data.createDeptNames.length + ';'
+  for (const name of data.createDeptNames) {
+    text += '< ' + name + ' >'
+  }
+  text += '导入失败数量:' + Object.keys(data.failureDeptNames).length + ';'
+  for (const name in data.failureDeptNames) {
+    text += '< ' + name + ': ' + data.failureDeptNames[name] + ' >'
+  }
+  message.alert(text)
+  formLoading.value = false
+  dialogVisible.value = false
+  emits('success')
+}
+
+/** 上传错误提示 */
+const submitFormError = (): void => {
+  message.error('上传失败,请您重新上传!')
+  formLoading.value = false
+}
+
+/** 重置表单 */
+const resetForm = async (): Promise<void> => {
+  formLoading.value = false
+  await nextTick()
+  uploadRef.value?.clearFiles()
+}
+
+/** 文件数超出提示 */
+const handleExceed = (): void => {
+  message.error('最多只能上传一个文件!')
+}
+
+/** 下载模板操作 */
+const importTemplate = async () => {
+  const res = await DeptApi.importDeptTemplate()
+  download.excel(res, '部门导入模版.xls')
+}
+</script>

+ 12 - 0
yudao-ui-admin-vue3/src/views/system/dept/index.vue

@@ -43,6 +43,9 @@
         >
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
+        <el-button type="success" plain @click="handleImport">
+          <Icon icon="ep:upload" class="mr-5px" /> 导入
+        </el-button>
         <el-button type="danger" plain @click="toggleExpandAll">
           <Icon icon="ep:sort" class="mr-5px" /> 展开/折叠
         </el-button>
@@ -103,6 +106,8 @@
 
   <!-- 表单弹窗:添加/修改 -->
   <DeptForm ref="formRef" @success="getList" />
+  <!-- 导入弹窗 -->
+  <DeptImportForm ref="importFormRef" @success="getList" />
 </template>
 <script lang="ts" setup>
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
@@ -110,6 +115,7 @@ import { dateFormatter } from '@/utils/formatTime'
 import { handleTree } from '@/utils/tree'
 import * as DeptApi from '@/api/system/dept'
 import DeptForm from './DeptForm.vue'
+import DeptImportForm from './DeptImportForm.vue'
 import * as UserApi from '@/api/system/user'
 
 defineOptions({ name: 'SystemDept' })
@@ -168,6 +174,12 @@ const openForm = (type: string, id?: number) => {
   formRef.value.open(type, id)
 }
 
+/** 导入操作 */
+const importFormRef = ref()
+const handleImport = () => {
+  importFormRef.value.open()
+}
+
 /** 删除按钮操作 */
 const handleDelete = async (id: number) => {
   try {

+ 123 - 0
yudao-ui-admin-vue3/src/views/system/user/UserRoleImportForm.vue

@@ -0,0 +1,123 @@
+<template>
+  <Dialog v-model="dialogVisible" title="导入用户角色关联" width="400">
+    <el-upload
+      ref="uploadRef"
+      v-model:file-list="fileList"
+      :action="importUrl"
+      :auto-upload="false"
+      :disabled="formLoading"
+      :headers="uploadHeaders"
+      :limit="1"
+      :on-error="submitFormError"
+      :on-exceed="handleExceed"
+      :on-success="submitFormSuccess"
+      accept=".xlsx, .xls"
+      drag
+    >
+      <Icon icon="ep:upload" />
+      <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+      <template #tip>
+        <div class="el-upload__tip text-center">
+          <span>仅允许导入 xls、xlsx 格式文件。</span>
+          <el-link
+            :underline="false"
+            style="font-size: 12px; vertical-align: baseline"
+            type="primary"
+            @click="importTemplate"
+          >
+            下载模板
+          </el-link>
+        </div>
+      </template>
+    </el-upload>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import * as UserApi from '@/api/system/user'
+import { getAccessToken, getTenantId } from '@/utils/auth'
+import download from '@/utils/download'
+
+defineOptions({ name: 'SystemUserRoleImportForm' })
+
+const message = useMessage()
+
+const dialogVisible = ref(false)
+const formLoading = ref(false)
+const uploadRef = ref()
+const importUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/system/user/import-role'
+const uploadHeaders = ref()
+const fileList = ref([])
+
+/** 打开弹窗 */
+const open = () => {
+  dialogVisible.value = true
+  fileList.value = []
+  resetForm()
+}
+defineExpose({ open })
+
+/** 提交表单 */
+const submitForm = async () => {
+  if (fileList.value.length == 0) {
+    message.error('请上传文件')
+    return
+  }
+  uploadHeaders.value = {
+    Authorization: 'Bearer ' + getAccessToken(),
+    'tenant-id': getTenantId()
+  }
+  formLoading.value = true
+  uploadRef.value!.submit()
+}
+
+/** 文件上传成功 */
+const emits = defineEmits(['success'])
+const submitFormSuccess = (response: any) => {
+  if (response.code !== 0) {
+    message.error(response.msg)
+    formLoading.value = false
+    return
+  }
+  const data = response.data
+  let text = '关联成功数量:' + data.createUsernames.length + ';'
+  for (const name of data.createUsernames) {
+    text += '< ' + name + ' >'
+  }
+  text += '关联失败数量:' + Object.keys(data.failureUsernames).length + ';'
+  for (const name in data.failureUsernames) {
+    text += '< ' + name + ': ' + data.failureUsernames[name] + ' >'
+  }
+  message.alert(text)
+  formLoading.value = false
+  dialogVisible.value = false
+  emits('success')
+}
+
+/** 上传错误提示 */
+const submitFormError = (): void => {
+  message.error('上传失败,请您重新上传!')
+  formLoading.value = false
+}
+
+/** 重置表单 */
+const resetForm = async (): Promise<void> => {
+  formLoading.value = false
+  await nextTick()
+  uploadRef.value?.clearFiles()
+}
+
+/** 文件数超出提示 */
+const handleExceed = (): void => {
+  message.error('最多只能上传一个文件!')
+}
+
+/** 下载模板操作 */
+const importTemplate = async () => {
+  const res = await UserApi.importUserRoleTemplate()
+  download.excel(res, '用户角色关联导入模版.xls')
+}
+</script>

+ 18 - 1
yudao-ui-admin-vue3/src/views/system/user/index.vue

@@ -106,7 +106,15 @@
               @click="handleImport"
               v-hasPermi="['system:user:import']"
             >
-              <Icon icon="ep:upload" /> 导入
+              <Icon icon="ep:upload" /> 导入用户
+            </el-button>
+            <el-button
+              type="warning"
+              plain
+              @click="handleImportRole"
+              v-hasPermi="['system:user:import']"
+            >
+              <Icon icon="ep:upload" /> 导入角色关联
             </el-button>
             <el-button
               type="success"
@@ -221,6 +229,8 @@
   <UserForm ref="formRef" @success="getList" />
   <!-- 用户导入对话框 -->
   <UserImportForm ref="importFormRef" @success="getList" />
+  <!-- 用户角色关联导入对话框 -->
+  <UserRoleImportForm ref="importRoleFormRef" @success="getList" />
   <!-- 分配角色 -->
   <UserAssignRoleForm ref="assignRoleFormRef" @success="getList" />
 </template>
@@ -234,6 +244,7 @@ import * as UserApi from '@/api/system/user'
 import * as RoleApi from '@/api/system/role'
 import UserForm from './UserForm.vue'
 import UserImportForm from './UserImportForm.vue'
+import UserRoleImportForm from './UserRoleImportForm.vue'
 import UserAssignRoleForm from './UserAssignRoleForm.vue'
 import DeptTree from './DeptTree.vue'
 import SparkMD5 from 'spark-md5'
@@ -303,6 +314,12 @@ const handleImport = () => {
   importFormRef.value.open()
 }
 
+/** 用户角色关联导入 */
+const importRoleFormRef = ref()
+const handleImportRole = () => {
+  importRoleFormRef.value.open()
+}
+
 /** 修改用户状态 */
 const handleStatusChange = async (row: UserApi.UserVO) => {
   try {