Procházet zdrojové kódy

✨ feat: 设备状态列表支持手动生成运行记录

Zimo před 6 dny
rodič
revize
f65a930015

+ 12 - 4
src/api/pms/iotopeationfill/plan/index.ts

@@ -8,12 +8,12 @@ export interface IotOperationPlanVO {
   planCycle: number // 周期
   planUnit: string // 单位
   charge: string // 负责人
-  charges: [],
-  chargeName: string,
+  charges: []
+  chargeName: string
   deviceIds: string // 设备
   remark: string // 备注
   deptId: number // 部门id
-  planDevList:[]
+  planDevList: []
 }
 
 // 巡检计划 API
@@ -47,11 +47,19 @@ export const IotOperationPlanApi = {
   exportIotOperationPlan: async (params) => {
     return await request.download({ url: `/rq/iot-operation-plan/export-excel`, params })
   },
-  updateIotOperationStatus : (id: number, status: number) => {
+  updateIotOperationStatus: (id: number, status: number) => {
     const data = {
       id,
       status
     }
     return request.put({ url: '/rq/iot-operation-plan/update-status', data: data })
+  },
+
+  // 根据部门手动生成当天的运行记录
+  manualGenerateByDeptId: async (deptId: number) => {
+    return await request.post<string>({
+      url: '/rq/iot-operation-plan/manual-generate',
+      params: { deptId }
+    })
   }
 }

+ 279 - 10
src/views/pms/device/statuslog/DeviceStatus.vue

@@ -1,11 +1,16 @@
 <script setup lang="ts">
 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' })
 
@@ -20,7 +25,21 @@ 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()
 const { ZmTable, ZmTableColumn } = useTableComponents<IotDeviceVO>()
 
@@ -50,6 +69,18 @@ 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(() => [
   {
@@ -129,6 +160,60 @@ const handleView = async (deviceId: number) => {
   })
 }
 
+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 {
+    const result = await IotOperationPlanApi.manualGenerateByDeptId(generateForm.deptId)
+    message.success(result)
+    generateDialogVisible.value = false
+  } finally {
+    generateLoading.value = false
+  }
+}
+
+const handleBusinessCommand = (command: string) => {
+  if (command === 'adjust-status') {
+    openForm()
+    return
+  }
+  if (command === 'generate-record') {
+    openGenerateDialog()
+    return
+  }
+  if (command === 'export') {
+    handleExport()
+  }
+}
+
 const handleExport = async () => {
   exportLoading.value = true
   try {
@@ -256,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>
 
@@ -332,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"
@@ -370,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;
 }

+ 21 - 16
src/views/pms/iotopeationfill/index.vue

@@ -283,6 +283,7 @@
 
 <script setup lang="ts">
 import type { FormInstance } from 'element-plus'
+import dayjs from 'dayjs'
 import { dateFormatter } from '@/utils/formatTime'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import { onMounted, ref } from 'vue'
@@ -400,10 +401,26 @@ const handleDeptNodeClick = async (row: Tree) => {
   queryParams.pageNo = 1
   await getList()
 }
+
+const normalizeCreateTime = () => {
+  if (!Array.isArray(queryParams.createTime) || queryParams.createTime.length !== 2) return
+
+  const [startTime, endTime] = queryParams.createTime
+  const start = dayjs(startTime)
+  const end = dayjs(endTime)
+  if (!start.isValid() || !end.isValid()) return
+
+  queryParams.createTime = [
+    start.startOf('day').format('YYYY-MM-DD HH:mm:ss'),
+    end.endOf('day').format('YYYY-MM-DD HH:mm:ss')
+  ]
+}
+
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
+    normalizeCreateTime()
     const data = await IotOpeationFillApi.getOperationRecordPage(queryParams)
     list.value = data.list
     total.value = data.total
@@ -441,22 +458,10 @@ const openWrite = (id?: string) => {
 /** 初始化 **/
 onMounted(async () => {
   // 计算近一周时间
-  const end = new Date()
-  const start = new Date()
-  start.setTime(start.getTime() - 7 * 24 * 60 * 60 * 1000)
-
-  // 格式化日期为后端需要的格式
-  const formatDate = (date: Date) => {
-    const year = date.getFullYear()
-    const month = String(date.getMonth() + 1).padStart(2, '0')
-    const day = String(date.getDate()).padStart(2, '0')
-    const hours = String(date.getHours()).padStart(2, '0')
-    const minutes = String(date.getMinutes()).padStart(2, '0')
-    const seconds = String(date.getSeconds()).padStart(2, '0')
-    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
-  }
-
-  queryParams.createTime = [formatDate(start), formatDate(end)]
+  queryParams.createTime = [
+    dayjs().subtract(7, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'),
+    dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss')
+  ]
 
   if (deptId != null) {
     queryParams.deptId = deptId