Explorar o código

feat: 优化运行计划页面布局样式

- 重构运行计划列表页布局,统一使用侧栏树、查询区、自适应表格和分页结构
- 优化运行计划新增/编辑页表单样式,调整基础信息卡片标题和表单栅格布局
- 优化设备列表拖拽展示样式,清理调试残留代码
Zimo hai 1 semana
pai
achega
7a7b86ec71

+ 373 - 238
src/views/pms/iotopeationfill/plan/IotOperationPlan.vue

@@ -1,165 +1,153 @@
 <template>
-  <ContentWrap v-loading="formLoading">
-    <ContentWrap>
-      <el-form
-        ref="formRef"
-        :model="formData"
-        :rules="formRules"
-        style="margin-right: 4em; margin-left: 0.5em; margin-top: 1em"
-        label-width="130px"
-      >
-        <div class="base-expandable-content">
-          <el-row>
-            <el-col :span="12">
+  <div v-loading="formLoading" class="operation-plan-page">
+    <section class="info-card">
+      <div class="info-card__header">
+        <div class="info-card__title">
+          <span class="info-card__marker"></span>
+          <span>基础信息</span>
+        </div>
+      </div>
+
+      <div class="info-card__body">
+        <el-form
+          ref="formRef"
+          :model="formData"
+          :rules="formRules"
+          size="default"
+          label-position="top"
+          class="operation-plan-form">
+          <div class="operation-plan-form__grid">
+            <div class="form-cell">
               <el-form-item :label="t('main.planName')" prop="planTitle">
                 <el-input v-model="formData.planTitle" :placeholder="t('main.nameHolder')" />
               </el-form-item>
-            </el-col>
-            <el-col :span="12">
+            </div>
+            <div class="form-cell">
               <el-form-item :label="t('workOrderMaterial.unit')" prop="planUnit">
                 <el-select
                   v-model="formData.planUnit"
                   :placeholder="t('route.unitHolder')"
                   :disabled="formType === 'update'"
                   clearable
-                >
+                  class="w-full">
                   <el-option
                     v-for="dict in getStrDictOptions(DICT_TYPE.PMS_INSPECT_UNIT)"
                     :key="dict.label"
                     :label="dict.label"
-                    :value="dict.value"
-                  />
+                    :value="dict.value" />
                 </el-select>
               </el-form-item>
-            </el-col>
-            <el-col :span="12">
+            </div>
+            <div class="form-cell">
               <el-form-item :label="t('route.cycle')" prop="planCycle">
                 <el-input-number
-                  style="width: 100%"
                   v-model="formData.planCycle"
                   :precision="0"
                   :min="1"
                   :max="99999"
-                />
+                  class="!w-full" />
               </el-form-item>
-            </el-col>
-            <el-col :span="12">
+            </div>
+
+            <div class="form-cell form-cell--start-time">
               <el-form-item :label="t('route.beginCreateTime')" prop="beginCreateTime">
                 <el-date-picker
-                  style="width: 150%"
                   v-model="formData.beginCreateTime"
                   type="datetime"
                   value-format="x"
-                />
+                  class="!w-full" />
               </el-form-item>
-            </el-col>
-            <el-col :span="12">
+            </div>
+            <div class="form-cell form-cell--report">
               <el-form-item label="日报填报" prop="isReport">
                 <el-radio-group v-model="formData.isReport">
                   <el-radio
                     v-for="dict in getIntDictOptions(DICT_TYPE.RQ_IOT_ISCOLLECTION)"
                     :key="dict.value"
-                    :value="dict.value"
-                  >
+                    :value="dict.value">
                     {{ dict.label }}
                   </el-radio>
                 </el-radio-group>
               </el-form-item>
-            </el-col>
-
-              <el-col :span="12">
-                <el-form-item :label="t('form.remark')" prop="remark">
-                  <el-input type="textarea" v-model="formData.remark" />
-                </el-form-item>
-              </el-col>
-          </el-row>
-        </div>
-      </el-form>
-    </ContentWrap>
-
-    <ContentWrap>
-      <!-- 列表 -->
-      <!--      <ContentWrap>-->
-      <ContentWrap>
-        <el-form
-          class="-mb-15px"
-          :model="queryParams"
-          ref="queryFormRef"
-          :inline="true"
-          label-width="68px"
-        >
-          <el-form-item>
-            <el-button @click="openForm" type="primary"
-            ><Icon icon="ep:plus" class="mr-5px" />
-              {{ t('route.SelectOperationDevice') }}</el-button
-            >
-          </el-form-item>
-        </el-form>
-      </ContentWrap>
-      <draggable
-        v-model="list"
-        item-key="id"
-        tag="div"
-        class="sortable-container"
-        handle=".sortable-item"
-        :animation="150"
-        @start="dragStart"
-        @end="dragEnd"
-      >
-        <template #item="{ element, index }">
-          <div class="sortable-item">
-            <!-- 序号显示 -->
-            <div class="order-number">{{ index + 1 }}</div>
-
-            <!-- 拖动手柄 -->
-            <span class="drag-handle">≡</span>
-
-            <!-- 组件内容 -->
-            <div class="component-content">
-                <span style="font-weight: bold">{{ t('iotDevice.code') }}:</span
-                ><span style="font-size: 14px">{{ element.deviceCode }}</span
-            >&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
-              <span style="font-weight: bold">{{ t('iotDevice.name') }}:</span
-              ><span style="font-size: 14px">{{ element.deviceName }}</span>
             </div>
