Sfoglia il codice sorgente

✨ feat: 按部门手动生成运行记录并优化操作交互

Zimo 6 giorni fa
parent
commit
b06099d2b8

+ 3 - 3
src/api/pms/iotopeationfill/plan/index.ts

@@ -55,11 +55,11 @@ export const IotOperationPlanApi = {
     return request.put({ url: '/rq/iot-operation-plan/update-status', data: data })
   },
 
-  // 根据设备手动生成所在小部门当天的运行记录
-  manualGenerateByDeviceId: async (deviceId: number) => {
+  // 根据部门手动生成当天的运行记录
+  manualGenerateByDeptId: async (deptId: number) => {
     return await request.post<string>({
       url: '/rq/iot-operation-plan/manual-generate',
-      params: { deviceId }
+      params: { deptId }
     })
   }
 }

+ 272 - 29
src/views/pms/device/statuslog/DeviceStatus.vue

@@ -2,11 +2,15 @@
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
 import { IotOperationPlanApi } from '@/api/pms/iotopeationfill/plan'
+import * as DeptApi from '@/api/system/dept'
 import { useUserStore } from '@/store/modules/user'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import download from '@/utils/download'
 import { rangeShortcuts } from '@/utils/formatTime'
+import { checkPermi } from '@/utils/permission'
+import { defaultProps, handleTree } from '@/utils/tree'
 import DeviceStatusLogDrawer from '@/views/pms/device/statuslog/DeviceStatusLogDrawer.vue'
+import type { FormInstance, FormRules } from 'element-plus'
 
 defineOptions({ name: 'IotDeviceStatus' })
 
@@ -21,6 +25,19 @@ interface QueryParams extends PageParam {
   createTime?: string[]
 }
 
+interface GenerateDeptOption {
+  id: number
+  name: string
+  parentId?: number
+  type?: string
+  disabled?: boolean
+  children?: GenerateDeptOption[]
+}
+
+interface GenerateRecordForm {
+  deptId?: number
+}
+
 const { t } = useI18n()
 const message = useMessage()
 const { push } = useRouter()
@@ -46,13 +63,24 @@ const queryParams = reactive<QueryParams>({ ...initQuery })
 const queryFormRef = ref()
 const loading = ref(false)
 const exportLoading = ref(false)
-const generatingDeviceId = ref<number>()
 const ifShow = ref(false)
 const list = ref<IotDeviceVO[]>([])
 const total = ref(0)
 const currentDeviceId = ref<number>()
 const drawerVisible = ref(false)
 const showDrawer = ref()
