From aaf8055df7f72d305b748248d7923dbf662be19b Mon Sep 17 00:00:00 2001
From: dzq <dzq@ys.com>
Date: Thu, 15 May 2025 10:03:15 +0800
Subject: [PATCH] =?UTF-8?q?feat(=E6=99=BA=E8=83=BD=E6=9F=9C):=20=E6=96=B0?=
 =?UTF-8?q?=E5=A2=9E=E6=99=BA=E8=83=BD=E6=9F=9C=E8=AF=A6=E6=83=85=E9=A1=B5?=
 =?UTF-8?q?=E9=9D=A2=E5=8F=8A=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 添加智能柜详情页面,展示柜体信息及配置选项
- 新增智能柜详情API接口
- 在路由中配置智能柜详情页面路径
- 优化智能柜配置模态框,增加数据加载逻辑
- 新增智能柜图片映射工具文件
---
 src/api/cabinet/smart-cabinet.ts              |   4 +
 src/router/modules/global.ts                  |   8 +
 src/utils/cabinetImgMap.ts                    |  34 +++
 .../cabinet/smart-cabinet-card/detail.vue     | 160 +++++++++++++
 .../cabinet/smart-cabinet-card/index.vue      | 220 ++++++++++++++++++
 .../smart-cabinet-card-form-modal.vue         | 109 +++++++++
 .../smart-cabinet/GatewayConfigModal.vue      |   4 +
 .../cabinet/smart-cabinet/ShopConfigModal.vue |   3 +
 src/views/user/ab98/index.vue                 |   2 +
 9 files changed, 544 insertions(+)
 create mode 100644 src/utils/cabinetImgMap.ts
 create mode 100644 src/views/cabinet/smart-cabinet-card/detail.vue
 create mode 100644 src/views/cabinet/smart-cabinet-card/index.vue
 create mode 100644 src/views/cabinet/smart-cabinet-card/smart-cabinet-card-form-modal.vue

diff --git a/src/api/cabinet/smart-cabinet.ts b/src/api/cabinet/smart-cabinet.ts
index 25a4160..4fce1df 100644
--- a/src/api/cabinet/smart-cabinet.ts
+++ b/src/api/cabinet/smart-cabinet.ts
@@ -78,4 +78,8 @@ export const getAllCabinetsWithCells = () => {
 
 export const allCabinets = () => {
   return http.request<ResponseData<Array<SmartCabinetDTO>>>('get', '/cabinet/smartCabinet/allCabinets');
+};
+
+export const getSmartCabinetDetailApi = (cabinetId: number) => {
+  return http.request<ResponseData<SmartCabinetDTO>>("get", `/cabinet/smartCabinet/detail/${cabinetId}`);
 };
\ No newline at end of file
diff --git a/src/router/modules/global.ts b/src/router/modules/global.ts
index e6d7c43..e2ec14b 100644
--- a/src/router/modules/global.ts
+++ b/src/router/modules/global.ts
@@ -23,6 +23,14 @@ export default {
       meta: {
         title: "会员详情"
       }
+    },
+    {
+      path: "/cabinet/card/detail",
+      name: "smartCabinetCardDetail",
+      component: () => import("@/views/cabinet/smart-cabinet-card/detail.vue"),
+      meta: {
+        title: "柜机详情"
+      }
     }
   ]
 } as RouteConfigsTable;