-            <div>
-              <!--              <el-button type="warning" @click="deleteDraggable(index)">{{ t('form.delete') }}</el-button>-->
-              <el-button
-                style="vertical-align: middle"
-                link
-                type="danger"
-                @click="handleDelete(element.id,element.deviceId)"
-              >
-                <Icon style="vertical-align: middle; color: #ea3434" icon="ep:zoom-out" />
+            <div class="form-cell form-cell--remark">
+              <el-form-item :label="t('form.remark')" prop="remark">
+                <el-input
+                  v-model="formData.remark"
+                  type="textarea"
+                  :autosize="{ minRows: 4, maxRows: 4 }" />
+              </el-form-item>
+            </div>
+          </div>
+        </el-form>
+      </div>
+    </section>
+
+    <section class="info-card device-card">
+      <div class="info-card__header">
+        <div class="info-card__title">
+          <span class="info-card__marker"></span>
+          <span>设备列表</span>
+        </div>
+        <span class="card-total">共 {{ list.length }} 台设备</span>
+      </div>
+
+      <div class="info-card__body device-card__body">
+        <el-button type="primary" @click="openForm">
+          <Icon icon="ep:plus" class="mr-5px" />
+          {{ t('route.SelectOperationDevice') }}
+        </el-button>
+
+        <el-empty v-if="!list.length" description="暂无设备" class="device-empty" />
+
+        <draggable
+          v-else
+          v-model="list"
+          item-key="id"
+          tag="div"
+          class="sortable-container"
+          handle=".drag-handle"
+          :animation="150"
+          @start="dragStart"
+          @end="dragEnd">
+          <template #item="{ element, index }">
+            <div class="sortable-item">
+              <div class="order-number">{{ index + 1 }}</div>
+              <el-icon class="drag-handle" size="18"><Rank /></el-icon>
+              <div class="component-content">
+                <div class="device-title">{{ element.deviceName || '-' }}</div>
+                <div class="device-meta">
+                  <span>{{ t('iotDevice.code') }}:{{ element.deviceCode || '-' }}</span>
+                </div>
+              </div>
+              <el-button link type="danger" @click="handleDelete(element.id, element.deviceId)">
+                <Icon icon="ep:delete" class="mr-3px" />
                 {{ t('form.delete') }}
               </el-button>
             </div>
-          </div>
-        </template>
-      </draggable>
-    </ContentWrap>
-
-    <ContentWrap>
-      <el-form>
-        <el-form-item style="float: right">
-          <el-button @click="submitForm" type="primary" :disabled="formLoading">{{ t('iotMaintain.save') }}</el-button>
-          <el-button @click="close">{{ t('iotMaintain.cancel') }}</el-button>
-        </el-form-item>
-      </el-form>
-    </ContentWrap>
+          </template>
+        </draggable>
+      </div>
+    </section>
+
+    <div class="operation-plan-footer">
+      <el-button @click="close">{{ t('iotMaintain.cancel') }}</el-button>
+      <el-button type="primary" :disabled="formLoading" @click="submitForm">
+        {{ t('iotMaintain.save') }}
+      </el-button>
+    </div>
+
     <InspectItemList
       ref="inspectItemFormRef"
       :classify="formData.deviceClassify"
       :deviceId="formData.deviceId"
-      @choose="inspectItemChoose"
-    />
+      @choose="inspectItemChoose" />
     <PlanDeviceList ref="deviceFormRef" @choose="deviceChoose" />
     <RouteInspectItemDrawer
       ref="showDrawer"
       :model-value="drawerVisible"
-      @update:model-value="(val) => (drawerVisible = val)"
-    />
-  </ContentWrap>
+      @update:model-value="(val) => (drawerVisible = val)" />
+  </div>
 </template>
 <script setup lang="ts">
 import * as UserApi from '@/api/system/user'
@@ -167,18 +155,17 @@ import { useUserStore } from '@/store/modules/user'
 import { ref } from 'vue'
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import { Rank } from '@element-plus/icons-vue'
 import InspectItemList from '@/views/pms/inspect/route/InspectItemList.vue'
 import PlanDeviceList from '@/views/pms/iotopeationfill/plan/PlanDeviceList.vue'
 import { IotInspectRouteVO } from '@/api/pms/inspect/route'
-import { DICT_TYPE, getStrDictOptions,getIntDictOptions } from '@/utils/dict'
-import { IotInspectPlanApi, IotInspectPlanVO } from '@/api/pms/inspect/plan'
+import { DICT_TYPE, getStrDictOptions, getIntDictOptions } from '@/utils/dict'
 import RouteInspectItemDrawer from '@/views/pms/inspect/plan/RouteInspectItemDrawer.vue'
 import draggable from 'vuedraggable'
-import {IotDeviceApi, IotDeviceVO} from "@/api/pms/device";
-import {defaultProps,handleTree} from "@/utils/tree";
-import * as ProductClassifyApi from "@/api/pms/productclassify";
-import {IotInspectItemVO} from "@/api/pms/inspect/item";
-import {IotOperationPlanApi, IotOperationPlanVO} from "@/api/pms/iotopeationfill/plan";
+import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
+import { handleTree } from '@/utils/tree'
+import * as ProductClassifyApi from '@/api/pms/productclassify'
+import { IotOperationPlanApi, IotOperationPlanVO } from '@/api/pms/iotopeationfill/plan'
 
 /** 维修工单 表单 */
 defineOptions({ name: 'OperationPlanAdd' })
