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>