+const generateDialogVisible = ref(false)
+const generateLoading = ref(false)
+const generateDeptOptionsLoading = ref(false)
+const generateDeptOptions = ref<GenerateDeptOption[]>([])
+const generateFormRef = ref<FormInstance>()
+const generateForm = reactive<GenerateRecordForm>({
+  deptId: undefined
+})
+const generateFormRules: FormRules = {
+  deptId: [{ required: true, message: '请选择部门', trigger: 'change' }]
+}
+const generateDeptTreeProps = { ...defaultProps, disabled: 'disabled' }
 
 const resultOptions = computed(() => [
   {
@@ -132,17 +160,57 @@ const handleView = async (deviceId: number) => {
   })
 }
 
-const handleGenerateRecord = async (row: IotDeviceVO) => {
+const loadGenerateDeptOptions = async () => {
+  generateDeptOptionsLoading.value = true
+  try {
+    const depts = await DeptApi.specifiedSimpleDepts(deptId)
+    const selectableDepts = depts.map((item) => ({
+      ...item,
+      type: String(item.type || ''),
+      disabled: String(item.type || '') !== '3'
+    }))
+    generateDeptOptions.value = handleTree(selectableDepts)
+  } finally {
+    generateDeptOptionsLoading.value = false
+  }
+}
+
+const openGenerateDialog = async () => {
+  generateForm.deptId = undefined
+  generateDialogVisible.value = true
+  await nextTick()
+  generateFormRef.value?.clearValidate()
+  if (!generateDeptOptions.value.length) {
+    await loadGenerateDeptOptions()
+  }
+}
+
+const submitGenerateRecord = async () => {
+  if (!generateFormRef.value) return
+  const valid = await generateFormRef.value.validate().catch(() => false)
+  if (!valid || !generateForm.deptId) return
+
+  generateLoading.value = true
   try {
-    await message.confirm(
-      `将为“${row.deviceName}”所属部门重新生成今日运行记录,如已有记录将先删除再生成,是否继续?`
-    )
-    generatingDeviceId.value = row.id
-    const result = await IotOperationPlanApi.manualGenerateByDeviceId(row.id)
+    const result = await IotOperationPlanApi.manualGenerateByDeptId(generateForm.deptId)
     message.success(result)
-  } catch {
+    generateDialogVisible.value = false
   } finally {
-    generatingDeviceId.value = undefined
+    generateLoading.value = false
+  }
+}
+
+const handleBusinessCommand = (command: string) => {
+  if (command === 'adjust-status') {
+    openForm()
+    return
+  }
+  if (command === 'generate-record') {
+    openGenerateDialog()
+    return
+  }
+  if (command === 'export') {
+    handleExport()
   }
 }
 
@@ -273,16 +341,29 @@ onMounted(() => {
         <el-button @click="resetQuery">
           <Icon icon="ep:refresh" class="mr-5px" />{{ t('devicePerson.reset') }}
         </el-button>
-        <el-button
-          type="primary"
-          plain
-          @click="openForm"
-          v-hasPermi="['pms:iot-device-status-log:create']">
-          <Icon icon="ep:plus" class="mr-5px" />{{ t('deviceStatus.setUp') }}
-        </el-button>
-        <el-button type="success" plain :loading="exportLoading" @click="handleExport">
-          <Icon icon="ep:download" class="mr-5px" />导出
-        </el-button>
+        <el-dropdown trigger="click" placement="bottom-end" @command="handleBusinessCommand">
+          <el-button plain :loading="exportLoading">
+            <Icon icon="ep:operation" class="mr-5px" />业务操作
+            <Icon icon="ep:arrow-down" class="ml-5px" />
+          </el-button>
+          <template #dropdown>
+            <el-dropdown-menu>
+              <el-dropdown-item
+                v-if="checkPermi(['pms:iot-device-status-log:create'])"
+                command="adjust-status">
+                <Icon icon="ep:refresh" class="mr-8px" />调整状态
+              </el-dropdown-item>
+              <el-dropdown-item
+                v-if="checkPermi(['rq:iot-operation-plan:create'])"
+                command="generate-record">
+                <Icon icon="ep:document-add" class="mr-8px" />生成记录
+              </el-dropdown-item>
+              <el-dropdown-item command="export" divided :disabled="exportLoading">
+                <Icon icon="ep:download" class="mr-8px" />导出数据
+              </el-dropdown-item>
+            </el-dropdown-menu>
+          </template>
+        </el-dropdown>
       </el-form-item>
     </el-form>
 
@@ -317,7 +398,7 @@ onMounted(() => {
                   <dict-tag :type="DICT_TYPE.PMS_DEVICE_STATUS" :value="row.deviceStatus" />
                 </template>
               </ZmTableColumn>
-              <ZmTableColumn :label="t('deviceStatus.operation')" width="230" fixed="right" action>
+              <ZmTableColumn :label="t('deviceStatus.operation')" width="150" fixed="right" action>
                 <template #default="{ row }">
                   <el-button
                     link
@@ -326,15 +407,6 @@ onMounted(() => {
                     v-hasPermi="['rq:iot-device:query']">
                     {{ t('deviceStatus.adjustmentRecords') }}
                   </el-button>
-                  <el-button
-                    link
-                    type="primary"
-                    :loading="generatingDeviceId === row.id"
-                    :disabled="generatingDeviceId !== undefined && generatingDeviceId !== row.id"
-                    @click="handleGenerateRecord(row)"
-                    v-hasPermi="['rq:iot-operation-plan:create']">
-                    生成记录
-                  </el-button>
                 </template>
               </ZmTableColumn>
             </ZmTable>
@@ -358,6 +430,75 @@ onMounted(() => {
     </div>
   </div>
 
+  <Dialog
+    v-model="generateDialogVisible"
+    :fullscreen="false"
+    width="560px"
+    class="generate-record-dialog">
+    <template #title>
+      <div class="generate-dialog-title">
+        <div class="generate-dialog-title__icon">
+          <Icon icon="ep:document-add" :size="22" />
+        </div>
+        <div>
+          <div class="generate-dialog-title__text">生成运行记录</div>
+          <div class="generate-dialog-title__desc">选择作业部门并生成今日运行填报记录</div>
+        </div>
+      </div>
+    </template>
+
+    <div class="generate-dialog-content">
+      <section class="generate-panel">
+        <div class="generate-panel__header">
+          <div class="generate-panel__title">
+            <span class="generate-panel__marker"></span>
+            <span>生成范围</span>
+          </div>
+        </div>
+        <div class="generate-panel__body">
+          <el-form
+            ref="generateFormRef"
+            :model="generateForm"
+            :rules="generateFormRules"
+            label-position="top"
+            size="default">
+            <el-form-item label="作业部门" prop="deptId">
+              <el-tree-select
+                v-model="generateForm.deptId"
+                :data="generateDeptOptions"
+                :props="generateDeptTreeProps"
+                :loading="generateDeptOptionsLoading"
+                node-key="id"
+                check-strictly
+                default-expand-all
+                filterable
+                clearable
+                placeholder="请选择三级部门"
+                class="w-full" />
+            </el-form-item>
+          </el-form>
+        </div>
+      </section>
+
+      <div class="generate-warning">
+        <div class="generate-warning__icon">
+          <Icon icon="ep:warning-filled" :size="18" />
+        </div>
+        <div class="generate-warning__text">
+          所选部门今日如已有运行记录,系统将删除原记录并按当前配置重新生成。
+        </div>
+      </div>
+    </div>
+    <template #footer>
+      <el-button :disabled="generateLoading" @click="generateDialogVisible = false">
+        取消
+      </el-button>
+      <el-button type="primary" :loading="generateLoading" @click="submitGenerateRecord">
+        <Icon v-if="!generateLoading" icon="ep:check" class="mr-5px" />确认生成
+      </el-button>
+    </template>
+  </Dialog>
+
   <DeviceStatusLogDrawer
     ref="showDrawer"
     :model-value="drawerVisible"
@@ -396,6 +537,108 @@ onMounted(() => {
   margin-left: 0;
 }
 
+.generate-dialog-title {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding-right: 44px;
+}
+
+.generate-dialog-title__icon {
+  display: flex;
+  width: 38px;
+  height: 38px;
+  color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
+  border-radius: 8px;
+  align-items: center;
+  justify-content: center;
+}
+
+.generate-dialog-title__text {
+  font-size: 16px;
+  font-weight: 600;
+  line-height: 22px;
+  color: var(--el-text-color-primary);
+}
+
+.generate-dialog-title__desc {
+  margin-top: 2px;
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
+
+.generate-dialog-content {
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+  padding: 3px;
+}
+
+.generate-panel {
+  overflow: hidden;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 6px;
+  box-shadow: 0 8px 24px rgb(15 35 70 / 5%);
+}
+
+.generate-panel__header {
+  display: flex;
+  min-height: 52px;
+  padding: 0 16px;
+  background: #f4f9ff;
+  border-bottom: 1px solid var(--el-border-color-lighter);
+  align-items: center;
+}
+
+.generate-panel__title {
+  display: flex;
+  font-size: 15px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  align-items: center;
+}
+
+.generate-panel__marker {
+  width: 4px;
+  height: 18px;
+  margin-right: 10px;
+  background: var(--el-color-primary);
+  border-radius: 4px;
+}
+
+.generate-panel__body {
+  padding: 20px 18px 16px;
+}
+
+.generate-panel__body :deep(.el-form-item) {
+  margin-bottom: 10px;
+}
+
+.generate-warning {
+  display: flex;
+  gap: 10px;
+  padding: 13px 16px;
+  color: var(--el-color-warning-dark-2);
+  background: var(--el-color-warning-light-9);
+  border: 1px solid var(--el-color-warning-light-7);
+  border-radius: 6px;
+  align-items: center;
+}
+
+.generate-warning__icon {
+  display: flex;
+  flex: 0 0 auto;
+}
+
+.generate-warning__text {
+  font-size: 14px;
+  line-height: 22px;
+  color: var(--el-text-color-regular);
+}
+
 .query-control {
   width: 180px;
 }