@@ -211,7 +198,7 @@ const formData = ref({
   deviceIds: undefined,
   remark: undefined,
   deptId: undefined,
-  planDevList:[]
+  planDevList: []
 })
 const formRules = reactive({
   planTitle: [{ required: true, message: '运行计划标题不能为空', trigger: 'blur' }],
@@ -254,7 +241,7 @@ const queryParams = reactive({
   templateJson: undefined,
   creator: undefined,
   sortingFields: [],
-  assetClass: undefined,
+  assetClass: undefined
 })
 const queryFormRef = ref() // 搜索的表单
 /** 重置按钮操作 */
@@ -275,140 +262,138 @@ const selectedRows = ref<IotDeviceVO[]>([]) // 多选数据(存储所有选中
 const selectAll = async () => {
   try {
     // 显示加载状态
-    loading.value = true;
-    isAllSelected.value = true;
-    deselectedRows.value = [];
-    selectedRows.value = []; // 清空现有选中项
-    allDataCache.value = []; // 清空缓存
+    loading.value = true
+    isAllSelected.value = true
+    deselectedRows.value = []
+    selectedRows.value = [] // 清空现有选中项
+    allDataCache.value = [] // 清空缓存
 
-    const paramsCopy = { ...queryParams };
-    const originalPageNo = paramsCopy.pageNo;
-    const originalPageSize = paramsCopy.pageSize;
+    const paramsCopy = { ...queryParams }
+    const originalPageNo = paramsCopy.pageNo
+    const originalPageSize = paramsCopy.pageSize
 
-    paramsCopy.pageNo = 1;
-    paramsCopy.pageSize = 100; // 按后台限制的最大页数获取
+    paramsCopy.pageNo = 1
+    paramsCopy.pageSize = 100 // 按后台限制的最大页数获取
 
     // 获取第一页数据
-    const firstPageData = await IotDeviceApi.getPlanDevicePage(paramsCopy);
-    totalSelectedCount.value = firstPageData.total;
-    allDataCache.value.push(...firstPageData.list);
+    const firstPageData = await IotDeviceApi.getPlanDevicePage(paramsCopy)
+    totalSelectedCount.value = firstPageData.total
+    allDataCache.value.push(...firstPageData.list)
 
     // 分批获取剩余数据
     if (firstPageData.total > 100) {
-      const totalPages = Math.ceil(firstPageData.total / 100);
-      const fetchPromises = [];
+      const totalPages = Math.ceil(firstPageData.total / 100)
+      const fetchPromises = []
 
       for (let page = 2; page <= totalPages; page++) {
-        fetchPromises.push(IotDeviceApi.getPlanDevicePage({ ...paramsCopy, pageNo: page }));
+        fetchPromises.push(IotDeviceApi.getPlanDevicePage({ ...paramsCopy, pageNo: page }))
       }
 
-      const remainingPages = await Promise.all(fetchPromises);
-      remainingPages.forEach(pageData => {
-        allDataCache.value.push(...pageData.list);
-      });
+      const remainingPages = await Promise.all(fetchPromises)
+      remainingPages.forEach((pageData) => {
+        allDataCache.value.push(...pageData.list)
+      })
     }
 
     // 全选时,将所有数据放入selectedRows
-    selectedRows.value = [...allDataCache.value];
+    selectedRows.value = [...allDataCache.value]
 
     // 恢复原始分页参数
-    queryParams.pageNo = originalPageNo;
-    queryParams.pageSize = originalPageSize;
+    queryParams.pageNo = originalPageNo
+    queryParams.pageSize = originalPageSize
 
-    message.success(t('operationFill.allSelected', { count: totalSelectedCount.value }));
+    message.success(t('operationFill.allSelected', { count: totalSelectedCount.value }))
   } catch (error) {
-    console.error('全选失败', error);
-    message.error(t('operationFill.selectAllFailed'));
-    isAllSelected.value = false;
-    selectedRows.value = [];
+    console.error('全选失败', error)
+    message.error(t('operationFill.selectAllFailed'))
+    isAllSelected.value = false
+    selectedRows.value = []
   } finally {
-    loading.value = false;
+    loading.value = false
   }
-};
+}
 
 // 修改取消全选方法,同步清空selectedRows
 const deselectAll = () => {
-  isAllSelected.value = false;
-  deselectedRows.value = [];
-  selectedRows.value = []; // 清空所有选中项
-  allDataCache.value = []; // 清空缓存
-  totalSelectedCount.value = 0;
-  message.info(t('operationFill.allDeselected'));
-};
+  isAllSelected.value = false
+  deselectedRows.value = []
+  selectedRows.value = [] // 清空所有选中项
+  allDataCache.value = [] // 清空缓存
+  totalSelectedCount.value = 0
+  message.info(t('operationFill.allDeselected'))
+}
 
 // 修改单选逻辑,确保selectedRows正确更新
 const selectRow = (row) => {
   if (isAllSelected.value) {
     // 全选状态下的单选操作
     const deselectedIndex = deselectedRows.value.findIndex(
-      item => item.id === row.id && item.deviceCode === row.deviceCode
-    );
+      (item) => item.id === row.id && item.deviceCode === row.deviceCode
+    )
 
     const selectedIndex = selectedRows.value.findIndex(
-      item => item.id === row.id && item.deviceCode === row.deviceCode
-    );
+      (item) => item.id === row.id && item.deviceCode === row.deviceCode
+    )
 
     if (deselectedIndex > -1) {
       // 从取消列表中移除,添加到选中列表
-      deselectedRows.value.splice(deselectedIndex, 1);
+      deselectedRows.value.splice(deselectedIndex, 1)
       if (selectedIndex === -1) {
-        selectedRows.value.push({ ...row });
+        selectedRows.value.push({ ...row })
       }
     } else {
       // 从选中列表中移除,添加到取消列表
       if (selectedIndex > -1) {
-        selectedRows.value.splice(selectedIndex, 1);
+        selectedRows.value.splice(selectedIndex, 1)
       }
-      deselectedRows.value.push({ ...row });
+      deselectedRows.value.push({ ...row })
     }
   } else {
     // 非全选状态下的单选操作
     const index = selectedRows.value.findIndex(
-      item => item.id === row.id && item.deviceCode === row.deviceCode
-    );
+      (item) => item.id === row.id && item.deviceCode === row.deviceCode
+    )
 
     if (index > -1) {
       // 取消选择
-      selectedRows.value.splice(index, 1);
+      selectedRows.value.splice(index, 1)
     } else {
       // 选中
-      selectedRows.value.push({ ...row });
+      selectedRows.value.push({ ...row })
     }
   }
