添加代码详细注释,完善项目文档

This commit is contained in:
g82tt
2025-10-12 18:46:44 +08:00
parent b4f1734024
commit a8212be62e
14 changed files with 839 additions and 235 deletions

View File

@@ -1,5 +1,28 @@
# 仓库管理操作端项目更新记录
## 2024年9月17日 - 代码注释完善
**主要更新内容:**
- 为项目核心文件添加了详细的注释文档,包括:
- 视图组件LayoutView.vue、HomeView.vue、Map.vue、LoginView.vue
- 功能组件Fence.vue、Filter.vue
- 自定义钩子useFence.js
- 配置文件main.js、router/index.js、stores/user.js、config/index.js
- 优化了注释格式和内容组织,提高代码可读性和可维护性
- 为每个文件添加了文件级注释、类级注释、函数级注释、参数和返回值注释
- 补充了关键业务逻辑的说明和实现思路
**修复问题:**
- 修复了部分文件中的注释格式问题
**已知问题:**
- 暂无重大已知问题
**后续计划:**
- 继续完善剩余文件的代码注释
- 实现更多业务功能模块
- 优化3D地图显示效果和交互体验
## 2024年9月16日 - 项目初始化
**主要更新内容:**

View File

@@ -1,11 +1,27 @@
<template>
<!-- 应用根组件的路由视图容器 -->
<!-- 作为所有路由组件的挂载点根据当前激活的路由动态渲染对应的组件内容 -->
<router-view />
</template>
<script setup>
// App组件作为路由容器
/**
* 应用的根组件(App.vue)
* 作为整个应用的入口组件,主要职责是提供路由视图容器
* Vue Router会根据当前的URL路径在此处动态渲染对应的页面组件
*
* 组件特点:
* - 结构简洁仅包含router-view作为路由出口
* - 通过Vue Router的配置实现页面间的切换
* - 所有子组件通过路由系统挂载到此处
*/
</script>
<style scoped>
/**
* 根组件样式
* 当前保持为空全局样式定义在assets/main.css中
* 如需为根组件添加特定样式,可以在此处定义
*/
</style>

View File

@@ -1,50 +1,104 @@
<script setup>
/**
* 围栏组件
* 在3D地图上生成特定区域的围栏用于标识仓库中的重点监控区域
* 基于THREE.js实现3D围栏的渲染和管理
*/
import { inject, onBeforeUnmount } from "vue"
import { useFence } from "../hooks/useFence"
import { useFence } from "../hooks/useFence" // 导入围栏生成自定义钩子
/**
* 从VgoMap获取THREE.js实例
* THREE.js是一个JavaScript 3D库用于3D模型的渲染、动画和交互
* 此处用于创建围栏的3D几何体并添加到地图场景中
*/
const { THREE } = VgoMap
// 可以使用pinia等管理全局数据这里只是方便演示, 直接注入了上层提供的数据
/**
* 注入上级组件Map.vue提供的数据
* 使用Vue的provide/inject机制获取父组件共享的数据
*/
// 从父组件注入地图实例,用于将围栏添加到地图场景
const map = inject("map")
// 从父组件注入所有多边形数据,包含仓库中所有建筑的点位信息
const polygonDataAll = inject("polygonDataAll")
// 为了演示,随意取了几个模型,获取模型形状包含的所有点位
/**
* 筛选出需要包含在围栏内的建筑点位
* 从所有多边形数据中筛选出指定名称的建筑
*/
const inFencePoints = polygonDataAll.value.filter(p => {
// 筛选条件:建筑名称包含在指定的重点区域列表中
// 这里选择了'保卫部', '智慧大楼', '能源部办公楼'作为重点监控区域
return ['保卫部', '智慧大楼', '能源部办公楼'].includes(p.name)
}).map(p => p.points).flat()
}).map(p => p.points).flat() // 提取所有筛选出建筑的点位数据并扁平化数组
// 获取生成围栏函数高60并封顶
/**
* 使用自定义hook创建围栏生成函数
* 从useFence钩子中解构出generateFence函数用于生成围栏几何体
* @param {Object} options - 围栏配置参数
* @param {number} options.height - 围栏高度此处设置为60单位
* @param {boolean} options.isCloseTop - 是否封闭顶部true表示生成封闭的围栏
*/
const { generateFence } = useFence({
height: 60,
isCloseTop: true,
height: 60, // 围栏高度,单位与地图坐标系一致
isCloseTop: true, // 是否封闭顶部设置为true可生成顶部封闭的围栏
})
// 建立一个围栏组,便于销毁所有围栏
const fenceGroup = new THREE.Group()
map.value.scene.add(fenceGroup)
/**
* 创建围栏组,用于统一管理和销毁围栏对象
* 使用THREE.Group可以方便地组织和管理多个3D对象
*/
const fenceGroup = new THREE.Group() // 创建THREE.js组对象用于集中管理所有围栏相关的3D对象
map.value.scene.add(fenceGroup) // 将围栏组添加到地图场景中,使其可见
// 生成围栏,并添加到围栏组
/**
* 生成围栏并添加到围栏组
* 使用getMinBoxPoints计算出的最小包围矩形作为围栏的边界
*/
// 调用generateFence函数生成围栏几何体传入通过getMinBoxPoints计算的最小矩形顶点
const fence = generateFence(getMinBoxPoints(inFencePoints))
// 将生成的围栏几何体添加到围栏组中
fenceGroup.add(fence)
// 清理围栏
/**
* 组件生命周期钩子:卸载前执行
* 清理围栏资源,避免内存泄漏
*/
onBeforeUnmount(() => {
fenceGroup.dispose()
// 安全检查确保fenceGroup存在
if (fenceGroup) {
fenceGroup.clear() // 清除围栏组中的所有子对象
// 安全检查确保map和map.scene存在
if (map.value && map.value.scene) {
map.value.scene.remove(fenceGroup) // 从地图场景中移除围栏组
}
}
})
// 根据所有包含的点位, 获取最小的矩形顶点数组
/**
* 根据所有包含的点位,计算并获取最小包围矩形的顶点数组
* 算法原理使用THREE.Box2计算点集的最小包围盒然后提取四个顶点
* @param {Array<THREE.Vector3>} points - 输入的点位数组,包含需要包围的所有点
* @returns {Array<THREE.Vector3>} - 最小矩形的四个顶点坐标(按顺时针顺序)
*/
function getMinBoxPoints(points) {
const box = new THREE.Box2()
box.setFromPoints(points)
const box = new THREE.Box2() // 创建二维包围盒对象
box.setFromPoints(points) // 根据输入点集计算最小包围盒
// 返回最小矩形的四个顶点(按顺时针顺序)
// 注意这里z坐标统一设置为0表示在地图的平面上
return [
new THREE.Vector3(box.min.x, box.min.y, 0), // 左下角 (0)
new THREE.Vector3(box.max.x, box.min.y, 0), // 右下角 (1)
new THREE.Vector3(box.max.x, box.max.y, 0), // 右上角 (2)
new THREE.Vector3(box.min.x, box.max.y, 0) // 左上角 (3)
new THREE.Vector3(box.min.x, box.min.y, 0), // 左下角顶点
new THREE.Vector3(box.max.x, box.min.y, 0), // 右下角顶点
new THREE.Vector3(box.max.x, box.max.y, 0), // 右上角顶点
new THREE.Vector3(box.min.x, box.max.y, 0) // 左上角顶点
];
}
</script>
<template>
<!-- 围栏组件容器 -->
<!-- 注意此组件主要通过JavaScript在3D场景中渲染围栏DOM元素仅作为组件的挂载点 -->
<div class="fence"></div>
</template>

