|
|
|
|
@ -1,154 +1,208 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="page-container fade-in-up">
|
|
|
|
|
<div class="page-header">
|
|
|
|
|
<h2 class="page-title">教学楼信息</h2>
|
|
|
|
|
<h2 class="page-title">教室信息</h2>
|
|
|
|
|
<p class="page-subtitle">管理教学楼、教室及摄像头设备信息</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 教学楼区域 -->
|
|
|
|
|
<div class="section-card">
|
|
|
|
|
<div class="section-title">
|
|
|
|
|
<el-icon :size="18"><OfficeBuilding /></el-icon>
|
|
|
|
|
<span>教学楼列表</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="filter-bar">
|
|
|
|
|
<el-input v-model="buildingSearch" placeholder="搜索教学楼名称..." :prefix-icon="Search" clearable size="default" style="width: 240px" />
|
|
|
|
|
<el-button type="primary" :icon="Plus" @click="showBuildingDialog()">添加教学楼</el-button>
|
|
|
|
|
<el-button :icon="Delete" :disabled="selectedBuildings.length === 0" @click="batchDeleteBuilding">批量删除</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="data-table-card">
|
|
|
|
|
<el-table
|
|
|
|
|
:data="buildings"
|
|
|
|
|
stripe
|
|
|
|
|
@selection-change="handleBuildingSelect"
|
|
|
|
|
row-key="id"
|
|
|
|
|
highlight-current-row
|
|
|
|
|
@current-change="handleBuildingRowClick"
|
|
|
|
|
>
|
|
|
|
|
<el-table-column type="selection" width="45" />
|
|
|
|
|
<el-table-column prop="name" label="教学楼名称" min-width="220" />
|
|
|
|
|
<el-table-column prop="floorCount" label="楼层数" width="100" align="center" />
|
|
|
|
|
<el-table-column prop="roomCount" label="教室数量" width="100" align="center" />
|
|
|
|
|
<el-table-column prop="status" label="状态" width="90" align="center">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-tag :type="row.status === '启用' ? 'success' : 'info'" size="small">{{ row.status }}</el-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="操作" width="160" align="center" fixed="right">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-button link type="primary" size="small" :icon="Edit" @click.stop="showBuildingDialog(row)">编辑</el-button>
|
|
|
|
|
<el-button link type="danger" size="small" :icon="Delete" @click.stop="deleteBuildingRow(row)">删除</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
<el-pagination
|
|
|
|
|
v-model:current-page="buildingPage"
|
|
|
|
|
:total="buildingTotal"
|
|
|
|
|
:page-size="buildingPageSize"
|
|
|
|
|
size="small"
|
|
|
|
|
background
|
|
|
|
|
layout="total, prev, pager, next"
|
|
|
|
|
style="justify-content: flex-end; margin-top: 16px"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 教室区域 -->
|
|
|
|
|
<div class="section-card" v-if="currentBuilding">
|
|
|
|
|
<div class="section-title">
|
|
|
|
|
<el-icon :size="18"><HomeFilled /></el-icon>
|
|
|
|
|
<span>{{ currentBuilding.name }} — 教室列表</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="filter-bar">
|
|
|
|
|
<el-input v-model="roomSearch" placeholder="搜索教室编号..." :prefix-icon="Search" clearable size="default" style="width: 240px" />
|
|
|
|
|
<el-button type="primary" :icon="Plus" @click="showRoomDialog()">添加教室</el-button>
|
|
|
|
|
<el-button :icon="Delete" :disabled="selectedRooms.length === 0" @click="batchDeleteRoom">批量删除</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="data-table-card">
|
|
|
|
|
<el-table
|
|
|
|
|
:data="filteredRooms"
|
|
|
|
|
stripe
|
|
|
|
|
@selection-change="handleRoomSelect"
|
|
|
|
|
row-key="id"
|
|
|
|
|
highlight-current-row
|
|
|
|
|
@current-change="handleRoomRowClick"
|
|
|
|
|
>
|
|
|
|
|
<el-table-column type="selection" width="45" />
|
|
|
|
|
<el-table-column prop="roomNo" label="教室编号" width="140" />
|
|
|
|
|
<el-table-column prop="roomName" label="教室名称" min-width="220" />
|
|
|
|
|
<el-table-column prop="capacity" label="容纳人数" width="100" align="center" />
|
|
|
|
|
<el-table-column prop="cameraCount" label="摄像头数量" width="110" align="center" />
|
|
|
|
|
<el-table-column prop="status" label="状态" width="90" align="center">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-tag :type="row.status === '启用' ? 'success' : 'info'" size="small">{{ row.status }}</el-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="操作" width="160" align="center" fixed="right">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-button link type="primary" size="small" :icon="Edit" @click.stop="showRoomDialog(row)">编辑</el-button>
|
|
|
|
|
<el-button link type="danger" size="small" :icon="Delete" @click.stop="deleteRoomRow(row)">删除</el-button>
|
|
|
|
|
<div class="hierarchy-layout">
|
|
|
|
|
<!-- 左侧:树形导航 -->
|
|
|
|
|
<div class="tree-panel">
|
|
|
|
|
<div class="tree-panel-header">
|
|
|
|
|
<div class="tree-panel-title">
|
|
|
|
|
<el-icon :size="16"><OfficeBuilding /></el-icon>
|
|
|
|
|
<span>教学楼 / 教室</span>
|
|
|
|
|
</div>
|
|
|
|
|
<el-button type="primary" :icon="Plus" size="small" @click="showBuildingDialog()">添加</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="tree-search">
|
|
|
|
|
<el-input v-model="treeFilter" placeholder="搜索教学楼或教室..." :prefix-icon="Search" clearable size="small" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="tree-scroll">
|
|
|
|
|
<el-tree
|
|
|
|
|
ref="treeRef"
|
|
|
|
|
:data="treeData"
|
|
|
|
|
:props="{ children: 'children', label: 'label' }"
|
|
|
|
|
node-key="id"
|
|
|
|
|
highlight-current
|
|
|
|
|
default-expand-all
|
|
|
|
|
:filter-node-method="filterTreeNode"
|
|
|
|
|
@node-click="handleTreeNodeClick"
|
|
|
|
|
:expand-on-click-node="false"
|
|
|
|
|
>
|
|
|
|
|
<template #default="{ data }">
|
|
|
|
|
<div class="tree-node-content">
|
|
|
|
|
<el-icon :size="15">
|
|
|
|
|
<OfficeBuilding v-if="data.type === 'building'" />
|
|
|
|
|
<HomeFilled v-else />
|
|
|
|
|
</el-icon>
|
|
|
|
|
<span class="tree-node-label">{{ data.label }}</span>
|
|
|
|
|
<span v-if="data.type === 'building'" class="tree-node-badge">{{ data.roomCount }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
<el-pagination
|
|
|
|
|
v-model:current-page="roomPage"
|
|
|
|
|
:total="roomTotal"
|
|
|
|
|
:page-size="roomPageSize"
|
|
|
|
|
size="small"
|
|
|
|
|
background
|
|
|
|
|
layout="total, prev, pager, next"
|
|
|
|
|
style="justify-content: flex-end; margin-top: 16px"
|
|
|
|
|
@current-change="loadRooms"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 摄像头区域 -->
|
|
|
|
|
<div class="section-card" v-if="currentRoom">
|
|
|
|
|
<div class="section-title">
|
|
|
|
|
<el-icon :size="18"><Camera /></el-icon>
|
|
|
|
|
<span>{{ currentBuilding.name }} / {{ currentRoom.roomName }} — 摄像头列表</span>
|
|
|
|
|
</el-tree>
|
|
|
|
|
<div v-if="treeData.length === 0" class="tree-empty">暂无数据,请先添加教学楼</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="filter-bar">
|
|
|
|
|
<el-input v-model="cameraSearch" placeholder="搜索摄像头名称..." :prefix-icon="Search" clearable size="default" style="width: 240px" />
|
|
|
|
|
<el-button type="primary" :icon="Plus" @click="showCameraDialog()">添加摄像头</el-button>
|
|
|
|
|
<el-button :icon="Delete" :disabled="selectedCameras.length === 0" @click="batchDeleteCamera">批量删除</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="data-table-card">
|
|
|
|
|
<el-table :data="cameras" stripe @selection-change="handleCameraSelect">
|
|
|
|
|
<el-table-column type="selection" width="45" />
|
|
|
|
|
<el-table-column prop="deviceNo" label="摄像头编号" min-width="180" />
|
|
|
|
|
<el-table-column prop="name" label="摄像头名称" min-width="180" />
|
|
|
|
|
<el-table-column prop="streamUrl" label="流地址" width="160" />
|
|
|
|
|
<el-table-column prop="streamType" label="流类型" width="100" align="center" />
|
|
|
|
|
<el-table-column prop="position" label="安装位置" min-width="200" />
|
|
|
|
|
<el-table-column prop="status" label="状态" width="90" align="center">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-tag :type="row.status === '在线' ? 'success' : 'danger'" size="small">{{ row.status }}</el-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="操作" width="160" align="center" fixed="right">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-button link type="primary" size="small" :icon="Edit" @click.stop="showCameraDialog(row)">编辑</el-button>
|
|
|
|
|
<el-button link type="danger" size="small" :icon="Delete" @click.stop="deleteCameraRow(row)">删除</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
<el-pagination
|
|
|
|
|
v-model:current-page="cameraPage"
|
|
|
|
|
:total="cameraTotal"
|
|
|
|
|
:page-size="cameraPageSize"
|
|
|
|
|
size="small"
|
|
|
|
|
background
|
|
|
|
|
layout="total, prev, pager, next"
|
|
|
|
|
style="justify-content: flex-end; margin-top: 16px"
|
|
|
|
|
/>
|
|
|
|
|
<!-- 右侧:详情面板 -->
|
|
|
|
|
<div class="detail-panel">
|
|
|
|
|
<!-- 无选中 -->
|
|
|
|
|
<div v-if="!currentBuilding && !currentRoom" class="detail-empty">
|
|
|
|
|
<el-icon :size="48" color="#d9d9d9"><OfficeBuilding /></el-icon>
|
|
|
|
|
<p>请从左侧选择一个教学楼或教室</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 教学楼层级:展示教室列表 -->
|
|
|
|
|
<template v-if="currentBuilding && !currentRoom">
|
|
|
|
|
<div class="detail-header">
|
|
|
|
|
<div class="breadcrumb-path">
|
|
|
|
|
<el-icon :size="16" color="#52c41a"><OfficeBuilding /></el-icon>
|
|
|
|
|
<span class="breadcrumb-current">{{ currentBuilding.name }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-actions">
|
|
|
|
|
<el-button size="small" :icon="Edit" @click="showBuildingDialog(currentBuilding)">编辑</el-button>
|
|
|
|
|
<el-button size="small" type="danger" :icon="Delete" @click="deleteBuildingRow(currentBuilding)">删除</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-summary">
|
|
|
|
|
<div class="summary-item">
|
|
|
|
|
<span class="summary-label">楼层数</span>
|
|
|
|
|
<span class="summary-value">{{ currentBuilding.floorCount }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="summary-item">
|
|
|
|
|
<span class="summary-label">教室总数</span>
|
|
|
|
|
<span class="summary-value">{{ currentBuilding.roomCount }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="summary-item">
|
|
|
|
|
<span class="summary-label">状态</span>
|
|
|
|
|
<el-tag :type="currentBuilding.status === '启用' ? 'success' : 'info'" size="small">{{ currentBuilding.status }}</el-tag>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="detail-section">
|
|
|
|
|
<div class="detail-section-header">
|
|
|
|
|
<h4>
|
|
|
|
|
<el-icon :size="15"><HomeFilled /></el-icon>
|
|
|
|
|
<span>教室列表</span>
|
|
|
|
|
</h4>
|
|
|
|
|
<div class="detail-section-actions">
|
|
|
|
|
<el-input v-model="roomSearch" placeholder="搜索教室编号..." :prefix-icon="Search" clearable size="small" style="width: 200px" />
|
|
|
|
|
<el-button size="small" type="primary" :icon="Plus" @click="showRoomDialog()">添加教室</el-button>
|
|
|
|
|
<el-button size="small" :icon="Delete" :disabled="selectedRooms.length === 0" @click="batchDeleteRoom">批量删除</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<el-table :data="filteredRooms" stripe @selection-change="handleRoomSelect" row-key="id" highlight-current-row @current-change="handleRoomRowClick">
|
|
|
|
|
<el-table-column type="selection" width="40" />
|
|
|
|
|
<el-table-column prop="roomNo" label="教室编号" width="120" />
|
|
|
|
|
<el-table-column prop="roomName" label="教室名称" min-width="160" />
|
|
|
|
|
<el-table-column prop="capacity" label="容纳人数" width="90" align="center" />
|
|
|
|
|
<el-table-column prop="cameraCount" label="摄像头数" width="90" align="center" />
|
|
|
|
|
<el-table-column prop="status" label="状态" width="80" align="center">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-tag :type="row.status === '启用' ? 'success' : 'info'" size="small">{{ row.status }}</el-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="操作" width="130" align="center" fixed="right">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-button link type="primary" size="small" :icon="Edit" @click.stop="showRoomDialog(row)">编辑</el-button>
|
|
|
|
|
<el-button link type="danger" size="small" :icon="Delete" @click.stop="deleteRoomRow(row)">删除</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-pagination">
|
|
|
|
|
<el-pagination
|
|
|
|
|
v-model:current-page="roomPage"
|
|
|
|
|
:total="roomTotal"
|
|
|
|
|
:page-size="roomPageSize"
|
|
|
|
|
size="small"
|
|
|
|
|
background
|
|
|
|
|
layout="total, prev, pager, next"
|
|
|
|
|
@current-change="loadRooms"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!-- 教室层级:展示摄像头列表 -->
|
|
|
|
|
<template v-if="currentRoom">
|
|
|
|
|
<div class="detail-header">
|
|
|
|
|
<div class="breadcrumb-path">
|
|
|
|
|
<el-link type="primary" :underline="false" @click="backToBuilding">
|
|
|
|
|
<el-icon :size="14"><OfficeBuilding /></el-icon>
|
|
|
|
|
{{ currentBuilding?.name }}
|
|
|
|
|
</el-link>
|
|
|
|
|
<el-icon :size="12" color="#999"><ArrowRight /></el-icon>
|
|
|
|
|
<!-- <el-icon :size="16" color="#409eff"><HomeFilled /></el-icon> -->
|
|
|
|
|
<span class="breadcrumb-current">{{ currentRoom.roomName }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-actions">
|
|
|
|
|
<el-button size="small" :icon="Edit" @click="showRoomDialog(currentRoom)">编辑</el-button>
|
|
|
|
|
<el-button size="small" type="danger" :icon="Delete" @click="deleteRoomRow(currentRoom)">删除</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-summary">
|
|
|
|
|
<div class="summary-item">
|
|
|
|
|
<span class="summary-label">教室编号</span>
|
|
|
|
|
<span class="summary-value">{{ currentRoom.roomNo }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="summary-item">
|
|
|
|
|
<span class="summary-label">容纳人数</span>
|
|
|
|
|
<span class="summary-value">{{ currentRoom.capacity }} 人</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="summary-item">
|
|
|
|
|
<span class="summary-label">摄像头数量</span>
|
|
|
|
|
<span class="summary-value">{{ currentRoom.cameraCount }} 个</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="summary-item">
|
|
|
|
|
<span class="summary-label">状态</span>
|
|
|
|
|
<el-tag :type="currentRoom.status === '启用' ? 'success' : 'info'" size="small">{{ currentRoom.status }}</el-tag>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="detail-section">
|
|
|
|
|
<div class="detail-section-header">
|
|
|
|
|
<h4>
|
|
|
|
|
<el-icon :size="15"><Camera /></el-icon>
|
|
|
|
|
<span>摄像头列表</span>
|
|
|
|
|
</h4>
|
|
|
|
|
<div class="detail-section-actions">
|
|
|
|
|
<el-input v-model="cameraSearch" placeholder="搜索摄像头名称..." :prefix-icon="Search" clearable size="small" style="width: 200px" />
|
|
|
|
|
<el-button size="small" type="primary" :icon="Plus" @click="showCameraDialog()">添加摄像头</el-button>
|
|
|
|
|
<el-button size="small" :icon="Delete" :disabled="selectedCameras.length === 0" @click="batchDeleteCamera">批量删除</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<el-table :data="cameras" stripe @selection-change="handleCameraSelect">
|
|
|
|
|
<el-table-column type="selection" width="40" />
|
|
|
|
|
<el-table-column prop="deviceNo" label="摄像头编号" min-width="160" />
|
|
|
|
|
<el-table-column prop="name" label="摄像头名称" min-width="160" />
|
|
|
|
|
<el-table-column prop="streamUrl" label="流地址" width="150" />
|
|
|
|
|
<el-table-column prop="streamType" label="流类型" width="80" align="center" />
|
|
|
|
|
<el-table-column prop="position" label="安装位置" min-width="180" />
|
|
|
|
|
<el-table-column prop="status" label="状态" width="80" align="center">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-tag :type="row.status === '在线' ? 'success' : 'danger'" size="small">{{ row.status }}</el-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="操作" width="130" align="center" fixed="right">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-button link type="primary" size="small" :icon="Edit" @click.stop="showCameraDialog(row)">编辑</el-button>
|
|
|
|
|
<el-button link type="danger" size="small" :icon="Delete" @click.stop="deleteCameraRow(row)">删除</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-pagination">
|
|
|
|
|
<el-pagination
|
|
|
|
|
v-model:current-page="cameraPage"
|
|
|
|
|
:total="cameraTotal"
|
|
|
|
|
:page-size="cameraPageSize"
|
|
|
|
|
size="small"
|
|
|
|
|
background
|
|
|
|
|
layout="total, prev, pager, next"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@ -238,18 +292,124 @@
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, computed, onMounted, watch } from 'vue'
|
|
|
|
|
import { ref, computed, nextTick, onMounted, watch } from 'vue'
|
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
|
import { Search, Plus, Delete, Edit } from '@element-plus/icons-vue'
|
|
|
|
|
import { getBuildings, addBuilding, updateBuilding, deleteBuilding, getRooms, addRoom, updateRoom, deleteRoom, getCameras, addCamera, updateCamera, deleteCamera } from '@/api/info'
|
|
|
|
|
import { Search, Plus, Delete, Edit, ArrowRight } from '@element-plus/icons-vue'
|
|
|
|
|
import { getBuildingList, getBuildings, addBuilding, updateBuilding, deleteBuilding, getRoomsList, getRooms, addRoom, updateRoom, deleteRoom, getCameras, addCamera, updateCamera, deleteCamera } from '@/api/info'
|
|
|
|
|
|
|
|
|
|
// ========== 树形导航 ==========
|
|
|
|
|
const treeRef = ref(null)
|
|
|
|
|
const treeFilter = ref('')
|
|
|
|
|
const treeData = ref([])
|
|
|
|
|
|
|
|
|
|
const filterTreeNode = (value, data) => {
|
|
|
|
|
if (!value) return true
|
|
|
|
|
return data.label.toLowerCase().includes(value.toLowerCase())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
watch(treeFilter, (val) => {
|
|
|
|
|
treeRef.value?.filter(val)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const buildTreeData = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const [bRes, rRes] = await Promise.all([
|
|
|
|
|
getBuildingList(),
|
|
|
|
|
getRoomsList()
|
|
|
|
|
])
|
|
|
|
|
const buildings = bRes?.data || []
|
|
|
|
|
const rooms = rRes?.data || []
|
|
|
|
|
// 组装树数据:教学楼为父节点,下属教室为子节点
|
|
|
|
|
treeData.value = buildings.map(b => ({
|
|
|
|
|
id: `building-${b.id}`,
|
|
|
|
|
type: 'building',
|
|
|
|
|
label: b.buildingName,
|
|
|
|
|
roomCount: rooms.filter(r => r.buildingId === b.id).length,
|
|
|
|
|
floors: b.floors,
|
|
|
|
|
status: b.status === 1 ? '启用' : '停用',
|
|
|
|
|
buildingId: b.id,
|
|
|
|
|
children: rooms
|
|
|
|
|
.filter(r => r.buildingId === b.id)
|
|
|
|
|
.map(r => ({
|
|
|
|
|
id: `room-${r.id}`,
|
|
|
|
|
type: 'room',
|
|
|
|
|
label: r.roomName,
|
|
|
|
|
roomNo: r.roomNo,
|
|
|
|
|
capacity: r.capacity,
|
|
|
|
|
cameraCount: r.deviceCount || 0,
|
|
|
|
|
status: r.status === 1 ? '启用' : '停用',
|
|
|
|
|
buildingId: r.buildingId,
|
|
|
|
|
roomId: r.id
|
|
|
|
|
}))
|
|
|
|
|
}))
|
|
|
|
|
} catch {
|
|
|
|
|
// 错误已在拦截器中统一处理
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleTreeNodeClick = (data) => {
|
|
|
|
|
if (data.type === 'building') {
|
|
|
|
|
// 从 paginated 列表中找到对应教学楼(带有完整字段)
|
|
|
|
|
const matched = buildings.value.find(b => b.id === data.buildingId)
|
|
|
|
|
if (matched) {
|
|
|
|
|
currentBuilding.value = matched
|
|
|
|
|
} else {
|
|
|
|
|
// 未加载到列表时,从树节点信息构建
|
|
|
|
|
currentBuilding.value = {
|
|
|
|
|
id: data.buildingId,
|
|
|
|
|
name: data.label,
|
|
|
|
|
floorCount: data.floors,
|
|
|
|
|
roomCount: data.roomCount,
|
|
|
|
|
status: data.status
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
currentRoom.value = null
|
|
|
|
|
roomPage.value = 1
|
|
|
|
|
selectedRooms.value = []
|
|
|
|
|
loadRooms()
|
|
|
|
|
} else if (data.type === 'room') {
|
|
|
|
|
// 找到父教学楼
|
|
|
|
|
const parentNode = treeData.value.find(b => b.children?.some(r => r.id === data.id))
|
|
|
|
|
if (parentNode) {
|
|
|
|
|
const matched = buildings.value.find(b => b.id === parentNode.buildingId)
|
|
|
|
|
currentBuilding.value = matched || {
|
|
|
|
|
id: parentNode.buildingId,
|
|
|
|
|
name: parentNode.label,
|
|
|
|
|
floorCount: parentNode.floors,
|
|
|
|
|
roomCount: parentNode.roomCount,
|
|
|
|
|
status: parentNode.status
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
currentRoom.value = {
|
|
|
|
|
id: data.roomId,
|
|
|
|
|
roomNo: data.roomNo,
|
|
|
|
|
roomName: data.label,
|
|
|
|
|
capacity: data.capacity,
|
|
|
|
|
cameraCount: data.cameraCount,
|
|
|
|
|
status: data.status,
|
|
|
|
|
buildingId: data.buildingId
|
|
|
|
|
}
|
|
|
|
|
// 同步加载教室列表(当前页可能不包含此教室,加载对应分页)
|
|
|
|
|
if (currentBuilding.value) {
|
|
|
|
|
roomPage.value = 1
|
|
|
|
|
loadRooms()
|
|
|
|
|
}
|
|
|
|
|
cameraPage.value = 1
|
|
|
|
|
selectedCameras.value = []
|
|
|
|
|
loadCameras()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const backToBuilding = () => {
|
|
|
|
|
currentRoom.value = null
|
|
|
|
|
selectedCameras.value = []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== 教学楼 ==========
|
|
|
|
|
const buildingPage = ref(1)
|
|
|
|
|
const buildingPageSize = ref(8)
|
|
|
|
|
const buildingSearch = ref('')
|
|
|
|
|
const buildingDialogVisible = ref(false)
|
|
|
|
|
const buildingEditing = ref(false)
|
|
|
|
|
const selectedBuildings = ref([])
|
|
|
|
|
const currentBuilding = ref(null)
|
|
|
|
|
|
|
|
|
|
const buildings = ref([])
|
|
|
|
|
@ -259,15 +419,14 @@ const loadBuildings = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await getBuildings({
|
|
|
|
|
current: buildingPage.value,
|
|
|
|
|
size: buildingPageSize.value,
|
|
|
|
|
keyword: buildingSearch.value || undefined
|
|
|
|
|
size: buildingPageSize.value
|
|
|
|
|
})
|
|
|
|
|
const pageData = res.data || {}
|
|
|
|
|
buildings.value = (pageData.records || []).map(b => ({
|
|
|
|
|
id: b.id,
|
|
|
|
|
name: b.buildingName,
|
|
|
|
|
floorCount: b.floors,
|
|
|
|
|
roomCount: b.classroomCount || 0,
|
|
|
|
|
roomCount: rooms.filter(r => r.buildingId === b.id).length,
|
|
|
|
|
status: b.status === 1 ? '启用' : '停用'
|
|
|
|
|
}))
|
|
|
|
|
buildingTotal.value = pageData.total || 0
|
|
|
|
|
@ -276,24 +435,19 @@ const loadBuildings = async () => {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
watch(buildingPage, () => loadBuildings())
|
|
|
|
|
watch(buildingSearch, () => {
|
|
|
|
|
if (buildingPage.value !== 1) {
|
|
|
|
|
buildingPage.value = 1
|
|
|
|
|
} else {
|
|
|
|
|
loadBuildings()
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
await Promise.all([loadBuildings(), buildTreeData()])
|
|
|
|
|
// 默认选中第一个教学楼
|
|
|
|
|
if (treeData.value.length > 0) {
|
|
|
|
|
handleTreeNodeClick(treeData.value[0])
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
treeRef.value?.setCurrentKey(treeData.value[0].id)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
loadBuildings()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const buildingForm = ref({ name: '', floorCount: 1, status: '启用' })
|
|
|
|
|
|
|
|
|
|
const handleBuildingSelect = (rows) => { selectedBuildings.value = rows }
|
|
|
|
|
const handleBuildingRowClick = (row) => { currentBuilding.value = row; roomPage.value = 1; currentRoom.value = null; loadRooms() }
|
|
|
|
|
|
|
|
|
|
const showBuildingDialog = (row) => {
|
|
|
|
|
if (row) {
|
|
|
|
|
buildingEditing.value = true
|
|
|
|
|
@ -321,7 +475,7 @@ const saveBuilding = async () => {
|
|
|
|
|
ElMessage.success('添加成功')
|
|
|
|
|
}
|
|
|
|
|
buildingDialogVisible.value = false
|
|
|
|
|
await loadBuildings()
|
|
|
|
|
await Promise.all([loadBuildings(), buildTreeData()])
|
|
|
|
|
} catch {
|
|
|
|
|
// 错误已在拦截器中统一处理
|
|
|
|
|
}
|
|
|
|
|
@ -334,23 +488,12 @@ const deleteBuildingRow = (row) => {
|
|
|
|
|
await deleteBuilding([row.id])
|
|
|
|
|
if (currentBuilding.value?.id === row.id) currentBuilding.value = null
|
|
|
|
|
ElMessage.success('删除成功')
|
|
|
|
|
await loadBuildings()
|
|
|
|
|
} catch {
|
|
|
|
|
// 错误已在拦截器中统一处理
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const batchDeleteBuilding = () => {
|
|
|
|
|
ElMessageBox.confirm(`确认删除选中的 ${selectedBuildings.value.length} 个教学楼?`, '批量删除', { type: 'warning' })
|
|
|
|
|
.then(async () => {
|
|
|
|
|
try {
|
|
|
|
|
const ids = selectedBuildings.value.map(b => b.id)
|
|
|
|
|
await deleteBuilding(ids)
|
|
|
|
|
currentBuilding.value = null
|
|
|
|
|
ElMessage.success('批量删除成功')
|
|
|
|
|
await loadBuildings()
|
|
|
|
|
await Promise.all([loadBuildings(), buildTreeData()])
|
|
|
|
|
// 删除后自动选中第一个教学楼
|
|
|
|
|
if (treeData.value.length > 0) {
|
|
|
|
|
handleTreeNodeClick(treeData.value[0])
|
|
|
|
|
treeRef.value?.setCurrentKey(treeData.value[0].id)
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// 错误已在拦截器中统一处理
|
|
|
|
|
}
|
|
|
|
|
@ -402,7 +545,19 @@ const filteredRooms = computed(() => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const handleRoomSelect = (rows) => { selectedRooms.value = rows }
|
|
|
|
|
const handleRoomRowClick = (row) => { currentRoom.value = row; cameraPage.value = 1; loadCameras() }
|
|
|
|
|
const handleRoomRowClick = (row) => {
|
|
|
|
|
currentRoom.value = row
|
|
|
|
|
cameraPage.value = 1
|
|
|
|
|
selectedCameras.value = []
|
|
|
|
|
loadCameras()
|
|
|
|
|
// 同步树形选中
|
|
|
|
|
const treeNode = treeData.value
|
|
|
|
|
.flatMap(b => b.children || [])
|
|
|
|
|
.find(r => r.roomId === row.id)
|
|
|
|
|
if (treeNode) {
|
|
|
|
|
treeRef.value?.setCurrentKey(treeNode.id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const showRoomDialog = (row) => {
|
|
|
|
|
if (row) {
|
|
|
|
|
@ -434,7 +589,7 @@ const saveRoom = async () => {
|
|
|
|
|
}
|
|
|
|
|
roomDialogVisible.value = false
|
|
|
|
|
await loadRooms()
|
|
|
|
|
await loadBuildings()
|
|
|
|
|
await Promise.all([loadBuildings(), buildTreeData()])
|
|
|
|
|
} catch {
|
|
|
|
|
// 错误已在拦截器中统一处理
|
|
|
|
|
}
|
|
|
|
|
@ -448,7 +603,7 @@ const deleteRoomRow = (row) => {
|
|
|
|
|
if (currentRoom.value?.id === row.id) currentRoom.value = null
|
|
|
|
|
ElMessage.success('删除成功')
|
|
|
|
|
await loadRooms()
|
|
|
|
|
await loadBuildings()
|
|
|
|
|
await Promise.all([loadBuildings(), buildTreeData()])
|
|
|
|
|
} catch {
|
|
|
|
|
// 错误已在拦截器中统一处理
|
|
|
|
|
}
|
|
|
|
|
@ -465,7 +620,7 @@ const batchDeleteRoom = () => {
|
|
|
|
|
currentRoom.value = null
|
|
|
|
|
ElMessage.success('批量删除成功')
|
|
|
|
|
await loadRooms()
|
|
|
|
|
await loadBuildings()
|
|
|
|
|
await Promise.all([loadBuildings(), buildTreeData()])
|
|
|
|
|
} catch {
|
|
|
|
|
// 错误已在拦截器中统一处理
|
|
|
|
|
}
|
|
|
|
|
@ -504,6 +659,10 @@ const loadCameras = async () => {
|
|
|
|
|
status: c.onlineStatus === 1 ? '在线' : '离线'
|
|
|
|
|
}))
|
|
|
|
|
cameraTotal.value = pageData.total || 0
|
|
|
|
|
// 同步更新当前教室的摄像头数量
|
|
|
|
|
if (currentRoom.value) {
|
|
|
|
|
currentRoom.value.cameraCount = cameraTotal.value
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// 错误已在拦截器中统一处理
|
|
|
|
|
}
|
|
|
|
|
@ -556,6 +715,7 @@ const saveCamera = async () => {
|
|
|
|
|
cameraDialogVisible.value = false
|
|
|
|
|
await loadCameras()
|
|
|
|
|
await loadRooms()
|
|
|
|
|
await buildTreeData()
|
|
|
|
|
} catch {
|
|
|
|
|
// 错误已在拦截器中统一处理
|
|
|
|
|
}
|
|
|
|
|
@ -569,6 +729,7 @@ const deleteCameraRow = (row) => {
|
|
|
|
|
ElMessage.success('删除成功')
|
|
|
|
|
await loadCameras()
|
|
|
|
|
await loadRooms()
|
|
|
|
|
await buildTreeData()
|
|
|
|
|
} catch {
|
|
|
|
|
// 错误已在拦截器中统一处理
|
|
|
|
|
}
|
|
|
|
|
@ -585,6 +746,7 @@ const batchDeleteCamera = () => {
|
|
|
|
|
ElMessage.success('批量删除成功')
|
|
|
|
|
await loadCameras()
|
|
|
|
|
await loadRooms()
|
|
|
|
|
await buildTreeData()
|
|
|
|
|
} catch {
|
|
|
|
|
// 错误已在拦截器中统一处理
|
|
|
|
|
}
|
|
|
|
|
@ -614,7 +776,247 @@ const batchDeleteCamera = () => {
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.data-table-card {
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
/* ===== 树形 + 详情分栏布局 ===== */
|
|
|
|
|
.hierarchy-layout {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 20px;
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 左侧树形面板 */
|
|
|
|
|
.tree-panel {
|
|
|
|
|
width: 280px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tree-panel-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
padding: 16px 16px 12px;
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tree-panel-title {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #262626;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tree-search {
|
|
|
|
|
padding: 10px 16px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tree-scroll {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: 4px 0 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tree-empty {
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 32px 16px;
|
|
|
|
|
color: #999;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tree-node-content {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
padding: 2px 0;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tree-node-label {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tree-node-badge {
|
|
|
|
|
background: #a7e280;
|
|
|
|
|
color: #fff;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
padding: 0 6px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
line-height: 18px;
|
|
|
|
|
min-width: 20px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 覆盖父容器,让分栏撑满可视区域 */
|
|
|
|
|
.page-container {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
height: 100%;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.page-header {
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 右侧详情面板 */
|
|
|
|
|
.detail-panel {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-empty {
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
padding: 80px 0;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
|
|
|
|
p {
|
|
|
|
|
color: #999;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 8px 8px 0 0;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.breadcrumb-path {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
|
|
|
|
.breadcrumb-current {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #262626;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-summary {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 0;
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
padding: 12px 20px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.summary-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
padding: 0 24px;
|
|
|
|
|
border-right: 1px solid #eee;
|
|
|
|
|
|
|
|
|
|
&:first-child { padding-left: 0; }
|
|
|
|
|
&:last-child { border-right: none; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.summary-label {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: #999;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.summary-value {
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #262626;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-section {
|
|
|
|
|
background: #fff;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
|
|
|
padding: 20px;
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-pagination {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 12px 20px;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 0 0 8px 8px;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-section-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
|
|
|
|
h4 {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #262626;
|
|
|
|
|
margin: 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-section-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 树形选择高亮 */
|
|
|
|
|
.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
|
|
|
|
|
background: #e6f7ff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 响应式 */
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
|
|
.hierarchy-layout {
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
.tree-panel {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|