Bladeren bron

feat: 优化模型属性模板页面样式

- 模型属性模板列表页改用 DeviceCategoryTree 侧边栏和自适应表格布局
- 优化新增/编辑模板弹窗样式,统一标题区、表单卡片和底部操作
- 优化属性信息详情页布局,增加模板信息卡片并调整属性列表展示
Zimo 1 week geleden
bovenliggende
commit
adef7a0de1

+ 186 - 67
src/views/pms/modeltemplate/TemplateForm.vue

@@ -1,54 +1,74 @@
 <template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle">
-    <el-form
-      ref="formRef"
-      v-loading="formLoading"
-      :model="formData"
-      :rules="formRules"
-      label-width="100px"
-    >
-      <el-row>
-        <el-col :span="12">
-          <el-form-item label="设备分类" prop="deviceCategoryId">
-            <el-tree-select
-              v-model="formData.deviceCategoryId"
-              :data="deviceCategoryTree"
-              :props="defaultProps"
-              check-strictly
-              default-expand-all
-              value-key="deviceCategoryId"
-              placeholder="请选择设备分类"
-              @node-click="handleDeviceCategoryTreeNodeClick"
-              clearable
-              filterable
-            />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item label="模板名称" prop="name">
-            <el-input v-model="formData.name" maxlength="32" placeholder="请输入模板名称" />
-          </el-form-item>
-        </el-col>
-<!--        <el-col :span="12">
-          <el-form-item label="模板编码" prop="code">
-            <el-input v-model="formData.code" maxlength="50" placeholder="请输入模板编码" />
-          </el-form-item>
-        </el-col>-->
-      </el-row>
-      <el-row>
-        <el-col :span="24">
-          <el-form-item label="备注">
-            <el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
-          </el-form-item>
-        </el-col>
-      </el-row>
-    </el-form>
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="920px" class="model-template-dialog">
+    <template #title>
+      <div class="dialog-title">
+        <div class="dialog-title__icon">
+          <Icon icon="ep:set-up" :size="22" />
+        </div>
+        <div>
+          <div class="dialog-title__text">{{ dialogTitle }}</div>
+          <div class="dialog-title__desc">维护设备分类下的模型属性模板信息</div>
+        </div>
+      </div>
+    </template>
+
+    <div v-loading="formLoading" class="template-form-body">
+      <section class="template-form-card">
+        <div class="template-form-card__header">
+          <div class="template-form-card__title">
+            <span class="template-form-card__marker"></span>
+            <span>模板信息</span>
+          </div>
+        </div>
+
+        <el-form
+          ref="formRef"
+          :model="formData"
+          :rules="formRules"
+          size="default"
+          label-position="top"
+          class="template-form">
+          <el-row :gutter="20">
+            <el-col :xs="24" :sm="12">
+              <el-form-item label="设备分类" prop="deviceCategoryId">
+                <el-tree-select
+                  v-model="formData.deviceCategoryId"
+                  :data="deviceCategoryTree"
+                  :props="defaultProps"
+                  check-strictly
+                  default-expand-all
+                  value-key="deviceCategoryId"
+                  placeholder="请选择设备分类"
+                  clearable
+                  filterable
+                  class="w-full"
+                  @node-click="handleDeviceCategoryTreeNodeClick" />
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24" :sm="12">
+              <el-form-item label="模板名称" prop="name">
+                <el-input v-model="formData.name" maxlength="32" placeholder="请输入模板名称" />
+              </el-form-item>
+            </el-col>
+            <el-col :xs="24">
+              <el-form-item label="备注">
+                <el-input
+                  v-model="formData.remark"
+                  placeholder="请输入内容"
+                  type="textarea"
+                  :autosize="{ minRows: 3, maxRows: 5 }" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-form>
+      </section>
+    </div>
+
     <template #footer>
-      <div v-show="showButton">
-        <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <div v-show="showButton" class="dialog-footer">
         <el-button @click="dialogVisible = false">取 消</el-button>
+        <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
       </div>
-
     </template>
   </Dialog>
 </template>
@@ -58,15 +78,14 @@ import { defaultProps, handleTree } from '@/utils/tree'
 import * as ProductClassifyApi from '@/api/pms/productclassify'
 import * as DeviceTemplateApi from '@/api/pms/modeltemplate'
 import { FormRules } from 'element-plus'