View File

@@ -1,96 +1,202 @@
<script setup>
/**
* 筛选组件
* 在3D地图上创建可筛选的设备标记点并提供分类筛选功能
* 用于对地图上的不同类型设备进行显示/隐藏控制
* 基于THREE.js实现3D标记点的渲染和管理
*/
import { inject, computed, ref, watch, onBeforeUnmount } from "vue"
/**
* 从VgoMap获取THREE.js实例
* THREE.js是一个JavaScript 3D库用于3D模型的渲染、动画和交互
* 此处用于创建和管理3D标记点对象
*/
const { THREE } = VgoMap
// 可以使用pinia等管理全局数据这里只是方便演示, 直接注入了上层提供的数据
/**
* 注入上级组件Map.vue提供的数据
* 使用Vue的provide/inject机制获取父组件共享的数据
*/
// 从父组件注入地图实例,用于将标记点添加到地图场景
const map = inject("map")
// 从父组件注入所有多边形数据,包含仓库中所有设备的点位信息
const polygonDataAll = inject("polygonDataAll")
// 筛选出有分类的点位
/**
* 计算属性:筛选出有分类的点位
* 将点位按shortcutsId进行分组用于生成筛选选项
* @returns {Object} - 按shortcutsId分组的点位对象键为分类ID值为该分类下的点位数组
*/
const shortcutList = computed(() => {
// 筛选出有shortcutsId属性的点位并按shortcutsId进行分组
return polygonDataAll.value.filter(i => i?.shortcutsId).reduce((result, item) => {
// 如果该分类ID对应的数组不存在则创建一个新数组
if(!result?.[item.shortcutsId]) {
result[item.shortcutsId] = []
}
// 将当前点位添加到对应分类的数组中
result[item.shortcutsId].push(item)
return result
}, {})
})
const markerGroup = new THREE.Group()
map.value.scene.add(markerGroup)
/**
* 创建标记组,用于统一管理和销毁所有标记点
* 使用THREE.Group可以方便地组织和管理多个3D对象
*/
const markerGroup = new THREE.Group() // 创建THREE.js组对象用于集中管理所有标记点
map.value.scene.add(markerGroup) // 将标记组添加到地图场景中,使其可见
/**
* 组件生命周期钩子:卸载前执行
* 清理标记点资源,避免内存泄漏
*/
onBeforeUnmount(() => {
markerGroup.dispose()
// 安全检查确保markerGroup存在
if (markerGroup) {
markerGroup.clear() // 清除标记组中的所有子对象
// 安全检查确保map和map.scene存在
if (map.value && map.value.scene) {
map.value.scene.remove(markerGroup) // 从地图场景中移除标记组
}
}
})
// 根据有分类点位创建marker
/**
* 标记点状态引用
* 存储各个分类下的标记点对象,用于后续根据用户选择控制显示/隐藏
* 结构为:{分类ID: [标记点对象1, 标记点对象2, ...]}
*/
const markerStatus = ref({})
/**
* 监听shortcutList变化创建或更新标记点
* 当shortcutList变化时清理旧标记点并创建新标记点
* @param {Object} value - 变化后的shortcutList值
*/
watch(shortcutList, (value) => {
// 清理所有marker
// 清理所有旧的标记点
while(markerGroup.children.length) {
markerGroup.children?.[0]?.dispose()
const child = markerGroup.children[0]
if (child && child.dispose) {
child.dispose()
}
markerGroup.remove(child)
}
// 重置标记点状态对象
markerStatus.value = {}
// 创建新的marker
const listGroup = Object.entries(value)
// 创建新的标记点
const listGroup = Object.entries(value) // 转换为[key, value]格式的数组,便于遍历
// 遍历每个分类及其对应的点位列表
listGroup.forEach(([key, list]) => {
// 遍历该分类下的每个点位
list.forEach((item) => {
// 创建标记点,显示"设备类型+分类ID"
const marker = createDivMarker('设备类型' + item.shortcutsId)
// 设置标记点位置在点位中心Z轴方向上移10单位使其悬浮在点位上方
marker.position.copy(item.center).setZ(10)
// 将标记点添加到标记组
markerGroup.add(marker)
// 将标记点添加到对应分类的状态数组中,便于后续控制显示/隐藏
if(!markerStatus.value?.[key]) {
markerStatus.value[key] = []
}
markerStatus.value[key].push(marker)
// 初始化时默认选中所有分类
checkedStatus.value[key] = true
})
})
}, {
immediate: true,
immediate: true, // 立即执行一次以初始化标记点
})
// 根据选中状态控制marker显示
/**
* 选中状态引用
* 存储各个设备类型分类的选中状态,用于控制对应分类标记点的显示/隐藏
* 结构为:{分类ID: boolean}
*/
const checkedStatus = ref({})
/**
* 监听checkedStatus变化控制标记点的显示/隐藏
* 当用户通过复选框改变选中状态时,更新对应分类标记点的可见性
* @param {Object} checked - 变化后的checkedStatus值
*/
watch(checkedStatus, (checked) => {
// 遍历所有分类的选中状态
for(let key in checked) {
markerStatus.value[key].forEach((marker) => {
marker.visible = checked[key]
})
// 安全检查:确保该分类下有标记点
if (markerStatus.value[key] && Array.isArray(markerStatus.value[key])) {
// 遍历该分类下的所有标记点
markerStatus.value[key].forEach((marker) => {
// 根据选中状态设置标记点的可见性
marker.visible = checked[key]
})
}
}
}, {
deep: true,
deep: true, // 深度监听,确保能捕获到对象属性的变化
})
/**
* 创建DOM标记点
* 在地图上创建一个自定义DOM元素作为标记点
* @param {string} content - 标记点显示的文本内容
* @returns {Object} - 创建的标记点对象,可用于进一步操作(如设置位置、可见性等)
*/
function createDivMarker(content) {
// 创建包装元素
const wrapper = document.createElement("div")
const contentNode = document.createElement("div")
contentNode.className = "marker"
contentNode.textContent = content
// 创建内容元素
const contentNode = document.createElement("div")
contentNode.className = "marker" // 设置样式类,用于自定义标记点外观
contentNode.textContent = content // 设置显示文本
// 将内容元素添加到包装元素中
wrapper.appendChild(contentNode)
// 获取当前楼层ID如果获取失败则默认为'1'
const floorId = map.status?.floor?.data?.id || '1'
return map.value.addDomMarker(floorId, wrapper) // 之前记错了这边直接传dom元素也是可以的
// 使用地图API添加DOM标记点到指定楼层
return map.value.addDomMarker(floorId, wrapper)
}
</script>
<template>
<!-- 筛选组件界面 -->
<!-- 用于显示设备类型筛选选项允许用户控制不同类型设备标记点的显示/隐藏 -->
<ul class="filter">
<!-- 遍历shortcutList的所有键为每个设备类型生成筛选选项 -->
<li v-for="key in Object.keys(shortcutList)" :key="key">
<!-- 筛选选项标签显示"设备类型+分类ID" -->
<label :for="key">设备类型{{ key }}</label>
<!-- 筛选复选框与checkedStatus中的对应状态双向绑定 -->
<input type="checkbox" :id="key" v-model="checkedStatus[key]">
</li>
</ul>
</template>
<style scoped>
/**
* 筛选组件样式
* 设置筛选面板的位置、层级和交互样式
*/
.filter {
position: absolute;
right: 50px;
bottom: 20px;
z-index: 10;
position: absolute; /* 绝对定位,固定在地图右下角 */
right: 50px; /* 距离右侧50px */
bottom: 20px; /* 距离底部20px */
z-index: 10; /* 设置较高层级,确保在地图上层显示 */
/* 为列表项、标签和输入框添加指针样式,表示可交互 */
li, label, input {
cursor: pointer;
}
@@ -99,10 +205,14 @@ function createDivMarker(content) {
</style>
<style>
/**
* 全局样式 - 标记点样式
* 定义地图上设备标记点的外观样式
*/
.marker {
background-color: #fff;
padding: 2px 10px;
border-radius: 20px;
transform: scale(0.7);
background-color: #fff; /* 白色背景,提高可见度 */
padding: 2px 10px; /* 内边距上下2px左右10px */
border-radius: 20px; /* 圆角边框,使标记点看起来更柔和 */
transform: scale(0.7); /* 缩小标记点显示尺寸,避免遮挡过多地图内容 */
}
</style>

View File

@@ -1,74 +1,143 @@
// 系统配置文件
// 这个文件包含所有全局配置项,可以在整个应用中访问
/**
* 全局配置文件
* 包含应用的所有全局配置项,可在整个应用中访问
* 支持通过外部配置文件window.APP_CONFIG覆盖默认配置
*/
// 优先使用外部配置文件中的配置,如果没有则使用默认值
/**
* 获取外部配置
* 优先使用外部配置文件中的配置,如果没有则使用默认值
* @type {Object} 外部配置对象
*/
const externalConfig = typeof window !== 'undefined' && window.APP_CONFIG || {};
/**
* 导出全局配置对象
* 包含应用基本信息、API配置、布局配置、主题配置等
* @returns {Object} 全局配置对象
*/
export default {
// 应用基本信息
/**
* 应用基本信息配置
* 包含应用名称、版本和描述等基本信息
*/
app: {
// 合并外部配置中的app配置
...externalConfig.app,
// 应用名称(优先使用外部配置,否则使用默认值)
name: externalConfig.app?.name || '仓库管理操作端',
// 应用版本号(优先使用外部配置,否则使用默认值)
version: externalConfig.app?.version || '1.0.0',
// 应用描述信息(优先使用外部配置,否则使用默认值)
description: externalConfig.app?.description || '现代化仓库管理系统前端操作界面'
},
// API配置
/**
* API配置
* 包含API请求相关的配置如基础URL、超时时间等
*/
api: {
// 合并外部配置中的api配置
...externalConfig.api,
// 优先使用外部配置,其次使用环境变量,最后使用默认值
// API基础URL优先使用外部配置,其次使用环境变量,最后使用默认值
baseUrl: externalConfig.api?.baseUrl || import.meta.env.VITE_API_BASE_URL || '/api',
// API请求超时时间毫秒优先使用外部配置否则使用默认值
timeout: externalConfig.api?.timeout || 30000, // 30秒超时
// 请求失败重试次数(优先使用外部配置,否则使用默认值)
retryCount: externalConfig.api?.retryCount || 3, // 请求失败重试次数
// 重试间隔时间(毫秒)(优先使用外部配置,否则使用默认值)
retryDelay: externalConfig.api?.retryDelay || 1000 // 重试间隔(毫秒)
},
// 布局配置
/**
* 布局配置
* 包含应用界面布局相关的配置,如侧边栏宽度、头部高度等
*/
layout: {
sidebarWidth: 240, // 侧边栏宽度
headerHeight: 60, // 头部高度
transitionDuration: 300, // 过渡动画时长
showBreadcrumb: true // 是否显示面包屑
// 侧边栏宽度(像素)
sidebarWidth: 240,
// 头部导航栏高度(像素)
headerHeight: 60,
// 过渡动画时长(毫秒)
transitionDuration: 300,
// 是否显示面包屑导航
showBreadcrumb: true
},
// 主题配置
/**
* 主题配置
* 包含应用界面主题相关的配置,如颜色、字体大小等
*/
theme: {
primaryColor: '#1890ff', // 主色调
successColor: '#52c41a', // 成功色
warningColor: '#faad14', // 警告色
errorColor: '#f5222d', // 错误色
fontSize: 14, // 基础字体大小
darkMode: false // 是否开启暗黑模式
// 主色调(蓝色)
primaryColor: '#1890ff',
// 成功状态颜色(绿色)
successColor: '#52c41a',
// 警告状态颜色(黄色)
warningColor: '#faad14',
// 错误状态颜色(红色)
errorColor: '#f5222d',
// 基础字体大小(像素)
fontSize: 14,
// 是否开启暗黑模式
darkMode: false
},
// 缓存配置
/**
* 缓存配置
* 包含应用数据缓存相关的配置
*/
cache: {
// 是否启用缓存
enable: true,
defaultExpireTime: 24 * 60 * 60 * 1000 // 默认过期时间24小时
// 默认缓存过期时间(毫秒)(24小时
defaultExpireTime: 24 * 60 * 60 * 1000
},
// 分页配置
/**
* 分页配置
* 包含数据分页显示相关的配置
*/
pagination: {
// 可选的每页记录数
pageSizes: [10, 20, 50, 100],
// 默认每页记录数
defaultPageSize: 20
},
// 上传配置
/**
* 上传配置
* 包含文件上传相关的配置,如最大文件大小、允许的文件类型等
*/
upload: {
maxSize: 10 * 1024 * 1024, // 最大文件大小10MB
// 最大文件大小(字节)(10MB
maxSize: 10 * 1024 * 1024,
// 允许上传的文件类型
allowedTypes: ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx', 'xls', 'xlsx']
},
// 日期时间格式
/**
* 日期时间格式配置
* 包含日期和时间的格式化配置
*/
date: {
// 日期格式
format: 'YYYY-MM-DD',
// 时间格式
timeFormat: 'HH:mm:ss',
// 日期时间格式
dateTimeFormat: 'YYYY-MM-DD HH:mm:ss'
},
// 权限配置
/**
* 权限配置
* 包含应用权限相关的配置,如超级管理员角色、默认角色等
*/
permission: {
// 超级管理员角色名称
superAdminRole: 'admin',
// 默认用户角色名称
defaultRole: 'user'
}
}

View File

@@ -1,63 +1,136 @@
/**
* 围栏生成Hook
* 提供在3D地图上生成围栏的功能
* 基于THREE.js实现3D围栏的创建、纹理生成和材质配置
*
* @param {Object} options - 围栏配置选项
* @param {number} options.height - 围栏高度默认值为20
* @param {string} options.color - 围栏颜色,默认值为橙色'#ff7f50'
* @param {boolean} options.isCloseTop - 是否封闭顶部默认值为false不封闭
* @returns {Object} - 包含生成围栏函数的对象
*/
export function useFence({height = 20, isCloseTop = false, color = '#ff7f50'}) {
const { THREE } = window.VgoMap
const tex = generateTexture(64)
if(tex) {
tex.wrapT = tex.wrapS = THREE.RepeatWrapping
}
/**
* 生成围栏材质纹理
* 使用64x64的HTML5 Canvas创建垂直渐变纹理
* 纹理将用于围栏的侧面材质
*/
const tex = generateTexture(64)
// 设置纹理的重复模式为REPEAT
if(tex) {
tex.wrapT = tex.wrapS = THREE.RepeatWrapping
}
/**
* 生成围栏函数
* 创建一个3D围栏网格对象使用THREE.js的ExtrudeGeometry实现挤出效果
*
* @param {Array<THREE.Vector3>} points - 围栏的顶点数组,按顺时针或逆时针顺序排列
* @returns {THREE.Mesh} - 生成的围栏3D网格对象
*/
function generateFence(points) {
// 创建THREE.js形状对象
const shape = new THREE.Shape()
// 根据输入的顶点数组设置形状
shape.setFromPoints(points)
// 创建挤出几何体将2D形状挤出为3D围栏
const geo = new THREE.ExtrudeGeometry(shape, {
steps: 2,
depth: height,
bevelEnabled: false,
steps: 2, // 挤出的层数
depth: height, // 挤出的深度(围栏高度)
bevelEnabled: false, // 禁用斜角
})
// 创建围栏材质数组
const mater = [
// 顶部材质
new THREE.MeshBasicMaterial({
color,
transparent: true,
opacity: 0.1,
visible: isCloseTop,
visible: isCloseTop, // 根据配置决定是否显示顶部
}),
// 侧面材质
new THREE.MeshBasicMaterial({
map: tex,
map: tex, // 使用渐变纹理
transparent: true,
color,
opacity: 0.1,
side: 2,
side: 2, // 设置为双面显示THREE.DoubleSide
})
]
// 创建围栏网格对象
const mesh = new THREE.Mesh(geo, mater)
// 统一设置所有材质的透明度为0.7
mesh.material.forEach(m => m.opacity = 0.7)
return mesh
}
/**
* 生成纹理函数
* 创建一个垂直渐变的HTML5 Canvas纹理用于围栏侧面的材质
*
* @param {number} size - 纹理大小默认值为64
* @returns {THREE.Texture|null} - 生成的THREE.js纹理对象如果创建失败则返回null
*/
function generateTexture (size = 64) {
// 创建HTML5 Canvas元素
let canvas = document.createElement('canvas')
canvas.width = size
canvas.height = size
// 获取Canvas 2D上下文
let ctx = canvas.getContext('2d')
if(!ctx) return
if(!ctx) return null // 如果无法获取上下文则返回null
// 创建垂直线性渐变
let linearGradient = ctx.createLinearGradient(0, 0, 0, size)
// 顶部:完全透明
linearGradient.addColorStop(0.2, hexToRgba(color, 0.0))
// 中部:半透明
linearGradient.addColorStop(0.8, hexToRgba(color, 0.5))
// 底部:不透明
linearGradient.addColorStop(1.0, hexToRgba(color, 1.0))
// 使用渐变填充整个Canvas
ctx.fillStyle = linearGradient
ctx.fillRect(0, 0, size, size)
// 创建THREE.js纹理对象
let texture = new THREE.Texture(canvas)
texture.needsUpdate = true // 必须
texture.needsUpdate = true // 必须设置为true通知THREE.js更新纹理
return texture
}
/**
* 将十六进制颜色转换为RGBA格式
* 用于在Canvas绘制时设置带透明度的颜色
*
* @param {string} hex - 十六进制颜色值,格式为'#RRGGBB'
* @param {number} opacity - 透明度取值范围为0-1默认值为1完全不透明
* @returns {string} - RGBA格式的颜色字符串格式为'rgba(R,G,B,A)'
*/
function hexToRgba (hex, opacity = 1) {
return 'rgba(' + parseInt('0x' + hex.slice(1, 3)) + ',' + parseInt('0x' + hex.slice(3, 5)) + ',' +
parseInt('0x' + hex.slice(5, 7)) + ',' + opacity + ')'
// 提取R、G、B分量并转换为十进制
const r = parseInt('0x' + hex.slice(1, 3))
const g = parseInt('0x' + hex.slice(3, 5))
const b = parseInt('0x' + hex.slice(5, 7))
// 构造并返回RGBA格式的颜色字符串
return `rgba(${r},${g},${b},${opacity})`
}
/**
* 返回hook提供的函数
* 将内部实现的generateFence函数暴露给外部使用
*/
return {
generateFence,
generateFence, // 生成围栏的主要函数
}
}

View File

@@ -1,29 +1,71 @@
/**
/**
* 应用入口文件
* 负责创建Vue应用实例、配置插件、注册全局组件和属性
* 是整个Vue应用的启动点和配置中心
*/
// 导入Vue的createApp函数用于创建应用实例
import { createApp } from 'vue'
// 导入应用的根组件
import App from './App.vue'
// 导入路由配置
import router from './router'
// 导入Element Plus UI组件库
import ElementPlus from 'element-plus'
// 导入Element Plus的CSS样式
import 'element-plus/dist/index.css'
// 导入全局自定义样式
import './assets/main.css'
// 导入Pinia状态管理库
import { createPinia } from 'pinia'
// 导入Element Plus的所有图标组件
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// 导入全局配置文件
import config from './config'
const app = createApp(App)
const pinia = createPinia()
/**
* 创建Vue应用实例和状态管理实例
*/
const app = createApp(App) // 创建Vue应用实例以App组件为根组件
const pinia = createPinia() // 创建Pinia状态管理实例
// 全局注册配置
/**
* 全局注册配置对象
* 使配置在整个应用中可访问
*/
// 为Options API提供全局配置属性
app.config.globalProperties.$config = config
// 为Composition API提供配置
// 为Composition API提供全局配置通过inject('config')获取
app.provide('config', config)
// 全局注册所有图标
/**
* 全局注册所有Element Plus图标组件
* 使所有图标组件在应用中无需单独导入即可使用
*/
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
app.component(key, component) // 注册每个图标组件
}
app.use(ElementPlus)
app.use(router)
app.use(pinia)
/**
* 应用插件配置
* 依次注册Element Plus、Vue Router和Pinia插件
*/
app.use(ElementPlus) // 注册Element Plus UI组件库
app.use(router) // 注册Vue Router路由管理
app.use(pinia) // 注册Pinia状态管理
app.mount('#app')
/**
* 将Vue应用实例挂载到DOM元素
* 使应用在id为'app'的DOM元素中渲染
*/
app.mount('#app') // 将应用挂载到HTML中的#app元素上

View File

@@ -1,37 +1,52 @@
/**
/**
* 应用路由配置文件
* 定义应用的路由结构、组件映射和权限控制规则
*/
// 导入Vue Router核心函数
import { createRouter, createWebHistory } from 'vue-router'
import LoginView from '../views/LoginView.vue'
import LayoutView from '../views/LayoutView.vue'
import HomeView from '../views/HomeView.vue'
import KeyManager from '../views/Key/KeyManager.vue'
// 导入页面组件
import LoginView from '../views/LoginView.vue' // 登录页面组件
import LayoutView from '../views/LayoutView.vue' // 主布局组件
import HomeView from '../views/HomeView.vue' // 首页组件
import KeyManager from '../views/Key/KeyManager.vue' // 钥匙管理组件
// 导入用户状态管理
import { useUserStore } from '../stores/user'
/**
* 创建路由实例
* 使用HTML5 History模式管理路由需要服务器端配置支持
*/
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/login',
name: 'login',
component: LoginView
component: LoginView // 登录页面,无需身份验证即可访问
},
{
path: '/',
name: 'layout',
component: LayoutView,
meta: { requiresAuth: true },
component: LayoutView, // 主布局页面,包含侧边栏、顶栏等公共布局元素
meta: { requiresAuth: true }, // 标记此路由需要登录权限
children: [
{
path: '',
name: 'home',
components: {
map: HomeView
map: HomeView // 默认视图渲染首页组件到map视图容器
}
},
{
path: '/Key/KeyManager',
name: 'keyManager',
components: {
map: HomeView,
right: KeyManager
map: HomeView, // 左侧显示地图视图
right: KeyManager // 右侧显示钥匙管理视图
}
}
// 可以在这里添加更多子路由
@@ -40,18 +55,26 @@ const router = createRouter({
]
})
// 路由守卫
/**
* 全局前置路由守卫
* 用于拦截所有路由跳转请求,实现基于角色的权限控制
*/
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
const isLoggedIn = userStore.isLoggedIn
const userStore = useUserStore() // 获取用户状态管理实例
const isLoggedIn = userStore.isLoggedIn // 检查用户是否已登录
// 检查路由是否需要身份验证
if (to.matched.some(record => record.meta.requiresAuth)) {
// 需要身份验证的路由
if (!isLoggedIn) {
// 未登录时重定向到登录页面
next('/login')
} else {
// 已登录用户允许访问
next()
}
} else {
// 不需要身份验证的路由直接放行
next()
}
})