-};
-
-
+}
 
 // 生成行唯一标识
 const rowKey = (row: any) => {
-  return `${row.id}`; // 确保行更新时重新渲染
-};
+  return `${row.id}` // 确保行更新时重新渲染
+}
 
 // 修改移除标签方法,确保selectedRows同步更新
 const removeTag = (route) => {
   // 从选中列表中移除
   const selectedIndex = selectedRows.value.findIndex(
-    item => item.id === route.id && item.deviceCode === route.deviceCode
-  );
+    (item) => item.id === route.id && item.deviceCode === route.deviceCode
+  )
 
   if (selectedIndex > -1) {
-    selectedRows.value.splice(selectedIndex, 1);
+    selectedRows.value.splice(selectedIndex, 1)
   }
 
   // 全选状态下还要添加到取消列表
   if (isAllSelected.value) {
     const deselectedIndex = deselectedRows.value.findIndex(
-      item => item.id === route.id && item.deviceCode === route.deviceCode
-    );
+      (item) => item.id === route.id && item.deviceCode === route.deviceCode
+    )
 
     if (deselectedIndex === -1) {
-      deselectedRows.value.push({ ...route });
+      deselectedRows.value.push({ ...route })
     }
   }
 
   // 刷新表格
-  list.value = [...list.value];
-};
+  list.value = [...list.value]
+}
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
@@ -427,12 +412,10 @@ const getList = async () => {
 // 拖动状态管理
 const items = ref([])
 const deviceChoose = (rows) => {
-  debugger
   rows.forEach((it) => {
-    const ifExist = list.value.find((item) => item.id === it.id&&item.deviceId===it.deviceId)
+    const ifExist = list.value.find((item) => item.id === it.id && item.deviceId === it.deviceId)
     if (!ifExist) {
       list.value.push(it)
-      console.log(JSON.stringify(it))
     }
   })
   // list.value = rows
@@ -481,16 +464,17 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    formData.value.planDevList = list;
+    formData.value.planDevList = list
     const user = wsCache.get(CACHE_KEY.USER)
     const data = formData.value as unknown as IotOperationPlanVO
     if (formType.value === 'create') {
       //校验是否是已有计划
       const newitems = itemsWithIndex
       formData.value.deviceIds = JSON.stringify(newitems.value)
-      if (!formData.value.deptId) {formData.value.deptId = user.user.deptId}
-      await IotOperationPlanApi.createIotOperationPlan(data).then((res) => {
-      })
+      if (!formData.value.deptId) {
+        formData.value.deptId = user.user.deptId
+      }
+      await IotOperationPlanApi.createIotOperationPlan(data).then((res) => {})
       message.success(t('common.createSuccess'))
       close()
     } else {
@@ -510,7 +494,6 @@ const submitForm = async () => {
 
 /** 重置表单 */
 onMounted(async () => {
-
   const route = useRoute()
   const deptId = useUserStore().getUser.deptId
   formData.value.deptId = Number(route.query.deptId)
@@ -532,8 +515,7 @@ onMounted(async () => {
 })
 const handleDelete = async (id: number, deviceId: number) => {
   try {
-    debugger
-    const index = list.value.findIndex((item) => item.id === id&&item.deviceId===deviceId)
+    const index = list.value.findIndex((item) => item.id === id && item.deviceId === deviceId)
     if (index !== -1) {
       // 通过 splice 删除元素
       list.value.splice(index, 1)
@@ -542,87 +524,182 @@ const handleDelete = async (id: number, deviceId: number) => {
 }
 </script>
 <style scoped>
-.base-expandable-content {
-  overflow: hidden; /* 隐藏溢出的内容 */
-  transition: max-height 0.3s ease; /* 平滑过渡效果 */
+.operation-plan-page {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  min-height: calc(
+    100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
+  );
 }
-/* 横向布局容器 */
 
+.info-card {
+  overflow: hidden;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  box-shadow: 0 10px 28px rgb(31 45 61 / 6%);
+}
 
-/* 拖拽手柄样式 */
-.drag-handle {
-  opacity: 0.5;
-  cursor: move;
-  transition: opacity 0.3s;
+.device-card {
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+  min-height: 360px;
 }
-.drag-handle:hover {
-  opacity: 1;
+
+.info-card__header {
+  display: flex;
+  min-height: 58px;
+  padding: 0 20px;
+  background: linear-gradient(90deg, rgb(64 158 255 / 8%) 0%, rgb(255 255 255 / 0%) 72%);
+  border-bottom: 1px solid var(--el-border-color-lighter);
+  align-items: center;
+  justify-content: space-between;
 }
 
-/* 拖拽时的悬停效果 */
-.horizontal-item:hover {
-  transform: translateY(-2px);
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+.info-card__title {
+  display: flex;
+  gap: 10px;
+  font-size: 16px;
+  font-weight: 600;
+  line-height: 22px;
+  color: var(--el-text-color-primary);
+  align-items: center;
 }
 
-/* 滚动条样式 */
-.horizontal-list::-webkit-scrollbar {
-  height: 8px;
+.info-card__marker {
+  width: 4px;
+  height: 18px;
+  background: var(--el-color-primary);
+  border-radius: 2px;
 }
-.horizontal-list::-webkit-scrollbar-thumb {
-  background: #888;
-  border-radius: 4px;
+
+.info-card__body {
+  padding: 20px;
 }
 
-.sortable-container {
-  cursor: move;
+.card-total {
+  font-size: 13px;
+  color: var(--el-text-color-secondary);
+}
+
+.operation-plan-form__grid {
+  display: grid;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  gap: 18px 32px;
+  align-items: start;
+}
+
+.form-cell {
+  min-width: 0;
+}
+
+.form-cell--start-time {
+  grid-column: 1;
+  grid-row: 2;
+}
+
+.form-cell--report {
+  grid-column: 1;
+  grid-row: 3;
+}
+
+.form-cell--remark {
+  grid-column: 2 / 4;
+  grid-row: 2 / 4;
+}
+
+.operation-plan-form :deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+.operation-plan-form :deep(.el-form-item__label) {
+  margin-bottom: 6px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
+
+.form-cell--remark :deep(.el-form-item),
+.form-cell--remark :deep(.el-form-item__content),
+.form-cell--remark :deep(.el-textarea),
+.form-cell--remark :deep(.el-textarea__inner) {
+  height: 100%;
+}
+
+.device-card__body {
   display: flex;
+  flex: 1;
   flex-direction: column;
-  gap: 9px;
-  //max-height: 80vh;
+  gap: 14px;
+  min-height: 0;
+}
+
+.device-card__body > .el-button {
+  align-self: flex-start;
+}
+
+.device-empty {
+  flex: 1;
+  min-height: 260px;
+}
+
+.sortable-container {
+  display: flex;
+  max-height: calc(100vh - 430px);
+  min-height: 0;
+  padding-right: 4px;
   overflow-y: auto;
-  padding: 7px;
+  flex: 1;
+  flex-direction: column;
+  gap: 10px;
 }
 
 .sortable-item {
   display: flex;
+  gap: 12px;
   align-items: center;
-  padding: 8px;
-  background: #fff;
-  border: 1px solid #ebeef5;
+  min-height: 60px;
+  padding: 12px 14px;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
   border-radius: 8px;
   transition:
-    transform 0.3s,
-    box-shadow 0.3s;
+    border-color 0.2s,
+    box-shadow 0.2s,
+    transform 0.2s;
   user-select: none;
-  height: 50%;
 }
 
 .sortable-item:hover {
-  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+  border-color: var(--el-color-primary-light-5);
+  transform: translateY(-1px);
+  box-shadow: 0 4px 14px rgb(31 45 61 / 8%);
 }
 
 .order-number {
-  width: 24px;
-  height: 24px;
   display: flex;
+  flex: 0 0 28px;
   align-items: center;
   justify-content: center;
-  background: #409eff;
-  color: white;
+  width: 28px;
+  height: 28px;
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--el-color-primary);
+  background: var(--el-color-primary-light-9);
   border-radius: 50%;
-  margin-right: 7px;
-  font-weight: bold;
 }
 
 .drag-handle {
-  padding: 0 12px;
-  opacity: 0.4;
-  transition: opacity 0.3s;
+  flex: 0 0 auto;
+  color: var(--el-text-color-placeholder);
+  cursor: grab;
+  transition: color 0.2s;
 }
 
 .drag-handle:hover {
-  opacity: 1;
+  color: var(--el-color-primary);
 }
 
 .component-content {
@@ -630,12 +707,70 @@ const handleDelete = async (id: number, deviceId: number) => {
   min-width: 0;
 }
 
-/* 优化滚动条 */
+.device-title {
+  overflow: hidden;
+  font-size: 14px;
+  font-weight: 600;
+  line-height: 22px;
+  color: var(--el-text-color-primary);
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.device-meta {
+  display: flex;
+  gap: 18px;
+  margin-top: 2px;
+  overflow: hidden;
+  font-size: 13px;
+  line-height: 20px;
+  color: var(--el-text-color-secondary);
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.operation-plan-footer {
+  display: flex;
+  justify-content: flex-end;
+  padding: 16px 24px;
+  background: var(--el-bg-color);
+  border-radius: 8px;
+  box-shadow: var(--el-box-shadow-light);
+}
+
 .sortable-container::-webkit-scrollbar {
   width: 8px;
 }
+
 .sortable-container::-webkit-scrollbar-thumb {
-  background: #c0c4cc;
+  background: var(--el-border-color);
   border-radius: 4px;
 }
+
+@media (width <= 1200px) {
+  .operation-plan-form__grid {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  .form-cell--start-time,
+  .form-cell--report,
+  .form-cell--remark {
+    grid-column: auto;
+    grid-row: auto;
+  }
+
+  .form-cell--remark {
+    grid-column: 1 / -1;
+  }
+}
+
+@media (width <= 768px) {
+  .operation-plan-form__grid {
+    grid-template-columns: minmax(0, 1fr);
+  }
+
+  .form-cell--remark {
+    grid-column: auto;
+  }
+}
 </style>

+ 258 - 283
src/views/pms/iotopeationfill/plan/index.vue

@@ -1,210 +1,45 @@
-<template>
-  <el-row :gutter="20">
-    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
-
-    <el-col :xs="24" :span="isLeftContentCollapsed ? 24 : 20">
-      <div
-        style="border: none; background: #fff; display: flex; align-items: center"
-        class="py-2 rounded-sm px-2"
-      >
-        <!-- 搜索工作栏 -->
-        <el-form
-          class="-mb-15px"
-          :model="queryParams"
-          ref="queryFormRef"
-          :inline="true"
-          label-width="68px"
-        >
-          <el-form-item :label="t('main.planName')" prop="planTitle" style="margin-left: 25px">
-            <el-input
-              v-model="queryParams.planTitle"
-              :placeholder="t('main.nameHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-180px"
-            />
-          </el-form-item>
-          <el-form-item :label="t('main.planCode')" prop="planCode">
-            <el-input
-              v-model="queryParams.planCode"
-              :placeholder="t('main.codeHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-180px"
-            />
-          </el-form-item>
-          <el-form-item :label="t('common.createTime')" prop="createTime" label-width="100px">
-            <el-date-picker
-              v-model="queryParams.createTime"
-              value-format="YYYY-MM-DD HH:mm:ss"
-              type="daterange"
-              :start-placeholder="t('info.start')"
-              :end-placeholder="t('info.end')"
-              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-              class="!w-180px"
-            />
-          </el-form-item>
-          <el-form-item>
-            <el-button @click="handleQuery"
-              ><Icon icon="ep:search" class="mr-5px" /> {{ t('operationFill.search') }}</el-button
-            >
-            <el-button @click="resetQuery"
-              ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('operationFill.reset') }}</el-button
-            >
-            <el-button
-              type="primary"
-              plain
-              @click="openForm('create')"
-              v-hasPermi="['rq:iot-inspect-plan:create']"
-            >
-              <Icon icon="ep:plus" class="mr-5px" /> {{ t('action.add') }}
-            </el-button>
-            <el-button
-              type="success"
-              plain
-              @click="handleExport"
-              :loading="exportLoading"
-              v-hasPermi="['rq:iot-inspect-plan:export']"
-            >
-              <Icon icon="ep:download" class="mr-5px" /> 导出
-            </el-button>
-          </el-form-item>
-        </el-form>
-      </div>
-
-      <!-- 列表 -->
-      <ContentWrap style="border: none; margin-top: 20px">
-        <zm-table :loading="loading" :data="list" height="calc(85vh - 175px)">
-          <el-table-column :label="t('iotDevice.serial')" width="70" align="center">
-            <template #default="scope">
-              {{ scope.$index + 1 }}
-            </template>
-          </el-table-column>
-          <zm-table-column :label="t('route.planTitle')" align="center" prop="planTitle" />
-          <!--          <el-table-column :label="t('route.planNumber')" align="center" prop="planCode" min-width="200" />-->
-          <zm-table-column
-            :label="t('route.cycle')"
-            align="center"
-            prop="planCycle"
-            min-width="80"
-          />
-          <zm-table-column
-            :label="t('workOrderMaterial.unit')"
-            align="center"
-            prop="planUnit"
-            min-width="80"
-          >
-            <template #default="scope">
-              <dict-tag :type="DICT_TYPE.PMS_INSPECT_UNIT" :value="scope.row.planUnit" />
-            </template>
-          </zm-table-column>
-          <zm-table-column
-            :label="t('bomList.status')"
-            prop="status"
-            column-key="status"
-            min-width="80"
-          >
-            <template #default="scope">
-              <el-switch
-                :model-value="scope.row.status"
-                :active-value="0"
-                :inactive-value="1"
-                @change="(value) => handleStatusChange(scope.row, value)"
-              />
-            </template>
-          </zm-table-column>
-          <zm-table-column
-            :label="t('route.beginCreateTime')"
-            align="center"
-            prop="beginCreateTime"
-            :formatter="dateFormatter"
-            min-width="180px"
-          />
-          <zm-table-column
-            :label="t('route.lastCreateTime')"
-            align="center"
-            prop="lastCreateTime"
-            :formatter="dateFormatter"
-            min-width="180px"
-          />
-          <zm-table-column
-            :label="t('iotDevice.dept')"
-            align="center"
-            prop="deptName"
-            min-width="130"
-          />
-          <zm-table-column
-            :label="t('inspect.createName')"
-            align="center"
-            prop="createName"
-            min-width="130"
-          />
-          <zm-table-column
-            :label="t('operationFill.operation')"
-            align="center"
-            min-width="120"
-            fixed="right"
-            action
-          >
-            <template #default="scope">
-              <el-button
-                link
-                type="primary"
-                @click="openForm('update', scope.row.id)"
-                v-hasPermi="['rq:iot-operation-plan:update']"
-              >
-                {{ t('fault.edit') }}
-              </el-button>
-              <el-button
-                link
-                type="danger"
-                @click="handleDelete(scope.row.id)"
-                v-hasPermi="['rq:iot-operation-plan:delete']"
-              >
-                {{ t('fault.del') }}
-              </el-button>
-            </template>
-          </zm-table-column>
-        </zm-table>
-        <!-- 分页 -->
-        <Pagination
-          :total="total"
-          v-model:page="queryParams.pageNo"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
-      </ContentWrap>
-    </el-col>
-  </el-row>
-  <!-- 表单弹窗:添加/修改 -->
-  <IotOperationPlanForm ref="formRef" @success="getList" />
-</template>
-
-<script setup lang="ts">
-import { ref, reactive, onMounted } from 'vue'
-import { dateFormatter } from '@/utils/formatTime'
-import download from '@/utils/download'
+<script lang="ts" setup>
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { IotOperationPlanApi, IotOperationPlanVO } from '@/api/pms/iotopeationfill/plan'
-import IotOperationPlanForm from './IotOperationPlanForm.vue'
 import { CommonStatusEnum } from '@/utils/constants'
+import { dateFormatter } from '@/utils/formatTime'
 import { DICT_TYPE } from '@/utils/dict'
-import DeptTree from '@/views/system/user/DeptTree2.vue'
-import { useRouter } from 'vue-router'
-import { useI18n } from 'vue-i18n'
+import { checkPermi } from '@/utils/permission'
+import download from '@/utils/download'
+import DeptTreeSelect from '@/components/DeptTreeSelect/index.vue'
 
-import { useTableComponents } from '@/components/ZmTable/useTableComponents'
-const { ZmTable, ZmTableColumn } = useTableComponents()
+defineOptions({ name: 'IotOperationPlan' })
 
-defineOptions({ name: 'iotOperationPlan' })
-const message = useMessage() // 消息弹窗
+type IotOperationPlanRow = IotOperationPlanVO & {
+  status?: number
+  beginCreateTime?: string
+  lastCreateTime?: string
+  deptName?: string
+  createName?: string
+}
 
+interface QueryParams extends PageParam {
+  planTitle?: string
+  planCode?: string
+  planCycle?: number
+  planUnit?: string
+  charge?: string
+  deviceIds?: string
+  remark?: string
+  createTime?: string[]
+  deptId?: number
+}
+
+const message = useMessage()
 const { push } = useRouter()
 const { t } = useI18n()
+const { ZmTable, ZmTableColumn } = useTableComponents<IotOperationPlanRow>()
 
 const loading = ref(true)
-const list = ref<IotOperationPlanVO[]>([])
 const total = ref(0)
-const queryParams = reactive({
+const list = ref<IotOperationPlanRow[]>([])
+const exportLoading = ref(false)
+const queryParams = reactive<QueryParams>({
   pageNo: 1,
   pageSize: 10,
   planTitle: undefined,
@@ -218,48 +53,34 @@ const queryParams = reactive({
   deptId: undefined
 })
 const queryFormRef = ref()
-const exportLoading = ref(false)
-
-const isLeftContentCollapsed = ref(false)
-const hoverText = ref('')
 const selectedDept = ref<{ id: number; name: string }>()
 
-const handleDeptNodeClick = async (row) => {
-  selectedDept.value = { id: row.id, name: row.name }
-  queryParams.deptId = row.id
-  await getList()
+const normalizeStatus = (status: unknown) => {
+  const value = Number(status)
+  if (value === CommonStatusEnum.ENABLE || value === CommonStatusEnum.DISABLE) {
+    return value
+  }
+  return undefined
 }
 
-const handleStatusChange = async (row: IotOperationPlanVO, value: number) => {
-  // 如果还未初始化完成,直接返回
-  if (!isInitialized.value) {
-    return
-  }
-  const previousStatus = row.status
-  if (previousStatus === value) {
-    return
-  }
-  try {
-    const text = value === CommonStatusEnum.ENABLE ? '启用' : '停用'
-    await message.confirm('确认要"' + text + '""' + row.planTitle + '"巡检计划吗?')
-    row.status = value
-    await IotOperationPlanApi.updateIotOperationStatus(row.id, value)
-    await getList()
-  } catch {
-    row.status = previousStatus
-  }
+const isValidStatus = (status: unknown) => normalizeStatus(status) !== undefined
+
+const getStatusText = (status: unknown) => {
+  const value = normalizeStatus(status)
+  if (value === CommonStatusEnum.ENABLE) return '启用'
+  if (value === CommonStatusEnum.DISABLE) return '停用'
+  return '未知'
 }
 
-const isInitialized = ref(false)
 const getList = async () => {
   loading.value = true
   try {
     const data = await IotOperationPlanApi.getIotOperationPlanPage(queryParams)
-    list.value = data.list
+    list.value = data.list.map((item: IotOperationPlanRow) => ({
+      ...item,
+      status: normalizeStatus(item.status)
+    }))
     total.value = data.total
-    setTimeout(() => {
-      isInitialized.value = true
-    }, 100)
   } finally {
     loading.value = false
   }
@@ -271,11 +92,29 @@ const handleQuery = () => {
 }
 
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  queryFormRef.value?.resetFields()
   selectedDept.value = undefined
+  queryParams.deptId = undefined
   handleQuery()
 }
 
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
+  handleQuery()
+}
+
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
+}
+
+const handleDeptNodeClick = async (row: Tree) => {
+  selectedDept.value = { id: row.id, name: row.name }
+  queryParams.deptId = row.id
+  queryParams.pageNo = 1
+  await getList()
+}
+
 const openForm = (type: string, id?: number) => {
   if (typeof id === 'number') {
     push({ name: 'OperationPlanEdit', params: { id } })
@@ -290,6 +129,32 @@ const openForm = (type: string, id?: number) => {
   })
 }
 
+const handleStatusChange = async (
+  row: IotOperationPlanRow,
+  nextStatus: string | number | boolean
+) => {
+  const status = normalizeStatus(nextStatus)
+  const oldStatus = normalizeStatus(row.status)
+  if (
+    !row.id ||
+    !row.planTitle ||
+    status === undefined ||
+    oldStatus === undefined ||
+    status === oldStatus
+  ) {
+    return
+  }
+
+  try {
+    await message.confirm(`确认要"${getStatusText(status)}""${row.planTitle}"巡检计划吗?`)
+    row.status = status
+    await IotOperationPlanApi.updateIotOperationStatus(row.id, status)
+    await getList()
+  } catch {
+    row.status = oldStatus
+  }
+}
+
 const handleDelete = async (id: number) => {
   try {
     await message.delConfirm()
@@ -311,70 +176,180 @@ const handleExport = async () => {
   }
 }
 
-const toggleLeftContent = () => {
-  isLeftContentCollapsed.value = !isLeftContentCollapsed.value
-}
-
-const handleMouseOver = () => {
-  hoverText.value = isLeftContentCollapsed.value ? '展开' : '收起'
-}
-
-const handleMouseOut = () => {
-  hoverText.value = ''
-}
-
 onMounted(() => {
   getList()
 })
 </script>
 
-<style scoped>
-.leftcontent {
-  transition: width 0.3s ease;
-  position: relative;
-}
-
-.leftcontent.collapsed {
-  width: 0;
-  overflow: hidden;
-}
+<template>
+  <div
+    class="grid grid-cols-[auto_1fr] grid-rows-[auto_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <DeptTreeSelect
+      v-model="queryParams.deptId"
+      :dept-id="queryParams.deptId ?? 0"
+      :top-id="0"
+      :init-select="false"
+      :show-title="false"
+      request-api="getSimpleDeptList"
+      class="row-span-2"
+      @node-click="handleDeptNodeClick" />
 
-.rightcontent {
-  position: relative;
-}
+    <el-form
+      ref="queryFormRef"
+      :model="queryParams"
+      size="default"
+      label-width="68px"
+      class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 py-3 gap-6 flex items-center justify-between flex-wrap min-w-0">
+      <div class="flex items-center gap-6 flex-wrap">
+        <el-form-item :label="t('main.planName')" prop="planTitle">
+          <el-input
+            v-model="queryParams.planTitle"
+            :placeholder="t('main.nameHolder')"
+            clearable
+            class="!w-200px"
+            @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item :label="t('main.planCode')" prop="planCode">
+          <el-input
+            v-model="queryParams.planCode"
+            :placeholder="t('main.codeHolder')"
+            clearable
+            class="!w-200px"
+            @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item :label="t('common.createTime')" prop="createTime" label-width="100px">
+          <el-date-picker
+            v-model="queryParams.createTime"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            type="daterange"
+            :start-placeholder="t('info.start')"
+            :end-placeholder="t('info.end')"
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+            class="!w-320px" />
+        </el-form-item>
+      </div>
 
-.toggle-button {
-  position: absolute;
-  top: 44%;
-  transform: translate(-65%, -50%);
-  width: 12px;
-  height: 40px;
-  background-color: #f0f0f0;
-  cursor: pointer;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  z-index: 1;
-  clip-path: polygon(0 0, 100% 18%, 100% 85%, 0 100%);
-  border-radius: 8px;
-}
+      <el-form-item>
+        <el-button type="primary" @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" /> {{ t('operationFill.search') }}
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" /> {{ t('operationFill.reset') }}
+        </el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['rq:iot-inspect-plan:create']">
+          <Icon icon="ep:plus" class="mr-5px" /> {{ t('action.add') }}
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['rq:iot-inspect-plan:export']">
+          <Icon icon="ep:download" class="mr-5px" />导出
+        </el-button>
+      </el-form-item>
+    </el-form>
 
-/* 添加鼠标悬停样式 */
-.toggle-button:hover {
-  background-color: #afafaf;
-}
+    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-w-0">
+      <div class="flex-1 relative min-h-0">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <ZmTable
+              :data="list"
+              :loading="loading"
+              :width="width"
+              :height="height"
+              :max-height="height"
+              show-border>
+              <ZmTableColumn
+                type="index"
+                :label="t('iotDevice.serial')"
+                :width="70"
+                fixed="left"
+                hide-in-column-settings />
+              <ZmTableColumn prop="planTitle" :label="t('route.planTitle')" fixed="left" />
+              <ZmTableColumn prop="planCycle" :label="t('route.cycle')" width="90" />
+              <ZmTableColumn prop="planUnit" :label="t('workOrderMaterial.unit')" width="90">
+                <template #default="{ row }">
+                  <dict-tag :type="DICT_TYPE.PMS_INSPECT_UNIT" :value="row.planUnit" />
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                prop="status"
+                :label="t('bomList.status')"
+                :real-value="(row) => getStatusText(row.status)"
+                width="90">
+                <template #default="{ row }">
+                  <el-switch
+                    v-if="isValidStatus(row.status)"
+                    :model-value="row.status"
+                    :active-value="0"
+                    :inactive-value="1"
+                    :disabled="!checkPermi(['rq:iot-operation-plan:update'])"
+                    @change="(status) => handleStatusChange(row, status)" />
+                  <el-tag v-else type="info">未知</el-tag>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                prop="beginCreateTime"
+                :label="t('route.beginCreateTime')"
+                :formatter="dateFormatter"
+                width="180" />
+              <ZmTableColumn
+                prop="lastCreateTime"
+                :label="t('route.lastCreateTime')"
+                :formatter="dateFormatter"
+                width="180" />
+              <ZmTableColumn prop="deptName" :label="t('iotDevice.dept')" width="130" />
+              <ZmTableColumn prop="createName" :label="t('inspect.createName')" width="130" />
+              <ZmTableColumn :label="t('operationFill.operation')" width="150" fixed="right" action>
+                <template #default="{ row }">
+                  <div class="flex items-center justify-center gap-2">
+                    <el-button
+                      type="primary"
+                      link
+                      @click="openForm('update', row.id)"
+                      v-hasPermi="['rq:iot-operation-plan:update']">
+                      <Icon icon="ep:edit" class="mr-3px" />{{ t('fault.edit') }}
+                    </el-button>
+                    <el-button
+                      type="danger"
+                      link
+                      @click="handleDelete(row.id)"
+                      v-hasPermi="['rq:iot-operation-plan:delete']">
+                      <Icon icon="ep:delete" class="mr-3px" />{{ t('fault.del') }}
+                    </el-button>
+                  </div>
+                </template>
+              </ZmTableColumn>
+            </ZmTable>
+          </template>
+        </el-auto-resizer>
+      </div>
 
-.triangle {
-  width: 0;
-  height: 0;
-  border-top: 4px solid transparent;
-  border-bottom: 4px solid transparent;
-  transition: transform 0.4s ease;
-  border-right: 5px solid gray; /* 修改为右边框显示颜色 */
-  border-left: none; /* 移除左边框 */
-}
+      <div class="h-8 mt-2 flex items-center justify-end">
+        <el-pagination
+          v-show="total > 0"
+          size="default"
+          :current-page="queryParams.pageNo"
+          :page-size="queryParams.pageSize"
+          :background="true"
+          :page-sizes="[10, 20, 30, 50, 100]"
+          :total="total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange" />
+      </div>
+    </div>
+  </div>
+</template>
 
-.triangle.rotated {
-  transform: rotate(180deg);
+<style scoped>
+:deep(.el-form-item) {
+  margin-bottom: 0;
 }
 </style>