-import { useTreeStore } from '@/store/modules/attrTemplateTreeStore';
-import {ModelAttrTemplateVO} from "@/api/pms/modeltemplate";
+import { useTreeStore } from '@/store/modules/attrTemplateTreeStore'
 
 defineOptions({ name: 'DeviceTemplateForm' })
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
-const treeStore = useTreeStore();
-const localDeviceCategoryId = ref(null);  // 通过store存储的设备分类id 由父组件传递过来
+const treeStore = useTreeStore()
+const localDeviceCategoryId = ref(null) // 通过store存储的设备分类id 由父组件传递过来
 const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
@@ -77,7 +96,7 @@ const formData = ref({
   code: '',
   id: undefined,
   remark: '',
-  status: CommonStatusEnum.ENABLE,
+  status: CommonStatusEnum.ENABLE
 })
 const formData1 = ref({
   name: '',
@@ -85,22 +104,21 @@ const formData1 = ref({
   code: '',
   id: undefined,
   remark: '',
-  status: CommonStatusEnum.ENABLE,
+  status: CommonStatusEnum.ENABLE
 })
 const formRules = reactive<FormRules>({
   name: [{ required: true, message: '模板名称不能为空', trigger: 'blur' }],
-  deviceCategoryId: [{ required: true, message: '所属设备分类不能为空', trigger: 'blur' }],
-
+  deviceCategoryId: [{ required: true, message: '所属设备分类不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
-const deviceCategoryTree = ref()  // 设备分类树
-const showButton = ref(true);
+const deviceCategoryTree = ref() // 设备分类树
+const showButton = ref(true)
 
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
   dialogVisible.value = true
   // 获取store中的设备分类id
-  localDeviceCategoryId.value = treeStore.selectedId;
+  localDeviceCategoryId.value = treeStore.selectedId
   dialogTitle.value = t('action.' + type)
   formType.value = type
   resetForm()
@@ -158,18 +176,18 @@ const getDeviceCategoryTree = async () => {
 /** 处理 设备分类 树 被点击 */
 const handleDeviceCategoryTreeNodeClick = async (row: { [key: string]: any }) => {
   emit('node-click', row)
-  isRepated(row.id);
+  isRepated(row.id)
   localDeviceCategoryId.value = row.id
 }
 
-const isRepated = async(id)=>{
-  showButton.value=true;
-  formData1.value = await DeviceTemplateApi.getAttrTemplateByModelCategoryId(id);
-  if(formData1.value){
+const isRepated = async (id) => {
+  showButton.value = true
+  formData1.value = await DeviceTemplateApi.getAttrTemplateByModelCategoryId(id)
+  if (formData1.value) {
     message.error(t('form.repeatDevice'))
-    showButton.value=false;
-  }else{
-    showButton.value=true;
+    showButton.value = false
+  } else {
+    showButton.value = true
   }
 }
 
@@ -181,8 +199,109 @@ const resetForm = () => {
     code: '',
     id: undefined,
     remark: '',
-    status: CommonStatusEnum.ENABLE,
+    status: CommonStatusEnum.ENABLE
   }
   formRef.value?.resetFields()
 }
 </script>
+
+<style scoped>
+.dialog-title {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+}
+
+.dialog-title__icon {
+  display: flex;
+  width: 40px;
+  height: 40px;
+  color: var(--el-color-primary);
+  background: rgb(64 158 255 / 12%);
+  border: 1px solid rgb(64 158 255 / 18%);
+  border-radius: 8px;
+  align-items: center;
+  justify-content: center;
+}
+
+.dialog-title__text {
+  font-size: 17px;
+  font-weight: 600;
+  line-height: 22px;
+  color: var(--el-text-color-primary);
+}
+
+.dialog-title__desc {
+  margin-top: 3px;
+  font-size: 13px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
+
+.template-form-body {
+  padding: 4px 0;
+}
+
+.template-form-card {
+  overflow: hidden;
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  box-shadow: 0 8px 24px rgb(31 45 61 / 5%);
+}
+
+.template-form-card__header {
+  display: flex;
+  min-height: 52px;
+  padding: 0 18px;
+  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;
+}
+
+.template-form-card__title {
+  display: flex;
+  gap: 10px;
+  font-size: 15px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  align-items: center;
+}
+
+.template-form-card__marker {
+  width: 4px;
+  height: 17px;
+  background: var(--el-color-primary);
+  border-radius: 2px;
+}
+
+.template-form {
+  padding: 20px 22px 4px;
+}
+
+.template-form :deep(.el-form-item) {
+  margin-bottom: 18px;
+}
+
+.template-form :deep(.el-form-item__label) {
+  margin-bottom: 6px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
+
+.dialog-footer {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+}
+
+@media (width <= 768px) {
+  .dialog-title__desc {
+    display: none;
+  }
+
+  .template-form {
+    padding: 16px 16px 0;
+  }
+}
+</style>

+ 268 - 126
src/views/pms/modeltemplate/detail/attrsModel/index.vue

@@ -1,180 +1,94 @@
 <!-- 设备分类属性列表 -->
-<template>
-  <ContentWrap>
-    <!-- 搜索工作栏 -->
-    <el-form
-      ref="queryFormRef"
-      :inline="true"
-      :model="queryParams"
-      class="-mb-15px"
-      label-width="68px"
-    >
-      <el-form-item>
-        <el-button @click="handleQuery">
-          <Icon class="mr-5px" icon="ep:search" />
-          搜索
-        </el-button>
-        <el-button @click="resetQuery">
-          <Icon class="mr-5px" icon="ep:refresh" />
-          重置
-        </el-button>
-        <el-button
-          v-hasPermi="[`rq:iot-model-template-attrs:create`]"
-          plain
-          type="primary"
-          @click="openForm('create')"
-        >
-          <Icon class="mr-5px" icon="ep:plus" />
-          添加属性
-        </el-button>
-      </el-form-item>
-      <el-form-item label="所属模板:" style="width: 200px" label-width="100px">
-        <span>
-          {{modelName}}
-        </span>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
-  <!-- 列表 -->
-  <ContentWrap>
-    <el-tabs>
-      <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
-        <el-table-column align="center" label="属性名称" prop="name" />
-<!--        <el-table-column align="center" label="标识符" prop="code" />-->
-        <el-table-column align="center" label="数据类型" prop="type">
-          <template #default="{ row }">
-            {{ dataTypeOptionsLabel(row.selectOptions?.type) ?? '-' }}
-          </template>
-        </el-table-column>
-        <!--
-        <el-table-column align="left" label="数据定义" prop="code">
-          <template #default="{ row }">
-            <DataDefinition :data="row" />
-          </template>
-        </el-table-column>
-        -->
-        <el-table-column align="center" label="操作">
-          <template #default="scope">
-            <el-button
-              v-hasPermi="[`rq:iot-model-template-attrs:update`]"
-              link
-              type="primary"
-              @click="openForm('update', scope.row.id)"
-            >
-              编辑
-            </el-button>
-            <el-button
-              v-hasPermi="['rq:iot-model-template-attrs:delete']"
-              link
-              type="danger"
-              @click="handleDelete(scope.row.id,scope.row.deviceCategoryId)"
-            >
-              删除
-            </el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-      <!-- 分页 -->
-      <Pagination
-        v-model:limit="queryParams.pageSize"
-        v-model:page="queryParams.pageNo"
-        :total="total"
-        @pagination="getList"
-      />
-    </el-tabs>
-  </ContentWrap>
-  <!-- 表单弹窗:添加/修改 -->
-  <AttrTemplateModelForm ref="formRef" @success="getList" :deviceCategoryId="deviceCategoryId" />
-</template>
 <script lang="ts" setup>
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { DeviceAttrModelApi, DeviceAttrModelData } from '@/api/pms/modelattrtemplate'
 import AttrTemplateModelForm from './AttrTemplateModelForm.vue'
 import { getDataTypeOptionsLabel } from './config'
-import { DataDefinition } from '../components'
-import {useTagsViewStore} from "@/store/modules/tagsView";
+import { useTagsViewStore } from '@/store/modules/tagsView'
+
+defineOptions({ name: 'ModelAttrTemplate' })
+
+interface QueryParams extends PageParam {
+  type?: string
+  deviceCategoryId: string | number
+}
 
-const { delView } = useTagsViewStore() // 视图操作
+const { delView } = useTagsViewStore()
 const route = useRoute()
 const { currentRoute } = useRouter()
+const { t } = useI18n()
+const message = useMessage()
+const { ZmTable, ZmTableColumn } = useTableComponents<DeviceAttrModelData>()
 
-defineOptions({ name: 'ModelAttrTemplate' })
+const routeParams = computed(() => String(route.params.id || '').split(','))
+const deviceCategoryId = computed(() => routeParams.value[0])
+const categoryName = computed(() => routeParams.value[1] || '-')
+const modelName = computed(() => routeParams.value[2] || '-')
 
-const deviceCategoryId = route.params.id.split(",")[0] // 设备分类id
-const categoryName = route.params.id.split(",")[1];//设备分类名称
-const modelName = route.params.id.split(",")[2];//模板名称
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
-
-const loading = ref(true) // 列表的加载中
-const list = ref<DeviceAttrModelData[]>([]) // 列表的数据
-const list1 = ref<DeviceAttrModelData[]>([]) // 列表的数据
-const total = ref(0) // 列表的总页数
-const queryParams = reactive({
+const loading = ref(true)
+const list = ref<DeviceAttrModelData[]>([])
+const list1 = ref<DeviceAttrModelData[]>([])
+const total = ref(0)
+const queryParams = reactive<QueryParams>({
   pageNo: 1,
   pageSize: 10,
   type: undefined,
   deviceCategoryId: -1
 })
 
-const queryFormRef = ref() // 搜索的表单
-const dataTypeOptionsLabel = computed(() => (value: string) => getDataTypeOptionsLabel(value)) // 解析数据类型
+const queryFormRef = ref()
+const dataTypeOptionsLabel = computed(() => (value: string) => getDataTypeOptionsLabel(value))
 
-/** 查询列表 */
 const getList = async () => {
   loading.value = true
   try {
-    queryParams.deviceCategoryId = deviceCategoryId
+    queryParams.deviceCategoryId = deviceCategoryId.value
     const data = await DeviceAttrModelApi.getDeviceAttrModelPage(queryParams)
     list.value = data.list
-    list.value.forEach(function (item,index){
-      if(item.isSum!=1){
-        list1.value.push(item)
-      }
-    })
+    list1.value = data.list.filter((item) => item.isSum !== 1)
     total.value = data.total
   } finally {
     loading.value = false
   }
 }
 
-/** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
 
-/** 重置按钮操作 */
 const resetQuery = () => {
-  queryFormRef.value.resetFields()
+  queryFormRef.value?.resetFields()
   queryParams.type = undefined
   handleQuery()
 }
 
-/** 添加/修改操作 */
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
+  handleQuery()
+}
+
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
+}
+
 const formRef = ref()
-const openForm = (type: string, id?: number,name?:string,list?:[]) => {
-  name = categoryName;
-  list = list1;
-    formRef.value.open(type,id,name,list)
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id, categoryName.value, list1)
 }
 
-/** 删除按钮操作 */
 const handleDelete = async (id: number) => {
   try {
-
-    // 删除的二次确认
     await message.delConfirm()
-    // 发起删除
     await DeviceAttrModelApi.deleteDeviceAttrModel(id)
     message.success(t('common.delSuccess'))
-    // 刷新列表
     await getList()
   } catch {}
 }
 
-/** 初始化 **/
 onMounted(() => {
-  if (!deviceCategoryId) {
+  if (!deviceCategoryId.value) {
     message.warning('参数错误,设备属性模板不能为空!')
     delView(unref(currentRoute))
     return
@@ -182,3 +96,231 @@ onMounted(() => {
   getList()
 })
 </script>
+
+<template>
+  <div
+    class="model-attr-detail h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <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">
+        <div class="detail-grid">
+          <div class="detail-item">
+            <div class="detail-item__label">所属模板</div>
+            <div class="detail-item__value">{{ modelName }}</div>
+          </div>
+          <div class="detail-item">
+            <div class="detail-item__label">设备分类</div>
+            <div class="detail-item__value">{{ categoryName }}</div>
+          </div>
+        </div>
+      </div>
+    </section>
+
+    <section class="info-card attr-card">
+      <div class="info-card__header">
+        <div class="info-card__title">
+          <span class="info-card__marker"></span>
+          <span>属性列表</span>
+        </div>
+        <span class="card-total">共 {{ total }} 条</span>
+      </div>
+
+      <div class="info-card__body attr-card__body">
+        <el-form ref="queryFormRef" :model="queryParams" size="default" class="table-toolbar">
+          <div class="flex items-center gap-2">
+            <el-button @click="handleQuery">
+              <Icon icon="ep:search" class="mr-5px" />搜索
+            </el-button>
+            <el-button @click="resetQuery">
+              <Icon icon="ep:refresh" class="mr-5px" />重置
+            </el-button>
+            <el-button
+              v-hasPermi="[`rq:iot-model-template-attrs:create`]"
+              plain
+              type="primary"
+              @click="openForm('create')">
+              <Icon icon="ep:plus" class="mr-5px" />添加属性
+            </el-button>
+          </div>
+        </el-form>
+
+        <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="序号"
+                  width="70"
+                  fixed="left"
+                  hide-in-column-settings />
+                <ZmTableColumn prop="name" label="属性名称" fixed="left" min-width="180" />
+                <ZmTableColumn prop="type" label="数据类型" min-width="150">
+                  <template #default="{ row }">
+                    {{ dataTypeOptionsLabel(row.selectOptions?.type) ?? '-' }}
+                  </template>
+                </ZmTableColumn>
+                <ZmTableColumn label="操作" width="180" fixed="right" action>
+                  <template #default="{ row }">
+                    <div class="flex items-center justify-center gap-2">
+                      <el-button
+                        v-hasPermi="[`rq:iot-model-template-attrs:update`]"
+                        link
+                        type="primary"
+                        @click="openForm('update', row.id)">
+                        <Icon icon="ep:edit" class="mr-3px" />编辑
+                      </el-button>
+                      <el-button
+                        v-hasPermi="['rq:iot-model-template-attrs:delete']"
+                        link
+                        type="danger"
+                        @click="handleDelete(row.id)">
+                        <Icon icon="ep:delete" class="mr-3px" />删除
+                      </el-button>
+                    </div>
+                  </template>
+                </ZmTableColumn>
+              </ZmTable>
+            </template>
+          </el-auto-resizer>
+        </div>
+
+        <div class="h-8 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>
+    </section>
+
+    <AttrTemplateModelForm ref="formRef" :deviceCategoryId="deviceCategoryId" @success="getList" />
+  </div>
+</template>
+
+<style scoped>
+.model-attr-detail {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.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%);
+}
+
+.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;
+}
+
+.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;
+}
+
+.info-card__marker {
+  width: 4px;
+  height: 18px;
+  background: var(--el-color-primary);
+  border-radius: 2px;
+}
+
+.info-card__body {
+  padding: 20px;
+}
+
+.detail-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 16px;
+}
+
+.detail-item {
+  min-width: 0;
+  padding: 14px 16px;
+  background: var(--el-fill-color-lighter);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+}
+
+.detail-item__label {
+  margin-bottom: 6px;
+  font-size: 13px;
+  line-height: 18px;
+  color: var(--el-text-color-secondary);
+}
+
+.detail-item__value {
+  overflow: hidden;
+  font-size: 15px;
+  font-weight: 600;
+  line-height: 22px;
+  color: var(--el-text-color-primary);
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.attr-card {
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+  min-height: 0;
+}
+
+.attr-card__body {
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+  gap: 12px;
+  min-height: 0;
+}
+
+.card-total {
+  font-size: 13px;
+  color: var(--el-text-color-secondary);
+}
+
+.table-toolbar :deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+@media (width <= 768px) {
+  .detail-grid {
+    grid-template-columns: minmax(0, 1fr);
+  }
+}
+</style>

+ 261 - 224
src/views/pms/modeltemplate/index.vue

@@ -1,202 +1,41 @@
-<template>
-  <doc-alert title="用户体系" url="https://doc.iocoder.cn/user-center/" />
-  <doc-alert title="三方登陆" url="https://doc.iocoder.cn/social-user/" />
-  <doc-alert title="Excel 导入导出" url="https://doc.iocoder.cn/excel-import-and-export/" />
-
-  <el-row :gutter="20">
-    <!-- 左侧 设备分类 树 -->
-    <el-col :span="4" :xs="24">
-      <ContentWrap class="h-1/1">
-        <ModelCategoryTree @node-click="handleModelCategoryTreeNodeClick" />
-      </ContentWrap>
-    </el-col>
-    <el-col :span="20" :xs="24">
-      <!-- 搜索 -->
-      <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('modelTemplate.name')" prop="name" style="margin-left: 45px">
-            <el-input
-              v-model="queryParams.name"
-              :placeholder="t('modelTemplate.nameHolder')"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-240px"
-            />
-          </el-form-item>
-          <!--          <el-form-item label="模板编码" prop="code">
-            <el-input
-              v-model="queryParams.code"
-              placeholder="请输入模板编码"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-240px"
-            />
-          </el-form-item>-->
-          <el-form-item :label="t('modelTemplate.status')" prop="status">
-            <el-select
-              v-model="queryParams.status"
-              :placeholder="t('modelTemplate.status')"
-              clearable
-              class="!w-240px"
-            >
-              <el-option
-                v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </el-form-item>
-          <el-form-item :label="t('modelTemplate.createTime')" prop="createTime">
-            <el-date-picker
-              v-model="queryParams.createTime"
-              value-format="YYYY-MM-DD HH:mm:ss"
-              type="datetimerange"
-              :start-placeholder="t('operationFill.start')"
-              :end-placeholder="t('operationFill.end')"
-              class="!w-240px"
-            />
-          </el-form-item>
-          <el-form-item>
-            <el-button @click="handleQuery"
-              ><Icon icon="ep:search" />{{ t('operationFill.search') }}</el-button
-            >
-            <el-button @click="resetQuery"
-              ><Icon icon="ep:refresh" />{{ t('operationFill.reset') }}</el-button
-            >
-            <el-button
-              type="primary"
-              plain
-              @click="openForm('create')"
-              v-hasPermi="['rq:iot-model-template:create']"
-            >
-              <Icon icon="ep:plus" />{{ t('operationFill.add') }}
-            </el-button>
-          </el-form-item>
-        </el-form>
-      </div>
-      <ContentWrap style="border: none; margin-top: 20px">
-        <zm-table :loading="loading" :data="list">
-          <!--          <el-table-column
-            label="模板编码"
-            align="center"
-            prop="code"
-            :show-overflow-tooltip="true"
-          />-->
-          <zm-table-column
-            :label="t('modelTemplate.name')"
-            align="center"
-            prop="name"
-            :show-overflow-tooltip="true"
-          />
-          <zm-table-column
-            :label="t('modelTemplate.ec')"
-            align="center"
-            key="deviceCategoryName"
-            prop="deviceCategoryName"
-            :show-overflow-tooltip="true"
-          />
-          <zm-table-column :label="t('modelTemplate.status')" key="status">
-            <template #default="scope">
-              <el-switch
-                v-model="scope.row.status"
-                :active-value="0"
-                :inactive-value="1"
-                @change="handleStatusChange(scope.row)"
-                :disabled="!checkPermi(['rq:iot-model-template:update'])"
-              />
-            </template>
-          </zm-table-column>
-          <zm-table-column
-            :label="t('modelTemplate.createTime')"
-            align="center"
-            prop="createTime"
-            :formatter="dateFormatter"
-          />
-          <zm-table-column :label="t('operationFill.operation')" align="center" width="220" action>
-            <template #default="scope">
-              <div class="flex items-center justify-center">
-                <el-button
-                  type="primary"
-                  link
-                  @click="
-                    openDetail(
-                      scope.row.deviceCategoryId +
-                        ',' +
-                        scope.row.deviceCategoryName +
-                        ',' +
-                        scope.row.name
-                    )
-                  "
-                >
-                  <Icon icon="ep:edit" />{{ t('operationFill.view') }}
-                </el-button>
-                <el-button
-                  type="primary"
-                  link
-                  @click="openForm('update', scope.row.id)"
-                  v-hasPermi="['rq:iot-model-template:update']"
-                >
-                  <Icon icon="ep:edit" />{{ t('modelTemplate.update') }}
-                </el-button>
-
-                <el-button
-                  type="danger"
-                  link
-                  @click="handleDelete(scope.row.id, scope.row.deviceCategoryId)"
-                  v-hasPermi="['rq:iot-model-template:delete']"
-                >
-                  <Icon icon="ep:delete" />{{ t('modelTemplate.delete') }}
-                </el-button>
-              </div>
-            </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>
-
-  <!-- 添加或修改 属性模板 对话框 -->
-  <TemplateForm ref="formRef" :category_id="selectedId" @success="getList" />
-</template>
 <script lang="ts" setup>
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { checkPermi } from '@/utils/permission'
-import { dateFormatter } from '@/utils/formatTime'
+import { dateFormatter, rangeShortcuts } from '@/utils/formatTime'
 import { CommonStatusEnum } from '@/utils/constants'
 import * as ModelTemplateApi from '@/api/pms/modeltemplate'
+import DeviceCategoryTree from '@/components/DeviceCategoryTree/index.vue'
 import TemplateForm from './TemplateForm.vue'
-import ModelCategoryTree from './ModelCategoryTree.vue'
 import { useTreeStore } from '@/store/modules/attrTemplateTreeStore'
-import { useTableComponents } from '@/components/ZmTable/useTableComponents'
-const { ZmTable, ZmTableColumn } = useTableComponents()
 
 defineOptions({ name: 'ModelAttrsTemplate' })
 
-const treeStore = useTreeStore()
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
+type ModelTemplateRow = Omit<ModelTemplateApi.ModelAttrTemplateVO, 'status'> & {
+  status?: number
+  deviceCategoryName?: string
+  createTime?: string
+}
 
+interface QueryParams extends PageParam {
+  name?: string
+  code?: string
+  status?: number
+  deviceCategoryId?: number
+  deviceCategoryName?: string
+  createTime?: string[]
+}
+
+const treeStore = useTreeStore()
+const message = useMessage()
+const { t } = useI18n()
 const { push } = useRouter()
-const loading = ref(true) // 列表的加载中
-const total = ref(0) // 列表的总页数
-const list = ref([]) // 列表的数
-const queryParams = reactive({
+const { ZmTable, ZmTableColumn } = useTableComponents<ModelTemplateRow>()
+
+const loading = ref(true)
+const total = ref(0)
+const list = ref<ModelTemplateRow[]>([])
+const queryParams = reactive<QueryParams>({
   pageNo: 1,
   pageSize: 10,
   name: undefined,
@@ -206,81 +45,106 @@ const queryParams = reactive({
   deviceCategoryName: undefined,
   createTime: []
 })
-const queryFormRef = ref() // 搜索的表单
+const queryFormRef = ref()
 
-// 从 Store 中获取左侧 设备分类树 选中的 节点ID
 const selectedId = computed(() => treeStore.selectedId)
 
-const isInitialized = ref(false)
-/** 查询 设备属性模板 列表 */
+const normalizeStatus = (status: unknown) => {
+  const value = Number(status)
+  if (value === CommonStatusEnum.ENABLE || value === CommonStatusEnum.DISABLE) {
+    return value
+  }
+  return undefined
+}
+
+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 getList = async () => {
   loading.value = true
   try {
     const data = await ModelTemplateApi.getModelTemplatePage(queryParams)
-    list.value = data.list
+    list.value = data.list.map((item: ModelTemplateRow) => ({
+      ...item,
+      status: normalizeStatus(item.status)
+    }))
     total.value = data.total
-    // 数据加载完成后,标记为已初始化
-    setTimeout(() => {
-      isInitialized.value = true
-    }, 100)
   } finally {
     loading.value = false
   }
 }
 
-/** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
 }
 
-/** 重置按钮操作 */
 const resetQuery = () => {
   queryFormRef.value?.resetFields()
   handleQuery()
 }
 
-/** 处理 设备分类树 被点击 */
-const handleModelCategoryTreeNodeClick = async (row) => {
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val
+  handleQuery()
+}
+
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNo = val
+  getList()
+}
+
+const handleDeviceCategoryTreeNodeClick = async (row: Tree) => {
+  treeStore.setSelectedId(row.id)
   queryParams.deviceCategoryId = row.id
+  queryParams.pageNo = 1
   await getList()
 }
 
-/** 添加/修改操作 */
 const formRef = ref()
 const openForm = (type: string, id?: number) => {
   formRef.value.open(type, id)
 }
 
-/** 打开详情 */
-const openDetail = (id: string) => {
-  push({ name: 'ModelAttrTemplate', params: { id } })
+const openDetail = (row: ModelTemplateRow) => {
+  push({
+    name: 'ModelAttrTemplate',
+    params: {
+      id: `${row.deviceCategoryId},${row.deviceCategoryName},${row.name}`
+    }
+  })
 }
 
-/** 修改 设备属性模板 状态 */
-const handleStatusChange = async (row: ModelTemplateApi.ModelAttrTemplateVO) => {
-  // 如果还未初始化完成,直接返回
-  if (!isInitialized.value) {
+const handleStatusChange = async (row: ModelTemplateRow, nextStatus: string | number | boolean) => {
+  const status = normalizeStatus(nextStatus)
+  const oldStatus = normalizeStatus(row.status)
+  if (
+    !row.id ||
+    !row.name ||
+    status === undefined ||
+    oldStatus === undefined ||
+    status === oldStatus
+  ) {
     return
   }
 
   try {
-    // 修改状态的二次确认
-    const text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用'
-    await message.confirm('确认要"' + text + '""' + row.name + '"属性模板吗?')
-    // 发起修改状态
-    await ModelTemplateApi.updateModelTemplateStatus(row.id, row.status)
-    // 刷新列表
+    await message.confirm(`确认要${getStatusText(status)}"${row.name}"属性模板吗?`)
+    row.status = status
+    await ModelTemplateApi.updateModelTemplateStatus(row.id, status)
     await getList()
   } catch {
-    // 取消后,进行恢复按钮
-    row.status =
-      row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
+    row.status = oldStatus
   }
 }
 
-/** 操作分发 */
-const handleCommand = (command: string, row: ModelTemplateApi.ModelAttrTemplateVO) => {
+const handleCommand = (command: string, row: ModelTemplateRow) => {
   switch (command) {
     case 'handleDelete':
       handleDelete(row.id, row.deviceCategoryId)
@@ -290,27 +154,200 @@ const handleCommand = (command: string, row: ModelTemplateApi.ModelAttrTemplateV
   }
 }
 
-/** 删除按钮操作 */
 const handleDelete = async (id: number | undefined, deviceId: number) => {
   try {
     const data = await ModelTemplateApi.getIsRelated(deviceId)
     if (data.length === 0) {
-      // 删除的二次确认
       await message.delConfirm()
-      // 发起删除
       await ModelTemplateApi.deleteModelTemplate(id)
       message.success(t('common.delSuccess'))
     } else {
       message.error(t('form.relatedModel'))
     }
 
-    // 刷新列表
     await getList()
   } catch {}
 }
 
-/** 初始化 */
 onMounted(() => {
   getList()
 })
 </script>
+
+<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))]">
+    <DeviceCategoryTree
+      v-model="queryParams.deviceCategoryId"
+      :show-title="false"
+      class="row-span-2"
+      @node-click="handleDeviceCategoryTreeNodeClick" />
+
+    <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('modelTemplate.name')" prop="name">
+          <el-input
+            v-model="queryParams.name"
+            :placeholder="t('modelTemplate.nameHolder')"
+            clearable
+            class="!w-200px"
+            @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item :label="t('modelTemplate.status')" prop="status">
+          <el-select
+            v-model="queryParams.status"
+            :placeholder="t('modelTemplate.status')"
+            clearable
+            class="!w-180px">
+            <el-option
+              v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="t('modelTemplate.createTime')" prop="createTime">
+          <el-date-picker
+            v-model="queryParams.createTime"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            type="datetimerange"
+            :start-placeholder="t('operationFill.start')"
+            :end-placeholder="t('operationFill.end')"
+            :shortcuts="rangeShortcuts"
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+            class="!w-320px" />
+        </el-form-item>
+      </div>
+
+      <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-model-template:create']">
+          <Icon icon="ep:plus" class="mr-5px" />{{ t('operationFill.add') }}
+        </el-button>
+      </el-form-item>
+    </el-form>
+
+    <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="name"
+                :label="t('modelTemplate.name')"
+                fixed="left"
+                min-width="180"
+                show-overflow-tooltip />
+              <ZmTableColumn
+                prop="deviceCategoryName"
+                :label="t('modelTemplate.ec')"
+                min-width="160"
+                show-overflow-tooltip />
+              <ZmTableColumn
+                prop="status"
+                :label="t('modelTemplate.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-model-template:update'])"
+                    @change="(status) => handleStatusChange(row, status)" />
+                  <el-tag v-else type="info">未知</el-tag>
+                </template>
+              </ZmTableColumn>
+              <ZmTableColumn
+                prop="createTime"
+                :label="t('modelTemplate.createTime')"
+                :formatter="dateFormatter"
+                width="180" />
+              <ZmTableColumn :label="t('operationFill.operation')" width="220" fixed="right" action>
+                <template #default="{ row }">
+                  <div class="flex items-center justify-center gap-2">
+                    <el-button type="primary" link @click="openDetail(row)">
+                      <Icon icon="ep:view" class="mr-3px" />{{ t('operationFill.view') }}
+                    </el-button>
+                    <el-button
+                      type="primary"
+                      link
+                      @click="openForm('update', row.id)"
+                      v-hasPermi="['rq:iot-model-template:update']">
+                      <Icon icon="ep:edit" class="mr-3px" />{{ t('modelTemplate.update') }}
+                    </el-button>
+                    <el-dropdown
+                      @command="(command) => handleCommand(command, row)"
+                      v-hasPermi="['rq:iot-model-template:delete']">
+                      <el-button type="primary" link>
+                        <Icon icon="ep:more-filled" class="mr-3px" />更多
+                      </el-button>
+                      <template #dropdown>
+                        <el-dropdown-menu>
+                          <el-dropdown-item
+                            command="handleDelete"
+                            v-if="checkPermi(['rq:iot-model-template:delete'])">
+                            <Icon icon="ep:delete" class="mr-5px" />{{ t('modelTemplate.delete') }}
+                          </el-dropdown-item>
+                        </el-dropdown-menu>
+                      </template>
+                    </el-dropdown>
+                  </div>
+                </template>
+              </ZmTableColumn>
+            </ZmTable>
+          </template>
+        </el-auto-resizer>
+      </div>
+
+      <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>
+
+  <TemplateForm ref="formRef" :category_id="selectedId" @success="getList" />
+</template>
+
+<style scoped>
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+</style>