View File

@@ -1,28 +1,63 @@
/**
* 用户状态管理模块
* 使用Pinia实现的用户状态管理负责管理用户的登录状态、认证token和用户信息
*/
// 导入Pinia的store定义函数
import { defineStore } from 'pinia'
// 导入Axios用于API请求
import axios from 'axios'
/**
* 用户状态管理store定义
* 使用Pinia的defineStore函数创建用户状态管理模块
* @returns {Object} - 用户store实例提供用户状态管理的各种方法和属性
*/
export const useUserStore = defineStore('user', {
/**
* 状态定义
* 初始化用户相关的状态数据
*/
state: () => ({
token: '',
userInfo: null,
menuList: []
token: '', // 用户认证令牌用于API请求认证
userInfo: null, // 用户详细信息对象
menuList: [] // 用户可访问的菜单列表
}),
/**
* 计算属性getters
* 提供基于state的派生状态用于获取用户登录状态等信息
*/
getters: {
/**
* 检查用户是否已登录
* @param {Object} state - store当前状态
* @returns {boolean} - 用户是否已登录的布尔值
*/
isLoggedIn: (state) => !!state.token
},
/**
* 动作方法actions
* 提供修改状态的方法,包括登录、登出和初始化用户信息
*/
actions: {
// 用户登录
/**
* 用户登录方法
* 处理用户登录请求,保存登录状态和用户信息
* @param {Object} credentials - 用户登录凭据,包含用户名和密码等信息
* @returns {Promise<Object>} - 登录结果对象,包含成功状态和错误信息
*/
async login(credentials) {
try {
// 模拟登录请求
const response = await axios.post('/api/login', credentials)
const { token, userInfo, menuList } = response.data
this.token = token
this.userInfo = userInfo
this.menuList = menuList
this.token = token // 保存token到状态
this.userInfo = userInfo // 保存用户信息
this.menuList = menuList // 保存菜单列表
// 保存token到localStorage
localStorage.setItem('token', token)
@@ -34,15 +69,22 @@ export const useUserStore = defineStore('user', {
}
},
// 用户登出
/**
* 用户登出方法
* 清除用户所有状态信息并移除本地存储的token
*/
logout() {
this.token = ''
this.userInfo = null
this.menuList = []
localStorage.removeItem('token')
this.token = '' // 清除token
this.userInfo = null // 清除用户信息
this.menuList = [] // 清除菜单列表
localStorage.removeItem('token') // 移除本地存储的token
},
// 初始化用户信息
/**
* 初始化用户信息方法
* 从本地存储恢复用户登录状态并获取最新用户信息
* @returns {Promise<void>} - 初始化过程的Promise
*/
async initUserInfo() {
const token = localStorage.getItem('token')
if (token) {
@@ -50,11 +92,11 @@ export const useUserStore = defineStore('user', {
try {
// 模拟获取用户信息
const response = await axios.get('/api/user/info')
this.userInfo = response.data.userInfo
this.menuList = response.data.menuList
this.userInfo = response.data.userInfo // 保存用户信息
this.menuList = response.data.menuList // 保存菜单列表
} catch (error) {
console.error('获取用户信息失败:', error)
this.logout()
this.logout() // 失败时自动登出
}
}
}

View File

@@ -1,18 +1,21 @@
<template>
<!-- 首页组件 -->
<div class="w-full h-full relative">
<!-- 3D地图容器 - 充满整个页面 -->
<!-- 3D地图容器 - 充满整个页面展示仓库3D可视化效果 -->
<div id="map3d-container" class="absolute inset-0 z-0 bg-gradient-to-br from-blue-950 via-[rgba(12,43,77,0.9)] to-cyan-950">
<Map/>
<Map/> <!-- 3D地图组件 -->
</div>
<!-- 悬浮信息卡片 - 科技感UI -->
<!-- 悬浮信息卡片 - 科技感UI显示系统状态信息 -->
<div class="absolute top-24 right-8 z-10 bg-[rgba(12,43,77,0.85)] backdrop-blur-sm rounded-lg border border-cyan-700/50 shadow-lg p-4 w-64">
<!-- 卡片标题和状态指示器 -->
<div class="flex items-center justify-between mb-3">
<h3 class="text-cyan-300 font-medium">系统状态</h3>
<div class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
<div class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div> <!-- 绿色脉冲动画表示系统正常运行 -->
</div>
<!-- 最后更新时间 -->
<div class="text-xs text-cyan-100/70 mb-4">最后更新: {{ currentDate }}</div>
<!-- 系统信息列表 -->
<!-- 系统信息列表 - 显示API地址版本等信息 -->
<div class="space-y-3">
<div class="flex items-center justify-between">
<span class="text-sm text-cyan-100/80">API地址</span>
@@ -32,10 +35,21 @@
</template>
<script setup>
/**
* 首页组件脚本部分
* 使用组合式API实现首页功能
*/
// 导入Vue组合式API
import { ref, onMounted, inject } from 'vue'
// 导入3D地图组件
import Map from "@/views/Map.vue"
// 通过inject获取全局配置并提供默认值以防止页面空白
/**
* 通过inject获取全局配置并提供默认值以防止页面空白
* @type {Object} config - 全局配置对象
*/
const config = inject('config', {
app: {
name: '仓库管理操作端',
@@ -49,15 +63,21 @@ const config = inject('config', {
}
})
// 提取常用配置项(带安全检查)
const appName = config?.app?.name || '仓库管理操作端'
const appVersion = config?.app?.version || '1.0.0'
const apiBaseUrl = config?.api?.baseUrl || '/api'
const defaultPageSize = config?.pagination?.defaultPageSize || 20
/**
* 提取常用配置项(带安全检查)
*/
const appName = config?.app?.name || '仓库管理操作端' // 应用名称
const appVersion = config?.app?.version || '1.0.0' // 应用版本
const apiBaseUrl = config?.api?.baseUrl || '/api' // API基础地址
const defaultPageSize = config?.pagination?.defaultPageSize || 20 // 默认分页大小
const currentDate = ref('')
const currentDate = ref('') // 当前日期字符串
// 格式化当前日期
/**
* 格式化当前日期为指定格式
* @param {Date} date - 日期对象
* @returns {string} - 格式化后的日期字符串
*/
const formatDate = (date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
@@ -70,8 +90,11 @@ const formatDate = (date) => {
return `${year}${month}${day}日 星期${weekDay} ${hours}:${minutes}`
}
/**
* 组件挂载时的初始化操作
*/
onMounted(() => {
currentDate.value = formatDate(new Date())
currentDate.value = formatDate(new Date()) // 设置当前日期
// 模拟3D地图加载进度
setTimeout(() => {
@@ -82,12 +105,18 @@ onMounted(() => {
</script>
<style scoped>
/* 首页样式 */
/**
* 首页组件样式定义
*/
/* 地图容器样式 */
#map3d-container {
transition: all 0.5s ease;
}
/* 自定义动画 */
/**
* 自定义动画定义
*/
/* 慢速旋转动画 */
@keyframes spin-slow {
from {
transform: rotate(0deg);
@@ -97,6 +126,7 @@ onMounted(() => {
}
}
/* 慢速反向旋转动画 */
@keyframes spin-slow-reverse {
from {
transform: rotate(360deg);
@@ -106,6 +136,7 @@ onMounted(() => {
}
}
/* 极慢速旋转动画 */
@keyframes spin-very-slow {
from {
transform: rotate(0deg);
@@ -115,6 +146,7 @@ onMounted(() => {
}
}
/* 动画类定义 */
.animate-spin-slow {
animation: spin-slow 20s linear infinite;
}
@@ -127,7 +159,9 @@ onMounted(() => {
animation: spin-very-slow 40s linear infinite;
}
/* 科技感进度条样式 */
/**
* 科技感进度条样式定制
*/
:deep(.el-progress-bar__outer) {
background-color: rgba(104, 201, 255, 0.15) !important;
border-radius: 10px !important;

View File

@@ -1,19 +1,23 @@
<template>
<!-- 钥匙管理组件模板 -->
<div class="p-4">
<!--选择钥匙柜-->
<!-- 钥匙柜选择器 -->
<el-select v-model="value" placeholder="请选择钥匙柜" style="width: 240px">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-divider />
<!--显示钥匙柜及其中的钥匙状态-->
<!-- 钥匙柜及钥匙状态展示区域 -->
<el-row :gutter="20">
<!-- 遍历钥匙列表每个钥匙显示为一个卡片 -->
<el-col :span="8" v-for="k in keyList" :key="k">
<el-card shadow="always">
<!-- 卡片头部 - 显示槽位号 -->
<template #header>
<div class="card-header">
<span>{{ k.keySlot }}</span>
</div>
</template>
<!-- 卡片内容 - 显示钥匙详细信息 -->
<div class="flex gap-2">
<el-table :data="k.keyDatas" :show-header="false" style="width: 100%">
<el-table-column prop="infotype" label="项目" />
@@ -26,6 +30,7 @@
</el-table-column>
</el-table>
</div>
<!-- 卡片底部 - 操作按钮组 -->
<template #footer>
<div class="card-footer">
<el-button-group>
@@ -42,11 +47,21 @@
</template>
<script lang="ts" setup>
/**
* 钥匙管理组件脚本部分
* 负责钥匙柜的选择、钥匙状态的展示和操作
*/
import { ref } from 'vue'
import { Pointer, Unlock, Lock } from '@element-plus/icons-vue'
import { Pointer, Unlock, Lock } from '@element-plus/icons-vue' // 导入操作按钮图标
const value = ref('')
/**
* 状态定义
*/
const value = ref('') // 当前选择的钥匙柜ID
/**
* 钥匙柜选项列表
*/
const options = [
{
value: '1',
@@ -58,21 +73,31 @@ const options = [
}
]
/**
* 数据类型定义
*/
// 状态标签类型接口
type Status = {
type: string
value: string
type: string // 标签类型primary/success/warning/danger/info
value: string // 标签显示内容
}
// 钥匙数据项类型接口
type KeyData = {
infotype: string
status: Status[]
infotype: string // 信息类型(如:钥匙名称、钥匙状态等)
status: Status[] // 状态标签数组
}
// 钥匙槽位及其中钥匙数据的接口
type KeyDatas = {
keySlot: string
keyDatas: KeyData[]
keySlot: string // 槽位名称
keyDatas: KeyData[] // 钥匙详细数据
}
/**
* 钥匙列表数据
* 模拟从后端获取的钥匙柜中的钥匙数据
*/
const keyList: KeyDatas[] =
[
{
@@ -179,16 +204,21 @@ const keyList: KeyDatas[] =
</script>
<style scoped>
/* 将KeyManager的背景色设置为全透明 */
/**
* 钥匙管理组件样式定义
* 使用空军蓝为主色调,打造科技感界面
*/
/* 设置页面背景为透明 */
:deep(.p-4) {
background-color: transparent !important;
}
/* 确保容器元素背景也是透明的 */
/* 确保所有容器元素背景也是透明的 */
div {
background-color: transparent !important;
}
/* 布局相关样式 */
.el-row {
margin-bottom: 20px;
}
@@ -208,7 +238,7 @@ div {
/* 空军蓝为主色调的卡片样式 */
:deep(.el-card) {
/* 空军蓝半透明背景 */
/* 空军蓝半透明渐变背景 */
background: linear-gradient(to right, rgba(12, 43, 77, 0.95), rgba(12, 43, 77, 0.7)) !important;
backdrop-filter: blur(5px) !important;
border: 1px solid #68c9ff !important;
@@ -232,7 +262,7 @@ div {
font-size: 14px !important;
}
/* 卡片内容中的段落文本样式 */
/* 卡片内容中的表格样式 */
:deep(.el-card__body table tr) {
color: #b6dfff !important;
width: 100%;
@@ -256,7 +286,7 @@ div {
margin-top: 0 !important;
}
/* 调整选择器样式以适应整体色调 */
/* 钥匙柜选择器样式 */
:deep(.el-select) {
background: rgba(12, 43, 77, 0.7) !important;
border: 1px solid #68c9ff !important;

View File

@@ -1,6 +1,7 @@
<template>
<!-- 主布局组件 -->
<div class="h-screen flex flex-col relative overflow-hidden">
<!-- 顶栏 - 悬浮在最上层 -->
<!-- 顶栏 - 悬浮在最上层显示系统标题和用户信息 -->
<header class="h-16 bg-[rgba(20,60,110,0.85)] backdrop-blur-sm shadow-lg flex items-center justify-between px-6 z-30">
<div class="flex items-center">
<el-icon class="text-cyan-300 mr-2"><Database /></el-icon>
@@ -28,14 +29,14 @@
</div>
</header>
<!-- 3D地图容器 - 最底层全屏显示 -->
<!-- 3D地图容器 - 最底层全屏显示用于展示仓库的3D地图视图 -->
<div class="fixed inset-0 z-0">
<router-view name="map" />
</div>
<!-- 侧边栏容器 - 固定定位不占用主内容空间 -->
<!-- 侧边栏容器 - 固定定位不占用主内容空间用于显示系统功能菜单 -->
<div class="fixed left-0 top-16 h-[calc(100%-4rem)] z-20">
<!-- 左侧菜单栏 -->
<!-- 左侧菜单栏 - 可折叠显示多级菜单 -->
<aside class="bg-gradient-to-r from-[rgba(20,60,110,1)] to-[rgba(20,60,110,0.5)] backdrop-blur-sm border-r border-[#68c9ff] overflow-y-auto transition-all duration-300 ease-in-out h-full"
:style="{ width: isCollapsed ? '64px' : '250px' }">
<!-- 菜单内容 -->
@@ -50,8 +51,8 @@
v-if="menuList && menuList.length > 0"
unique-opened="true"
>
<!-- 二级菜单结构 -->
<template v-for="menu in menuList" :key="menu.id">
<!-- 二级菜单结构 - 动态渲染菜单列表 -->
<template v-for="menu in menuList" :key="menu.id">
<!-- 有子菜单的一级菜单 -->
<el-sub-menu
v-if="menu.children && menu.children.length > 0"
@@ -105,8 +106,8 @@
</aside>
</div>
<!-- 折叠按钮 - 固定定位不占用主内容空间 -->
<div class="fixed left-[250px] top-1/2 -translate-y-1/2 z-30 transition-all duration-300 ease-in-out"
<!-- 折叠按钮 - 固定定位不占用主内容空间用于切换侧边栏折叠状态 -->
<div class="fixed left-[250px] top-1/2 -translate-y-1/2 z-30 transition-all duration-300 ease-in-out"
:style="{ left: isCollapsed ? '64px' : '250px' }">
<el-button
@click="toggleCollapse"
@@ -116,7 +117,7 @@
/>
</div>
<!-- Drawer组件 - 用于显示页面内容 -->
<!-- Drawer组件 - 从右侧滑出用于显示具体功能页面内容 -->
<el-drawer
v-model="drawerVisible"
:title="currentMenu?.name || '页面内容'"
@@ -136,23 +137,28 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { ElMessage, ElDrawer } from 'element-plus'
/**
* 主布局组件脚本部分
* 使用组合式API实现布局功能和交互逻辑
* 管理侧边栏折叠状态、菜单导航和用户认证
*/
import { ref, onMounted, computed } from 'vue' // 导入Vue组合式API
import { useRouter } from 'vue-router' // 导入路由相关函数
import { useUserStore } from '@/stores/user' // 导入用户状态管理
import { ElMessage, ElDrawer } from 'element-plus' // 导入Element Plus组件
const router = useRouter()
const userStore = useUserStore()
const activeMenu = computed(() => router.currentRoute.value.path)
const router = useRouter() // 路由实例
const userStore = useUserStore() // 用户状态管理
const activeMenu = computed(() => router.currentRoute.value.path) // 当前激活的菜单路径
// 侧边栏折叠状态
const isCollapsed = ref(false)
const isCollapsed = ref(false) // 侧边栏是否折叠
// Drawer相关状态
const drawerVisible = ref(false)
const currentMenu = ref(null)
const drawerVisible = ref(false) // Drawer是否可见
const currentMenu = ref(null) // 当前选中的菜单项
// 模拟菜单数据(二级菜单结构)
// 模拟菜单数据实际应用中应从userStore获取
const menuList = ref([
{
id: '1',
@@ -160,8 +166,7 @@ const menuList = ref([
path: '/',
icon: 'House'
},
{
id: '2',
{ id: '2',
name: '钥匙管理',
icon: 'Key',
children: [
@@ -170,8 +175,7 @@ const menuList = ref([
{ id: '2-3', name: '钥匙取用记录', path: '/warehouse/stats' }
]
},
{
id: '3',
{ id: '3',
name: '巡检管理',
icon: 'View',
children: [
@@ -180,8 +184,7 @@ const menuList = ref([
{ id: '3-3', name: '巡检记录', path: '/inventory/alerts' }
]
},
{
id: '4',
{ id: '4',
name: '车辆管理',
icon: 'Van',
children: [
@@ -191,16 +194,14 @@ const menuList = ref([
{ id: '4-4', name: '车辆出入营区记录', path: '/inventory/alerts' }
]
},
{
id: '5',
{ id: '5',
name: '环境监控',
icon: 'Odometer',
children: [
{ id: '5-1', name: '环境变量管理', path: '/inventory/overview' }
]
},
{
id: '6',
{ id: '6',
name: '信息管理',
icon: 'Document',
children: [
@@ -208,8 +209,7 @@ const menuList = ref([
{ id: '6-2', name: '安全提示', path: '/inventory/detail' }
]
},
{
id: '7',
{ id: '7',
name: '访客管理',
icon: 'User',
children: [
@@ -217,8 +217,7 @@ const menuList = ref([
{ id: '7-2', name: '来访车辆管理', path: '/inventory/detail' }
]
},
{
id: '8',
{ id: '8',
name: '营区管理',
icon: 'MapLocation',
children: [
@@ -226,8 +225,7 @@ const menuList = ref([
{ id: '8-2', name: '人员区域管理', path: '/inventory/detail' }
]
},
{
id: '9',
{ id: '9',
name: '无人机管理',
icon: 'Promotion',
children: [
@@ -236,53 +234,83 @@ const menuList = ref([
}
])
/**
* 用户信息响应式数据
* 用于显示当前登录用户的信息
*/
const userInfo = ref({
name: '管理员'
name: '管理员' // 默认用户名称
})
// 切换侧边栏折叠状态
/**
* 切换侧边栏折叠状态
* 控制侧边栏的展开和收起
*/
const toggleCollapse = () => {
isCollapsed.value = !isCollapsed.value
}
// 处理一级菜单点击事件(在折叠状态下自动展开)
/**
* 处理一级菜单点击事件
* 在折叠状态下点击一级菜单时自动展开侧边栏
*/
const handleSubMenuClick = () => {
if (isCollapsed.value) {
toggleCollapse()
}
}
/**
* 处理菜单项点击事件
* @param {Object} menu - 菜单对象,包含路径和名称等信息
*/
const handleMenuClick = (menu) => {
currentMenu.value = menu
drawerVisible.value = true
// 首页菜单仍然使用路由跳转
currentMenu.value = menu // 保存当前选中的菜单项
drawerVisible.value = true // 显示右侧抽屉
// 首页菜单特殊处理:不显示抽屉,直接路由跳转
if (menu.path === '/') {
drawerVisible.value = false
}
router.push(menu.path)
router.push(menu.path) // 跳转到对应路由
}
/**
* 处理用户退出登录
* 清除用户状态并跳转到登录页面
*/
const handleLogout = () => {
userStore.logout()
ElMessage.success('退出登录成功')
router.push('/login')
userStore.logout() // 调用用户store的logout方法清除用户状态
ElMessage.success('退出登录成功') // 显示成功提示信息
router.push('/login') // 跳转到登录页面
}
/**
* 组件生命周期钩子:挂载时执行
* 初始化用户信息和菜单列表
*/
onMounted(() => {
// 初始化用户信息
userStore.initUserInfo().then(() => {
// 更新用户信息
if (userStore.userInfo) {
userInfo.value = userStore.userInfo
}
// 安全检查确保userStore.menuList存在并有值
if (userStore.menuList && userStore.menuList.length > 0) {
menuList.value = userStore.menuList
menuList.value = userStore.menuList // 更新菜单列表
}
})
})
</script>
<style scoped>
/**
* 布局组件样式定义
* 包含菜单、顶栏、Drawer等自定义样式
*/
/* 布局样式 */
.el-menu {
border-right: none;
@@ -293,7 +321,7 @@ onMounted(() => {
display: none !important;
}
/* 空军蓝背景 */
/* 空军蓝背景 - 用户下拉菜单样式 */
:deep(.el-dropdown-menu) {
background-color: rgba(12, 43, 77, 0.95) !important;
border: 1px solid #68c9ff !important;

View File

@@ -30,6 +30,7 @@
<el-checkbox v-model="loginForm.remember" class="text-gray-600">记住密码</el-checkbox>
</el-form-item>
<!-- 登录按钮 -->
<el-form-item>
<el-button
type="primary"
@@ -46,22 +47,40 @@
</template>
<script setup>
/**
* 登录页面脚本部分
* 使用组合式API实现登录逻辑
*/
// 导入Vue组合式API
import { ref } from 'vue'
// 导入Vue Router
import { useRouter } from 'vue-router'
// 导入用户状态管理
import { useUserStore } from '@/stores/user'
// 导入Element Plus消息提示
import { ElMessage } from 'element-plus'
const router = useRouter()
const userStore = useUserStore()
const loginFormRef = ref(null)
const loading = ref(false)
const router = useRouter() // 路由实例
const userStore = useUserStore() // 用户状态管理
const loginFormRef = ref(null) // 表单引用
const loading = ref(false) // 登录按钮加载状态
/**
* 登录表单数据
*/
const loginForm = ref({
username: '',
password: '',
remember: false
username: '', // 用户名
password: '', // 密码
remember: false // 是否记住密码
})
/**
* 表单验证规则
*/
const loginRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
@@ -71,10 +90,15 @@ const loginRules = {
]
}
/**
* 处理登录逻辑
* @returns {Promise<void>}
*/
const handleLogin = async () => {
try {
// 验证表单
await loginFormRef.value.validate()
loading.value = true
loading.value = true // 设置按钮加载状态
// 模拟登录API调用
// 由于是演示环境,我们直接模拟成功登录
@@ -101,18 +125,18 @@ const handleLogin = async () => {
userStore.token = mockResponse.token
userStore.userInfo = mockResponse.userInfo
userStore.menuList = mockResponse.menuList
localStorage.setItem('token', mockResponse.token)
localStorage.setItem('token', mockResponse.token) // 保存token到本地存储
ElMessage.success('登录成功')
router.push('/')
ElMessage.success('登录成功') // 显示成功提示
router.push('/') // 跳转到首页
} catch (error) {
ElMessage.error(error.message || '登录失败')
ElMessage.error(error.message || '登录失败') // 显示错误提示
} finally {
loading.value = false
loading.value = false // 重置加载状态
}
}, 1000)
} catch (error) {
loading.value = false
loading.value = false // 重置加载状态
}
}
</script>

View File

@@ -1,16 +1,34 @@
<script setup>
/**
* 3D地图主组件
* 负责初始化VgoMap 3D地图引擎管理地图状态并为子组件提供共享数据
*/
// 导入Vue组合式API
import { onMounted, ref, computed, provide } from "vue"
import Fence from "../components/Fence.vue"
import Filter from "../components/Filter.vue"
const { VgoMap } = window
let mapId = '1958120048849719296'
const isLoaded = ref(false)
// 导入子组件
import Fence from "../components/Fence.vue" // 3D地图围栏组件用于绘制区域边界
import Filter from "../components/Filter.vue" // 地图筛选组件,用于分类显示/隐藏标记点
const map = ref()
/**
* 地图相关常量和状态定义
*/
const { VgoMap } = window // 从全局对象获取VgoMap 3D地图引擎实例
let mapId = '1958120048849719296' // 地图资源唯一标识ID
const isLoaded = ref(false) // 地图加载完成状态标记
const map = ref() // 地图实例引用用于操作地图API
/**
* 计算所有多边形数据
* 合并室外和室内的多边形数据,为围栏和筛选组件提供完整的地图数据
* @returns {Array} - 合并后的多边形数据数组
*/
const polygonDataAll = computed(() => {
// 获取室外多边形数据
const outDoor = map.value?.mapData?.polygonData ?? []
// 获取室内多边形数据(遍历建筑和楼层)
const inDoor = map?.mapData?.build?.reduce((result, build) => {
build.floor.forEach(fItem => {
result.push(...fItem.polygonData)
@@ -18,43 +36,61 @@ const polygonDataAll = computed(() => {
return result
}, []) ?? []
// 合并室内外数据并返回
return [...outDoor, ...inDoor]
})
provide("map", computed(() => map.value))
provide('polygonDataAll', polygonDataAll)
/**
* 使用Vue的provide API为子组件提供共享数据
* 使Fence和Filter组件能够访问地图实例和多边形数据
*/
provide("map", computed(() => map.value)) // 提供地图实例
provide('polygonDataAll', polygonDataAll) // 提供多边形数据
/**
* 组件生命周期钩子:挂载后执行
* 初始化3D地图实例并设置事件监听器
*/
onMounted(() => {
// 创建地图实例
map.value = new VgoMap.Map({
el: "mapContainer",
id: mapId,
el: "mapContainer", // 地图容器元素ID
id: mapId, // 地图ID
})
// 监听地图加载完成事件
map.value.on("loaded", () => {
isLoaded.value = true
isLoaded.value = true // 更新地图加载状态
})
})
</script>
<template>
<!-- 地图组件模板 -->
<div class="wrapper">
<!-- 地图渲染容器VgoMap将在此处渲染3D地图 -->
<div id="mapContainer"></div>
<!-- UI组件容器 - 当地图加载完成后显示确保组件在地图数据准备就绪后初始化 -->
<div v-if="isLoaded" class="ui">
<Fence/>
<Filter/>
<Fence/> <!-- 围栏组件用于在地图上绘制区域边界 -->
<Filter/> <!-- 筛选组件用于控制地图上不同类型标记点的显示/隐藏 -->
</div>
</div>
</template>
<style scoped>
/**
* 地图组件样式定义
* 设置地图容器的基本布局样式
*/
.wrapper {
width: 100%;
height: 100%;
position: relative;
position: relative; /* 相对定位,为子组件的绝对定位提供基准 */
}
#mapContainer {
width: 100%;
height: 100%;
height: 100%; /* 使地图充满整个父容器 */
}
</style>