diff --git a/src/utils/cabinetImgMap.ts b/src/utils/cabinetImgMap.ts
new file mode 100644
index 0000000..3238bd4
--- /dev/null
+++ b/src/utils/cabinetImgMap.ts
@@ -0,0 +1,34 @@
+export const CabinetImgMap = {
+  1: {
+    img: "cabinet_16.jpg",
+    name: "16口机柜",
+  },
+  2: {
+    img: "cabinet_20.jpg",
+    name: "20口机柜",
+  },
+  3: {
+    img: "cabinet_22.jpg",
+    name: "22口机柜",
+  },
+  4: {
+    img: "cabinet_24.jpg",
+    name: "24口机柜",
+  },
+  5: {
+    img: "cabinet_40.jpg",
+    name: "40口机柜",
+  },
+  6: {
+    img: "cabinet_48.jpg",
+    name: "48口机柜",
+  },
+  7: {
+    img: "cabinet_60.jpg",
+    name: "60口机柜",
+  },
+  8: {
+    img: "cabinet_120.jpg",
+    name: "120口机柜",
+  },
+}
\ No newline at end of file
diff --git a/src/views/cabinet/smart-cabinet-card/detail.vue b/src/views/cabinet/smart-cabinet-card/detail.vue
new file mode 100644
index 0000000..1528409
--- /dev/null
+++ b/src/views/cabinet/smart-cabinet-card/detail.vue
@@ -0,0 +1,160 @@
+<script setup lang="ts">
+import { ref, onMounted } from "vue";
+import { useRoute } from "vue-router";
+import { getSmartCabinetDetailApi, type SmartCabinetDTO } from "@/api/cabinet/smart-cabinet";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import { CabinetImgMap } from "@/utils/cabinetImgMap";
+import GatewayConfigModal from "@/views/cabinet/smart-cabinet/GatewayConfigModal.vue";
+import ShopConfigModal from "@/views/cabinet/smart-cabinet/ShopConfigModal.vue";
+import MainCabinetConfigModal from "@/views/cabinet/smart-cabinet/MainCabinetConfigModal.vue";
+
+defineOptions({
+  name: "SmartCabinetDetail"
+});
+
+const route = useRoute();
+const cabinetInfo = ref<SmartCabinetDTO>({
+  cabinetName: "",
+  cabinetType: 0,
+  templateNo: "",
+  lockControlNo: 0,
+  location: 0
+});
+const loading = ref(false);
+const activeTab = ref('basic');
+const cabinetId = ref<number>(0);
+const gatewayConfigVisible = ref(false);
+const shopConfigVisible = ref(false);
+const mainCabinetConfigVisible = ref(false);
+
+async function fetchCabinetDetail() {
+  try {
+    loading.value = true;
+    const { data } = await getSmartCabinetDetailApi(cabinetId.value);
+    cabinetInfo.value = data;
+  } finally {
+    loading.value = false;
+  }
+}
+
+onMounted(() => {
+  cabinetId.value = Number(route.query.id);
+  fetchCabinetDetail();
+});
+</script>
+
+<template>
+  <div class="detail-container">
+    <div class="flex-container">
+      <el-card class="cabinet-info-card">
+        <div class="cabinet-header">
+          <img :src="`/img/cabinet/${CabinetImgMap[cabinetInfo.templateNo]?.img || 'default.jpg'}`"
+            class="cabinet-image" />
+          <div class="cabinet-name">{{ cabinetInfo.cabinetName }}</div>
+        </div>
+
+        <el-divider />
+
+        <el-descriptions class="cabinet-details" :column="1" border>
+          <el-descriptions-item label="柜体ID">{{ cabinetInfo.cabinetId }}</el-descriptions-item>
+          <el-descriptions-item label="柜体类型">
+            {{ cabinetInfo.cabinetType === 0 ? '主柜' : '副柜' }}
+          </el-descriptions-item>
+          <el-descriptions-item label="模板">
+            {{ CabinetImgMap[cabinetInfo.templateNo]?.name || '-' }}
+          </el-descriptions-item>
+          <el-descriptions-item label="归属商店">
+            {{ cabinetInfo.shopName || '-' }}
+            <el-button type="success" link :icon="useRenderIcon('ep:shop')" @click="shopConfigVisible = true">
+              配置
+            </el-button>
+          </el-descriptions-item>
+          <el-descriptions-item label="MQTT网关">
+            {{ cabinetInfo.mqttServerId || '-' }}
+            <el-button type="warning" link :icon="useRenderIcon('ant-design:gateway-outlined')"
+              @click="gatewayConfigVisible = true">
+              配置
+            </el-button>
+          </el-descriptions-item>
+          <el-descriptions-item label="归属主柜" v-if="cabinetInfo.cabinetType === 1">
+            {{ cabinetInfo.mainCabinetName || '-' }}
+            <el-button type="primary" link :icon="useRenderIcon('ep:setting')" @click="mainCabinetConfigVisible = true">
+              配置
+            </el-button>
+          </el-descriptions-item>
+        </el-descriptions>
+      </el-card>
+
+      <el-card class="info-card">
+        <div class="tab-header">
+          <el-tabs type="card" v-model="activeTab">
+            <el-tab-pane label="基本信息" name="basic"></el-tab-pane>
+            <el-tab-pane label="单元格配置" name="cells"></el-tab-pane>
+          </el-tabs>
+        </div>
+
+        <el-descriptions class="info-details" v-if="activeTab === 'basic'" :column="2" border>
+          <el-descriptions-item label="主柜ID">{{ cabinetInfo.mainCabinet || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="主柜名称">{{ cabinetInfo.mainCabinetName || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="MQTT服务器ID">{{ cabinetInfo.mqttServerId || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="操作员">{{ cabinetInfo.operator || '-' }}</el-descriptions-item>
+        </el-descriptions>
+
+        <div class="info-details" v-if="activeTab === 'cells'">
+          <!-- 单元格配置内容 -->
+        </div>
+      </el-card>
+      <GatewayConfigModal v-model="gatewayConfigVisible" :cabinet-id="cabinetId"
+        @refresh="fetchCabinetDetail" />
+      <ShopConfigModal v-model="shopConfigVisible" :cabinet-id="cabinetId" @refresh="fetchCabinetDetail" />
+      <MainCabinetConfigModal v-model="mainCabinetConfigVisible" :cabinet-id="cabinetId"
+        @refresh="fetchCabinetDetail" />
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.detail-container {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+
+  .flex-container {
+    display: flex;
+    flex: 1;
+    gap: 20px;
+    min-height: 0;
+
+    .cabinet-info-card {
+      width: 30%;
+    }
+
+    .info-card {
+      width: 70%;
+    }
+  }
+
+  .cabinet-header {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    margin-bottom: 20px;
+
+    .cabinet-image {
+      width: 100%;
+      height: 420px;
+      object-fit: contain;
+      margin-bottom: 15px;
+    }
+
+    .cabinet-name {
+      font-size: 20px;
+      font-weight: bold;
+    }
+  }
+
+  .tab-header {
+    margin-bottom: 20px;
+  }
+}
+</style>
\ No newline at end of file
diff --git a/src/views/cabinet/smart-cabinet-card/index.vue b/src/views/cabinet/smart-cabinet-card/index.vue
new file mode 100644
index 0000000..def280d
--- /dev/null
+++ b/src/views/cabinet/smart-cabinet-card/index.vue
@@ -0,0 +1,220 @@
+<script setup lang="ts">
+import { onMounted, reactive, ref } from "vue";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import { getSmartCabinetList, type SmartCabinetDTO } from "@/api/cabinet/smart-cabinet";
+import { type PaginationProps } from "@pureadmin/table";
+import { CommonUtils } from "@/utils/common";
+import { useRouter } from "vue-router";
+import { CabinetImgMap } from "@/utils/cabinetImgMap";
+
+import Search from "@iconify-icons/ep/search";
+import Refresh from "@iconify-icons/ep/refresh";
+import View from "@iconify-icons/ep/view";
+import AddFill from "@iconify-icons/ri/add-circle-line";
+import SmartCabinetCardFormModal from "./smart-cabinet-card-form-modal.vue";
+
+defineOptions({
+  name: "SmartCabinetCard"
+});
+
+const router = useRouter();
+const formRef = ref();
+const modalVisible = ref(false);
+const searchFormParams = ref({
+  cabinetName: "",
+  cabinetType: null
+});
+
+const pageLoading = ref(false);
+const dataList = ref<SmartCabinetDTO[]>([]);
+const pagination = ref<PaginationProps>({
+  total: 0,
+  pageSize: 5,
+  currentPage: 1,
+  background: true
+});
+
+async function onSearch() {
+  pagination.value.currentPage = 1;
+  getList();
+}
+
+async function getList() {
+  try {
+    pageLoading.value = true;
+    const { data } = await getSmartCabinetList({
+      ...searchFormParams.value,
+      pageSize: pagination.value.pageSize,
+      pageNum: pagination.value.currentPage
+    });
+    dataList.value = data.rows;
+    pagination.value.total = data.total;
+  } finally {
+    pageLoading.value = false;
+  }
+}
+
+const resetForm = formEl => {
+  if (!formEl) return;
+  formEl.resetFields();
+  onSearch();
+};
+
+const handleViewDetail = (row: SmartCabinetDTO) => {
+  router.push({
+    path: '/cabinet/card/detail',
+    query: {
+      id: row.cabinetId
+    }
+  });
+};
+
+onMounted(() => {
+  getList();
+});
+</script>
+
+<template>
+  <div class="main">
+    <div class="float-right w-full">
+      <el-form ref="formRef" :inline="true" :model="searchFormParams"
+        class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]">
+        <el-form-item label="柜体名称:" prop="cabinetName">
+          <el-input v-model="searchFormParams.cabinetName" placeholder="请输入柜体名称" clearable class="!w-[200px]" />
+        </el-form-item>
+        <el-form-item label="柜体类型:" prop="cabinetType">
+          <el-select v-model="searchFormParams.cabinetType" placeholder="请选择类型" clearable class="!w-[180px]">
+            <el-option label="主柜" :value="0" />
+            <el-option label="副柜" :value="1" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" :icon="useRenderIcon(Search)" :loading="pageLoading" @click="onSearch">
+            搜索
+          </el-button>
+        </el-form-item>
+        <el-form-item>
+          <el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
+            重置
+          </el-button>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" :icon="useRenderIcon(AddFill)" @click="modalVisible = true"
+            style="margin-right: 10px;">
+            新增设备
+          </el-button>
+        </el-form-item>
+      </el-form>
+
+      <div class="grid-container">
+        <el-row :gutter="20">
+          <el-col v-for="(item, index) in dataList" :key="item.cabinetId" :xs="24" :sm="12" :md="8" :lg="4" :xl="4">
+            <el-card class="cabinet-card" :body-style="{ padding: '8px 20px' }">
+              <div class="card-content">
+                <img :src="`/img/cabinet/${CabinetImgMap[item.templateNo]?.img || 'default.jpg'}`"
+                  class="cabinet-image" />
+                <div class="cabinet-info">
+                  <div class="name">柜体名称:{{ item.cabinetName }}</div>
+                  <div class="type">柜体类型:{{ item.cabinetType === 0 ? '主柜' : '副柜' }}</div>
+                  <div class="template">模板:{{ CabinetImgMap[item.templateNo]?.name || '-' }}</div>
+                  <div class="shop">归属商店:{{ item.shopName || '-' }}</div>
+                </div>
+              </div>
+              <el-divider class="divider" />
+              <el-button class="detail-btn" :icon="useRenderIcon(View)" @click="handleViewDetail(item)" />
+            </el-card>
+          </el-col>
+        </el-row>
+        <div class="pagination-wrapper">
+          <el-pagination background layout="prev, pager, next" :page-size="pagination.pageSize"
+            :total="pagination.total" v-model:current-page="pagination.currentPage" @current-change="getList"
+            @size-change="getList" />
+        </div>
+      </div>
+    </div>
+    <smart-cabinet-card-form-modal v-model:visible="modalVisible" @refresh="getList" />
+  </div>
+</template>
+
+<style scoped lang="scss">
+.search-form {
+  :deep(.el-form-item) {
+    margin-bottom: 12px;
+  }
+}
+
+.cabinet-card {
+  margin-bottom: 20px;
+  min-height: 210px;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+
+  .cabinet-image {
+    width: 100%;
+    height: 420px;
+    object-fit: cover;
+    border-radius: 4px;
+    margin-bottom: 10px;
+  }
+
+  .card-content {
+    flex: 1;
+    margin: 15px 0px;
+
+    .cabinet-info {
+      text-align: left;
+
+      .name,
+      .type,
+      .shop,
+      .location,
+      .template {
+        font-size: 14px;
+        color: #606266;
+        margin-bottom: 6px;
+        line-height: 1.5;
+      }
+
+      .name {
+        font-weight: 500;
+        color: #303133;
+      }
+    }
+  }
+
+  .divider {
+    margin: 10px 0px;
+  }
+
+  .detail-btn {
+    width: 100%;
+    border: 0;
+    margin-top: auto;
+    padding: 12px 0;
+  }
+}
+
+.grid-container {
+  margin: 20px 0;
+  padding-bottom: 60px;
+  position: relative;
+
+  .el-row {
+    margin-bottom: -20px;
+  }
+}
+
+.pagination-wrapper {
+  position: relative;
+  background: var(--el-bg-color);
+  padding: 12px 12px;
+  margin-top: 20px;
+  text-align: center;
+
+  :deep(.el-pagination) {
+    margin: 0;
+    padding: 8px 0;
+  }
+}
+</style>
\ No newline at end of file
diff --git a/src/views/cabinet/smart-cabinet-card/smart-cabinet-card-form-modal.vue b/src/views/cabinet/smart-cabinet-card/smart-cabinet-card-form-modal.vue
new file mode 100644
index 0000000..b94c401
--- /dev/null
+++ b/src/views/cabinet/smart-cabinet-card/smart-cabinet-card-form-modal.vue
@@ -0,0 +1,109 @@
+<script setup lang="ts">
+import { ref, reactive } from "vue";
+import { ElMessage } from "element-plus";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import { addSmartCabinet } from "@/api/cabinet/smart-cabinet";
+import Confirm from "@iconify-icons/ep/check";
+import type { FormRules } from 'element-plus';
+import { CabinetImgMap } from "@/utils/cabinetImgMap";
+
+export interface FormDTO {
+  cabinetName: string;
+  cabinetType: number;
+  templateNo: string;
+  lockControlNo: number;
+  location: number;
+}
+
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    default: false
+  }
+});
+
+const emit = defineEmits(["update:visible", "refresh"]);
+
+const formRef = ref();
+const formData = reactive<FormDTO>({
+  cabinetName: "",
+  cabinetType: 1,
+  templateNo: "",
+  lockControlNo: 0,
+  location: 0
+});
+
+const rules = reactive<FormRules>({
+  cabinetName: [{ required: true, message: "柜体名称必填", trigger: "blur" }],
+  cabinetType: [{ required: true, message: "请选择柜体类型", trigger: "change" }],
+  templateNo: [{ required: true, message: "请选择模板编号", trigger: "change" }],
+  lockControlNo: [
+    { required: true, message: "锁控板序号", trigger: "blur" }
+  ],
+  location: [
+    { required: true, message: "位置信息必填", trigger: "blur" },
+    { type: 'number', min: 0, message: '位置编号不能为负数', trigger: 'blur' }
+  ]
+});
+
+const handleConfirm = async () => {
+  try {
+    await formRef.value.validate();
+    await addSmartCabinet(formData);
+    ElMessage.success("新增成功");
+    emit("refresh");
+    closeDialog();
+  } catch (error) {
+    console.error("表单提交失败", error);
+  }
+};
+
+const closeDialog = () => {
+  formRef.value.resetFields();
+  emit("update:visible", false);
+};
+
+const templateOptions = Object.entries(CabinetImgMap).map(([value, item]) => ({
+  label: item.name,
+  value
+}));
+</script>
+
+<template>
+  <el-dialog title="新增智能柜" :model-value="visible" width="600px" @close="closeDialog">
+    <el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
+      <el-form-item label="柜体名称" prop="cabinetName">
+        <el-input v-model="formData.cabinetName" placeholder="请输入柜体名称" />
+      </el-form-item>
+
+      <el-form-item label="柜体类型" prop="cabinetType">
+        <el-select v-model="formData.cabinetType" placeholder="请选择类型">
+          <el-option label="主柜" :value="0" />
+          <el-option label="副柜" :value="1" />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="模板编号" prop="templateNo">
+        <el-select v-model="formData.templateNo" placeholder="请选择模板编号">
+          <el-option v-for="option in templateOptions" :key="option.value" :label="option.label"
+            :value="option.value" />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="锁控板序号" prop="lockControlNo">
+        <el-input v-model="formData.lockControlNo" placeholder="请输入锁控板序号" />
+      </el-form-item>
+
+      <el-form-item label="位置" prop="location">
+        <el-input-number v-model="formData.location" :min="0" controls-position="right" />
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="closeDialog">取消</el-button>
+      <el-button type="primary" :icon="useRenderIcon(Confirm)" @click="handleConfirm">
+        确认
+      </el-button>
+    </template>
+  </el-dialog>
+</template>
\ No newline at end of file
diff --git a/src/views/cabinet/smart-cabinet/GatewayConfigModal.vue b/src/views/cabinet/smart-cabinet/GatewayConfigModal.vue
index 3b9006e..eb342a9 100644
--- a/src/views/cabinet/smart-cabinet/GatewayConfigModal.vue
+++ b/src/views/cabinet/smart-cabinet/GatewayConfigModal.vue
@@ -4,6 +4,7 @@
       <el-table-column prop="mqttServerId" label="服务ID" width="100" />
       <el-table-column prop="serverUrl" label="服务地址" />
       <el-table-column prop="username" label="用户名" width="120" />
+      <el-table-column prop="publishTopic" label="发布主题" />
       <el-table-column label="操作" width="80" fixed="right">
         <template #default="{ row }">
           <el-button link type="primary" @click="handleConfig(row)">配置</el-button>
@@ -69,6 +70,9 @@ const onCurrentChange = (val: number) => {
 
 watch(() => props.modelValue, (val) => {
   visible.value = val;
+  if (val) {
+    loadMqttServers();
+  }
 });
 
 watch(() => props.cabinetId, (newVal) => {
diff --git a/src/views/cabinet/smart-cabinet/ShopConfigModal.vue b/src/views/cabinet/smart-cabinet/ShopConfigModal.vue
index 3f22241..329e450 100644
--- a/src/views/cabinet/smart-cabinet/ShopConfigModal.vue
+++ b/src/views/cabinet/smart-cabinet/ShopConfigModal.vue
@@ -68,6 +68,9 @@ const onCurrentChange = (val: number) => {
 
 watch(() => props.modelValue, (val) => {
   visible.value = val;
+  if (val) {
+    loadShops();
+  }
 });
 
 watch(() => props.cabinetId, (newVal) => {
diff --git a/src/views/user/ab98/index.vue b/src/views/user/ab98/index.vue
index a3fd1cf..afb89f7 100644
--- a/src/views/user/ab98/index.vue
+++ b/src/views/user/ab98/index.vue
@@ -88,6 +88,8 @@ onMounted(() => {
           <el-button type="primary" :icon="useRenderIcon(Search)" :loading="pageLoading" @click="onSearch">
             搜索
           </el-button>
+        </el-form-item>
+        <el-form-item>
           <el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
             重置
           </el-button>