更新项目:修复CarManager组件并提交全部文件

This commit is contained in:
g82tt
2025-10-17 14:09:07 +08:00
parent 85b63b23cd
commit 64a731b4a5
46 changed files with 5631 additions and 411 deletions

View File

@@ -1,10 +1,70 @@
# 仓库管理操作端项目更新记录
## 2024年4月17日 - 巡检路径管理功能开发
### 巡检路径模块
* **[新增]**创建PathManager组件用于管理巡检路径信息
* **[新增]**创建PathEditor组件包含路径编辑、地图显示和路径点配置功能
* **[新增]**创建PathSchedule组件用于管理巡检计划包含周期、时间、人员和路线配置
* 📝**[修改]**在PathEditor组件中引入Map组件实现地图展示功能
* 📝**[修改]**优化PathSchedule组件弹窗样式设置为空军蓝、透明度40%并添加高斯模糊效果
* 📝**[修改]**将PathSchedule组件表单布局从两列改为单列避免控件超出弹窗范围
* **[新增]**创建PathLog组件实现巡检日志管理功能包含人员选择、日期时间、设备和排班记录配置
* 📝**[修改]**:更新路由配置,添加巡检日志管理相关路由
* 📝**[修改]**更新LayoutView中的菜单配置添加巡检日志管理菜单项
* 📝**[修改]**设置PathLog组件表格背景为透明样式优化视觉效果
* 📝**[修改]**进一步优化PathLog组件表格表头透明度确保表头完全透明
* 📝**[修改]**:更新路由配置,添加巡检路径和计划管理相关路由
## 2024年4月17日 - 钥匙申请功能优化
### 钥匙管理模块
* 📝**[修改]**优化KeyApply组件从userStore中获取当前登录用户名并显示在申请人字段
* 📝**[修改]**:添加了错误处理和日志输出,方便调试
## 2024年4月15日 - 界面优化与路由参数修复
* **[新增]** 钥匙管理模块功能
* **[新增]** 创建钥匙使用申请组件(KeyApply.vue)
* **[新增]** 创建钥匙取用记录组件(KeyRecord.vue)
* 📝**[修改]** 更新路由配置,添加新组件路由
* 📝**[修改]** 增强KeyManager组件实现申请按钮与申请组件的绑定
* 🐛**[修复]** 钥匙管理模块问题修复
* 🐛**[修复]** 增强KeyApply组件的路由参数处理逻辑添加类型检查和错误处理
* 📝**[修改]** 将KeyRecord组件的表格背景修改为透明
* 📝**[修改]** 优化KeyManager组件中的handleApply函数从keyList中提取并组织钥匙信息
* 📝**[修改]** 改进KeyApply组件优先使用直接传递的钥匙信息同时保留后备提取逻辑
## 2025年10月12日 - 图标规范更新
* 📝**[修改]** 文档规范
* **[新增]** 修改新增功能图标为绿色+号()
* 📝**[修改]** 修改功能图标更换为橙色铅笔(📝)
* 🐛**[新增]** 新增[修复]标签,配置紫色虫子图标(🐛)
* 📝**[修改]** 更新所有示例格式以符合新规范
## 2025年10月12日 - 提示词规范优化
* 📝**[修改]** 文档规范
* 修改图标表示法:新增使用➕图标,修改使用📝图标,新增修复使用🐛图标
* 修改标签格式:变更类型使用方括号包裹
## 2025年10月12日 - 提示词规范更新
* **[新增]** 文档规范
* **[新增]**在TraePrompt_猫娘工程师.md中添加Git提交规范
* 📝**[修改]**明确要求Git提交前必须更新UpdateLog.md
* 📝**[修改]**规定使用YYYY年MM月DD日日期格式
* 📝**[修改]**:定义模块分级输出(最多三级)与图标表示法
* **[新增]**:使用➕表示新增、📝表示修改、🐛表示修复、❌表示删除
* **[新增]**:添加了标准示例格式模板
## 2025年10月12日 - 全屏功能实现
**主要更新内容:**
- 在应用顶栏中添加了全屏切换按钮,位于用户头像和用户名右侧
- 实现了完整的全屏功能,包括:
- **[新增]** 在应用顶栏中添加了全屏切换按钮,位于用户头像和用户名右侧
- **[新增]** 实现了完整的全屏功能,包括:
- 进入全屏模式
- 退出全屏模式
- 自动检测系统全屏状态变化

View File

@@ -0,0 +1 @@
{"code":200,"data":{"bodyParts":[],"symptoms":[],"symptomCauses":[]},"message":null}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"code":200,"data":{"id":"1977947221534052352","name":"安徽省滁州市南谯区大柳镇北园桥","themeId":1,"cover":"","industry":"园区","center":[118.08292026198603,32.42616151903459],"address":"大柳镇","remark":"安徽省滁州市南谯区大柳镇北园桥","creationTime":"2025-10-14T11:59:00.7533897","templateId":"d6aee0aa-0c20-4ded-2cf3-08ddcf3ee146","needLogin":true,"other":"{\"theme\":\"\"}","version":null,"tenantId":"1977945000528449536"},"message":null}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"code":200,"data":[],"message":null}

View File

@@ -54,6 +54,18 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe733;</span>
<div class="name">refresh</div>
<div class="code-name">&amp;#xe733;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe86e;</span>
<div class="name">route-line</div>
<div class="code-name">&amp;#xe86e;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe731;</span>
<div class="name">访客</div>
@@ -2382,9 +2394,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1756889729585') format('woff2'),
url('iconfont.woff?t=1756889729585') format('woff'),
url('iconfont.ttf?t=1756889729585') format('truetype');
src: url('iconfont.woff2?t=1760587007493') format('woff2'),
url('iconfont.woff?t=1760587007493') format('woff'),
url('iconfont.ttf?t=1760587007493') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -2410,6 +2422,24 @@
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont iconrefresh"></span>
<div class="name">
refresh
</div>
<div class="code-name">.iconrefresh
</div>
</li>
<li class="dib">
<span class="icon iconfont iconroute-line"></span>
<div class="name">
route-line
</div>
<div class="code-name">.iconroute-line
</div>
</li>
<li class="dib">
<span class="icon iconfont iconfangke"></span>
<div class="name">
@@ -5902,6 +5932,22 @@
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#iconrefresh"></use>
</svg>
<div class="name">refresh</div>
<div class="code-name">#iconrefresh</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#iconroute-line"></use>
</svg>
<div class="name">route-line</div>
<div class="code-name">#iconroute-line</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#iconfangke"></use>

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 2223488 */
src: url('iconfont.woff2?t=1756889729585') format('woff2'),
url('iconfont.woff?t=1756889729585') format('woff'),
url('iconfont.ttf?t=1756889729585') format('truetype');
src: url('iconfont.woff2?t=1760587007493') format('woff2'),
url('iconfont.woff?t=1760587007493') format('woff'),
url('iconfont.ttf?t=1760587007493') format('truetype');
}
.iconfont {
@@ -13,6 +13,14 @@
-moz-osx-font-smoothing: grayscale;
}
.iconrefresh:before {
content: "\e733";
}
.iconroute-line:before {
content: "\e86e";
}
.iconfangke:before {
content: "\e731";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,20 @@
"css_prefix_text": "icon",
"description": "",
"glyphs": [
{
"icon_id": "44044081",
"name": "refresh",
"font_class": "refresh",
"unicode": "e733",
"unicode_decimal": 59187
},
{
"icon_id": "42232309",
"name": "route-line",
"font_class": "route-line",
"unicode": "e86e",
"unicode_decimal": 59502
},
{
"icon_id": "41061831",
"name": "访客",

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

33
src/UpdateLog.md Normal file
View File

@@ -0,0 +1,33 @@
2024-6-7
- 修复CarManager组件多个显示问题
- 移除了formRef和applyFormRef的冗余初始化定义
- 增强了表单提交的错误处理逻辑
- 用原生alert替代可能不存在的$message组件
- 优化了弹窗样式提高背景不透明度至0.95z-index设置为9999
- 为表格和卡片添加边框样式确保可见
- 添加完整的按钮样式确保操作按钮可见
- 优化输入框、选择框等表单元素样式
- 添加分页组件和确认对话框样式
2024-1-21
- 创建车辆信息管理CarManager组件包含车辆信息表格展示、增删改查功能
- 创建车辆出入库记录CarUseLog组件实现车辆出入库记录的管理
- 创建车辆出入营区记录CarEntryExitLog组件实现车辆进出营区记录的管理
- CarManager组件中添加了车辆申请功能点击申请按钮可打开弹窗展示车辆信息、申请人和多行申请事由
- 更新路由配置,添加三个车辆管理组件的路由
- 更新菜单配置,添加车辆管理相关菜单项
- 对Car文件夹下的三个组件CarManager、CarUseLog、CarEntryExitLog进行样式修改
- 将所有表格背景改为透明
- 将所有表头背景改为透明
- 调整表格边框颜色为半透明浅蓝色
- 调整表格文字颜色为浅蓝色
- 添加表格悬停效果
- 修复CarManager弹窗无法正常打开的问题
- 添加表单引用的初始化
- 增加表单引用的安全检查
- 提高弹窗不透明度,确保弹窗可见
- 添加z-index属性确保弹窗在最上层显示
- PathLog组件中表格表头改为透明
- 添加了表头透明度的CSS样式确保表头在各种状态下都保持透明

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -0,0 +1,38 @@
<script setup>
import { inject, ref, watch, computed } from "vue"
import { useDisplayColor } from "../hooks/useDisplayColor"
// 可以使用pinia等管理全局数据这里只是方便演示, 直接注入了上层提供的数据
const map = inject("map")
const { changeDisplayColor } = useDisplayColor(computed(() => map.value))
const isChangeColor = ref(true)
watch(isChangeColor, (value) => {
changeDisplayColor(value)
})
</script>
<template>
<ul class="display-color">
<li>
<label for="defalut-map-color">默认</label>
<input id="defalut-map-color" name="color" type="radio" :value="true" v-model="isChangeColor">
</li>
<li>
<label for="changed-map-color">科技</label>
<input id="changed-map-color" name="color" type="radio" :value="false" v-model="isChangeColor">
</li>
</ul>
</template>
<style scoped>
.display-color {
margin: 0;
li, label, input {
cursor: pointer;
}
}
</style>

View File

@@ -0,0 +1,61 @@
<script setup>
import { inject } from "vue"
import { useNavi } from "../hooks/useNavi"
// 可以使用pinia等管理全局数据这里只是方便演示, 直接注入了上层提供的数据
const map = inject("map")
const {
pathIdList,
addPathId,
startNavi,
pauseNavi,
restoreNavi,
stopNavi,
} = useNavi(map)
map.value.on('click', (e) => {
const polygon = e.object?.userData?.polygonData // 获取点位数据
if(!polygon?.isNavi) return
addPathId(polygon.id)
})
defineExpose({
addPathId,
})
</script>
<template>
<div class="navi">
<div class="path-list-status">
<div>点击具有分类的标签或模型生成线路:</div>
<div class="selected">{{ pathIdList.join(",") }}</div>
<button @click="() => pathIdList.pop()">删除末尾</button>
<button @click="() => pathIdList = []">清空</button>
<button @click="() => startNavi()" :disabled="pathIdList.length < 2">开始导航</button>
<button @click="() => pauseNavi()" :disabled="pathIdList.length < 2">暂停</button>
<button @click="() => restoreNavi()" :disabled="pathIdList.length < 2">继续</button>
<button @click="() => stopNavi()" :disabled="pathIdList.length < 2">结束导航</button>
</div>
</div>
</template>
<style scoped>
.path-list-status {
display: flex;
align-items: center;
gap: 5px;
}
.selected {
min-height: 1.5em;
width: 300px;
padding: 0 5px;
background-color: #fff;
border: 1px solid #767676;
display: flex;
align-items: center;
overflow: auto;
}
</style>

View File

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

View File

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

41
src/components/Sky.vue Normal file
View File

@@ -0,0 +1,41 @@
<script setup>
import { inject, ref, watch, onBeforeUnmount } from "vue"
import { useSkyLight } from "../hooks/useSkyLight"
const map = inject("map")
const { skyGroup, init, setTime } = useSkyLight(map.value)
const defaultTime = ref(420)
const timeOptions = Array.from({length: 24}).map((_, i) => ({
label: `${i}`,
value: i * 60,
}))
map.value.amap.maxPitch = 180
window.$setTime = setTime
init()
setTime(defaultTime.value)
watch(defaultTime, (value) => {
setTime(value)
})
onBeforeUnmount(() => {
skyGroup.value?.dispose()
})
</script>
<template>
<div class="sky-select">
<select name="time" id="time" v-model="defaultTime">
<option v-for="time in timeOptions" :key="time.value" :value="time.value">{{ time.label }}</option>
</select>
</div>
</template>
<style scoped>
select {
min-width: 100px;
}
</style>

View File

@@ -0,0 +1,57 @@
import { computed } from "vue"
export function useClassPolygon(allPolygon) {
const classPoiList = computed(() => {
return allPolygon.value.filter(i => i?.class?.length).reduce((result, item) => {
item.class.forEach((classId) => {
if (!result?.[classId]) {
result[classId] = []
}
result[classId].push(item)
})
return result
}, {})
})
const classDict = {
"327": {
"parentId": 0,
"name": "摄像头",
"sortNo": 0,
"customerUserId": 0,
"id": 327
},
"328": {
"parentId": 0,
"name": "烟感器",
"sortNo": 0,
"customerUserId": 0,
"id": 328
},
"329": {
"parentId": 0,
"name": "温湿度感应器",
"sortNo": 0,
"customerUserId": 0,
"id": 329
},
"330": {
"parentId": 0,
"name": "立式空调",
"sortNo": 0,
"customerUserId": 0,
"id": 330
},
"331": {
"parentId": 0,
"name": "除湿机",
"sortNo": 0,
"customerUserId": 0,
"id": 331
}
}
return {
classPoiList,
classDict,
}
}

View File

@@ -0,0 +1,66 @@
import { computed } from "vue"
export function useDisplayColor(map) {
const replaceColorMap = computed(() => {
return Object.fromEntries(map.value.mapData?.options?.tilecolorReplace?.map(r => [r.sourceColor?.toLocaleLowerCase(), r.targetColor]) ?? [])
})
const floor = map.value.getFloorById(getCurrentFloorId())
const baseAddPolygon = floor.addPolygon.bind(floor)
const baseAddFaceLine = floor.addFaceLine.bind(floor)
const baseColorCache = {}
function overwriteAdd(isChange) {
function addPolygon(data) {
if(isChange) {
return baseAddPolygon(data)
}
const replace = {
...data,
color: replaceColorMap.value[data.color?.toLocaleLowerCase()] ?? data.color,
}
return baseAddPolygon(replace)
}
function addFaceLine() {
this.data.polygonData.filter(data => data.polygonType === 'faceline' && !data.parentArea)
.forEach(data => {
const replaceColor = replaceColorMap.value[data.color?.toLocaleLowerCase()]
if(replaceColor && !isChange) {
baseColorCache[data.id] = data.color
data.color = replaceColor
} else {
data.color = baseColorCache?.[data.id] ?? data.color
}
})
baseAddFaceLine()
}
return [
addPolygon,
addFaceLine,
].map(i => i.bind(floor))
}
function changeDisplayColor(isChange) {
([floor.addPolygon, floor.addFaceLine] = overwriteAdd(isChange))
floor.rectInstanceMesh?.clearInstances()
floor.circleInstanceMesh?.clearInstances()
floor.polygonGroup?.children?.forEach((polygon) =>{
polygon.dispose()
})
floor.initPolgon()
map.value?.mapTile?.switchBaseColor?.(isChange)
map.value?.mapTile?.init()
}
function getCurrentFloorId() {
return map.value.status?.floor?.data?.id ?? '1'
}
return {
changeDisplayColor,
}
}

View File

@@ -1,136 +1,63 @@
/**
* 围栏生成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
/**
* 生成围栏材质纹理
* 使用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, // 设置为双面显示THREE.DoubleSide
side: 2,
})
]
// 创建围栏网格对象
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 null // 如果无法获取上下文则返回null
// 创建垂直线性渐变
if(!ctx) return
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 // 必须设置为true通知THREE.js更新纹理
texture.needsUpdate = true // 必须
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) {
// 提取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})`
return 'rgba(' + parseInt('0x' + hex.slice(1, 3)) + ',' + parseInt('0x' + hex.slice(3, 5)) + ',' +
parseInt('0x' + hex.slice(5, 7)) + ',' + opacity + ')'
}
/**
* 返回hook提供的函数
* 将内部实现的generateFence函数暴露给外部使用
*/
return {
generateFence, // 生成围栏的主要函数
generateFence,
}
}

79
src/hooks/useNavi.js Normal file
View File

@@ -0,0 +1,79 @@
import { ref, watch } from "vue"
export function useNavi(map) {
const pathIdList = ref([])
function addPathId(id) {
if(pathIdList.value[pathIdList.value.length - 1] === id) return
pathIdList.value.push(id)
}
function startNavi() {
if(pathIdList.value.length < 2) return
map.value.navi.simulate()
map.value.navi.setSimulateSpeed(10)
}
function pauseNavi () {
// 判断当前是否处于导航中
if (map.value.navi.status.isSimulate) {
map.value.navi.pauseSimulate()
}
}
function restoreNavi () {
// 判断当前是否处于暂停中
if (!map.value.navi.isSimulatePause) {
map.value.navi.resumeSimulate()
}
}
function stopNavi () {
// 退出模拟导航
map.value.navi.stopSimulate()
removePathLine()
pathIdList.value = []
}
function removePathLine() {
// 销毁箭头
map.value.navi.removeNaviArrow()
// 销毁线路
map.value.navi.removeNaviLine()
//移除途径点
map.value.navi.removeAllWaypoint()
//移除起点
map.value.navi.removeStart()
//移除终点
map.value.navi.removeEnd()
}
watch(pathIdList, (pathIdList) => {
if(pathIdList?.length < 2) {
removePathLine()
return
}
const end = pathIdList[pathIdList.length - 1]
const [start, ...passPoints]= pathIdList.slice(0, -1);
map.value.navi.removeAllWaypoint()
map.value.navi.setStart(start)
map.value.navi.setWaypoints(passPoints)
map.value.navi.setEnd(end)
map.value.navi.find()
}, {
deep: true,
})
return {
pathIdList,
addPathId,
startNavi,
pauseNavi,
restoreNavi,
stopNavi,
}
}

453
src/hooks/useSkyLight.js Normal file
View File

@@ -0,0 +1,453 @@
import sunImg from "../assets/textures/corona.png"
import starsImg from "../assets/textures/stars.png"
import moonImg from "../assets/textures/moon.png"
const {
TextureLoader,
SphereGeometry,
ShaderMaterial,
MeshBasicMaterial,
Color,
RepeatWrapping,
BackSide,
Mesh,
DirectionalLight,
AmbientLight,
AdditiveBlending,
Group,
} = window.VgoMap.THREE
export function useSkyLight(map) {
let skyDome = null
let sun = null
let moon = null
let stars = null
let sunLight = null
let moonLight = null
let ambientLight = null
const skyGroup = new Group()
const loader = new TextureLoader()
document.querySelector("#sky-mask").remove()
// 加载贴图
const sunTexture = loader.load(sunImg)
const starsTexture = loader.load(starsImg)
const moonTexture = loader.load(moonImg)
const skyColorKeyframes = [
{
time: 0,
top: new Color(0x090b12),
bottom: new Color(0x0f131d)
},
{
time: 240,
top: new Color(0x090b12),
bottom: new Color(0x0f131d)
},
{
time: 360,
top: new Color(0x87ceeb),
bottom: new Color(0xd8f2ff)
},
{
time: 720,
top: new Color(0x87ceeb),
bottom: new Color(0xd8f2ff)
},
{
time: 1020,
top: new Color(0x87ceeb),
bottom: new Color(0xd8f2ff)
},
{
time: 1080,
top: new Color(0x090b12),
bottom: new Color(0x0f131d)
},
{
time: 1440,
top: new Color(0x090b12),
bottom: new Color(0x0f131d)
}
]
// 初始化天空
const init = () => {
createSkyDome()
createSun()
createMoon()
createStars()
createLights()
setupScene()
}
// 创建天空穹顶
const createSkyDome = () => {
const geometry = new SphereGeometry(8000, 64, 64)
const material = new ShaderMaterial({
uniforms: {
topColor: { value: new Color(0x87ceeb) },
bottomColor: { value: new Color(0xd8f2ff) },
sunsetColor: { value: new Color(0xff6b35) },
twilightColor: { value: new Color(0xffa07a) },
offset: { value: 0 },
exponent: { value: 0.6 },
sunPosition: { value: { x: 0, y: 0, z: -7900 } },
isSunset: { value: 0.0 }
},
vertexShader: `
varying vec3 vWorldPosition;
void main() {
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
vWorldPosition = worldPosition.xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform vec3 topColor;
uniform vec3 bottomColor;
uniform vec3 sunsetColor;
uniform vec3 twilightColor;
uniform float offset;
uniform float exponent;
uniform vec3 sunPosition;
uniform float isSunset;
varying vec3 vWorldPosition;
// 简单的云层噪声函数
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
void main() {
vec3 normalized = normalize(vWorldPosition);
float h = normalized.z;
// 基础天空颜色渐变
vec3 skyColor = mix(bottomColor, topColor, max(pow(max(h, 0.0), exponent), 0.0));
// 黄昏效果:仅在太阳临近地平线时显示
if (isSunset > 0.0) {
// 计算与太阳方向的角度
vec3 sunDir = normalize(sunPosition);
float sunDot = dot(normalized, sunDir);
// 检查太阳是否临近地平线z值接近0
float sunHorizonDist = abs(sunDir.z);
float sunNearHorizon = smoothstep(0.25, 0.0, sunHorizonDist); // 进一步收紧太阳接近地平线的范围
// 地平线因子:极度接近地平线时值更大(进一步缩小范围)
float horizonFactor = 1.0 - abs(h);
horizonFactor = pow(horizonFactor, 6.0); // 提高幂次至6大幅缩小范围
// 太阳方向因子:极度接近太阳方向才显示(进一步收紧范围)
float sunFactor = smoothstep(0.5, 0.9, sunDot); // 从0.3-0.85收紧到0.5-0.9
// 云层仅在太阳附近显示的因子
float cloudSunProximity = smoothstep(0.4, 0.8, sunDot); // 云层只在接近太阳方向显示
// 添加多层云层效果,增加密度
vec2 cloudUV = vec2(atan(normalized.x, normalized.y) * 3.0, asin(normalized.z) * 6.0);
// 第一层云:较大的云团
float cloudNoise1 = noise(cloudUV * 3.5);
cloudNoise1 = pow(cloudNoise1, 1.8);
// 第二层云:中等云团
float cloudNoise2 = noise(cloudUV * 6.0);
cloudNoise2 = pow(cloudNoise2, 2.0);
// 第三层云:细小云丝
float cloudNoise3 = noise(cloudUV * 10.0);
cloudNoise3 = pow(cloudNoise3, 2.5);
// 组合多层云,增加密度
float cloudNoise = cloudNoise1 * 0.5 + cloudNoise2 * 0.3 + cloudNoise3 * 0.2;
// 云层遮罩:仅在地平线和太阳附近显示
float cloudLayer = smoothstep(0.35, 0.65, cloudNoise) * horizonFactor * cloudSunProximity * 0.8;
// 综合因子(添加太阳位置检测)
float sunsetFactor = horizonFactor * sunFactor * sunNearHorizon * isSunset;
// 黄昏颜色渐变(柔和的橙色调)
vec3 deepOrange = vec3(0.9, 0.5, 0.3);
vec3 brightOrange = vec3(1.0, 0.65, 0.4);
vec3 duskColor = mix(deepOrange, brightOrange, sunFactor * 0.6);
// 云层受黄昏颜色影响(降低对比度)
vec3 cloudColor = mix(duskColor * 0.85, duskColor * 1.15, cloudNoise);
// 混合黄昏颜色和云层(降低混合强度)
skyColor = mix(skyColor, duskColor, sunsetFactor * 0.35);
skyColor = mix(skyColor, cloudColor, cloudLayer * sunsetFactor * isSunset * 0.6);
}
gl_FragColor = vec4(skyColor, 1.0);
}
`,
side: BackSide
})
skyDome = new Mesh(geometry, material)
skyDome.name = 'sky'
}
// 创建太阳
const createSun = () => {
// 创建太阳核心
const coreGeometry = new SphereGeometry(180, 32, 32)
const coreMaterial = new MeshBasicMaterial({
color: new Color(0xFFA500), // 调整为稍冷的橙色
transparent: true,
blending: AdditiveBlending
})
const core = new Mesh(coreGeometry, coreMaterial)
// 创建日冕效果
const coronaGeometry = new SphereGeometry(3200, 32, 32)
const coronaMaterial = new MeshBasicMaterial({
map: sunTexture,
transparent: true,
blending: AdditiveBlending,
opacity: 0.05,
color: new Color(0xFF8C00) // 调整为稍冷的橙色日冕
})
const corona = new Mesh(coronaGeometry, coronaMaterial)
// 组合太阳
sun = new Group()
sun.add(core)
sun.add(corona)
sun.name = 'sun'
sun.position.set(0, 0, -7900) // 初始位置调整为垂直地平线方向
}
// 创建月亮
const createMoon = () => {
const geometry = new SphereGeometry(200, 32, 32) // 增大月亮尺寸
const material = new MeshBasicMaterial({
map: moonTexture,
transparent: true,
blending: AdditiveBlending
})
moon = new Mesh(geometry, material)
moon.name = 'moon'
moon.position.set(0, 0, 7900) // 初始位置调整为垂直地平线方向
}
// 创建星空
const createStars = () => {
starsTexture.wrapS = RepeatWrapping
starsTexture.wrapT = RepeatWrapping
starsTexture.repeat.set(10, 10)
const geometry = new SphereGeometry(10000, 32, 32)
const material = new MeshBasicMaterial({
map: starsTexture,
transparent: true,
blending: AdditiveBlending,
side: BackSide,
opacity: 0.8
})
stars = new Mesh(geometry, material)
stars.name = 'stars'
}
// 创建光源
const createLights = () => {
// 太阳光
sunLight = new DirectionalLight(0xffffff, 1)
sunLight.position.set(0, 0, -1000)
sunLight.castShadow = true
// 月光
moonLight = new DirectionalLight(0x333366, 0.5)
moonLight.position.set(0, 0, 1000)
// 环境光
ambientLight = new AmbientLight(0x404040, 0.1)
}
// 设置场景
const setupScene = () => {
skyGroup.add(skyDome)
skyGroup.add(sun)
skyGroup.add(moon)
skyGroup.add(stars)
skyGroup.add(sunLight)
skyGroup.add(moonLight)
skyGroup.add(ambientLight)
map.scene.add(skyGroup)
}
// 设置时间分钟数0-1440代表一天
const setTime = (minutes) => {
if (minutes < 0 || minutes > 1440) return
// 计算太阳和月亮的位置(垂直方向圆周运动,自东向西)
const angle = -(minutes / 1440) * Math.PI * 2 // 负号反转方向,使其自东向西
const radius = -7900
const sunY = Math.sin(angle) * radius
const sunZ = Math.cos(angle) * radius
const sunX = 0 // X轴位置保持为0只在YZ平面旋转
// 更新太阳位置
sun.position.set(sunX, sunY, sunZ)
sunLight.position.set(sunX, sunY, sunZ)
if(minutes > 1080 || minutes < 360) {
sun.visible = false
} else {
sun.visible = true
}
// 更新月亮位置(与太阳相对)
moon.position.set(sunX, -sunY, -sunZ)
moonLight.position.set(sunX, -sunY, -sunZ)
if(minutes > 360 && minutes < 1080 ) {
moon.visible = false
} else {
moon.visible = true
}
// 根据时间调整光照强度和颜色
updateLighting(minutes)
// 根据时间调整天空颜色
updateSkyColor(minutes)
// 根据时间调整星空可见度
updateStarsVisibility(minutes)
}
// 更新光照
const updateLighting = (minutes) => {
// 日出日落时段
if (minutes > 360 && minutes < 480) { // 6:00-8:00
const progress = (minutes - 360) / 120
sunLight.intensity = progress
sunLight.color.setRGB(1, progress * 0.7 + 0.3, progress * 0.3 + 0.7)
moonLight.intensity = 0.2
// ambientLight.intensity = 0.2 + progress * 0.3
}
// 正午时段
else if (minutes >= 480 && minutes <= 960) { // 8:00-16:00
sunLight.intensity = 1.5
sunLight.color.setRGB(1, 1, 1)
moonLight.intensity = 0
// ambientLight.intensity = 0.5
}
// 日落时段
else if (minutes > 960 && minutes < 1080) { // 16:00-18:00
const progress = 1 - (minutes - 960) / 120
sunLight.intensity = progress + 0.2
sunLight.color.setRGB(1, progress * 0.7 + 0.3, progress * 0.3 + 0.7)
moonLight.intensity = 0
// ambientLight.intensity = 0.2 + progress * 0.3
}
// 夜晚时段
else {
sunLight.intensity = 0
moonLight.intensity = 0.5
// ambientLight.intensity = 0.1
}
}
// 更新天空颜色
const updateSkyColor = (minutes) => {
const material = skyDome.material
const normalizedMinutes = minutes === 1440 ? 0 : minutes
let currentFrame = skyColorKeyframes[0]
let nextFrame = skyColorKeyframes[skyColorKeyframes.length - 1]
for (let i = 0; i < skyColorKeyframes.length - 1; i++) {
const frame = skyColorKeyframes[i]
const following = skyColorKeyframes[i + 1]
if (normalizedMinutes >= frame.time && normalizedMinutes < following.time) {
currentFrame = frame
nextFrame = following
break
}
}
const segmentDuration = nextFrame.time - currentFrame.time || 1
const progress = (normalizedMinutes - currentFrame.time) / segmentDuration
material.uniforms.topColor.value
.copy(currentFrame.top)
.lerp(nextFrame.top, progress)
material.uniforms.bottomColor.value
.copy(currentFrame.bottom)
.lerp(nextFrame.bottom, progress)
// 更新太阳位置用于黄昏效果
material.uniforms.sunPosition.value = {
x: sun.position.x,
y: sun.position.y,
z: sun.position.z
}
// 仅在太阳临近地平线时显示黄昏效果(缩短时间范围)
let sunsetIntensity = 0.0
if (normalizedMinutes >= 270 && normalizedMinutes <= 330) {
// 日出黄昏270-300最强300-330逐渐减弱
if (normalizedMinutes <= 300) {
sunsetIntensity = (normalizedMinutes - 270) / 30
} else {
sunsetIntensity = 1.0 - (normalizedMinutes - 300) / 30
}
} else if (normalizedMinutes >= 1050 && normalizedMinutes <= 1110) {
// 日落黄昏1050-1080最强1080-1110逐渐减弱
if (normalizedMinutes <= 1080) {
sunsetIntensity = (normalizedMinutes - 1050) / 30
} else {
sunsetIntensity = 1.0 - (normalizedMinutes - 1080) / 30
}
}
material.uniforms.isSunset.value = sunsetIntensity
}
// 更新星空可见度
const updateStarsVisibility = (minutes) => {
// 夜晚时显示星空 (18:00-6:00)
if ((minutes >= 0 && minutes <= 360) || (minutes >= 1080 && minutes <= 1440)) {
stars.visible = true
// 在夜晚时段内0:00-6:00 和 18:00-24:00 有不同的透明度变化
let opacity = 0;
if (minutes >= 1080) { // 18:00-24:00
opacity = Math.min(1, (minutes - 1080) / 180); // 3小时渐显
} else if (minutes <= 360) { // 0:00-6:00
opacity = Math.min(1, (360 - minutes) / 180); // 3小时渐显
}
stars.material.opacity = opacity;
} else {
stars.visible = false;
}
}
return {
skyGroup,
init,
setTime,
}
}

View File

@@ -12,6 +12,14 @@ 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 KeyApply from '../views/Key/KeyApply.vue' // 钥匙使用申请组件
import KeyRecord from '../views/Key/KeyRecord.vue' // 钥匙取用记录组件
import PathManager from '../views/Path/PathManager.vue' // 巡检路径管理组件
import PathSchedule from '../views/Path/PathSchedule.vue' // 巡检计划管理组件
import PathLog from '../views/Path/PathLog.vue' // 巡检日志管理组件
import CarManager from '../views/Car/CarManager.vue' // 车辆管理组件
import CarUseLog from '../views/Car/CarUseLog.vue' // 车辆使用记录组件
import CarEntryExitLog from '../views/Car/CarEntryExitLog.vue' // 车辆进出记录组件
// 导入用户状态管理
import { useUserStore } from '../stores/user'
@@ -48,6 +56,70 @@ const router = createRouter({
map: HomeView, // 左侧显示地图视图
right: KeyManager // 右侧显示钥匙管理视图
}
},
{
path: '/Key/KeyApply',
name: 'keyApply',
components: {
map: HomeView, // 左侧显示地图视图
right: KeyApply // 右侧显示钥匙使用申请视图
}
},
{
path: '/Key/KeyRecord',
name: 'keyRecord',
components: {
map: HomeView, // 左侧显示地图视图
right: KeyRecord // 右侧显示钥匙取用记录视图
}
},
{
path: '/Path/PathManager',
name: 'pathManager',
components: {
map: HomeView, // 左侧显示地图视图
right: PathManager // 右侧显示巡检路径管理视图
}
},
{
path: '/Path/PathSchedule',
name: 'pathSchedule',
components: {
map: HomeView, // 左侧显示地图视图
right: PathSchedule // 右侧显示巡检计划管理视图
}
},
{
path: '/Path/PathLog',
name: 'pathLog',
components: {
map: HomeView, // 左侧显示地图视图
right: PathLog // 右侧显示巡检日志管理视图
}
},
{
path: '/Car/CarManager',
name: 'carManager',
components: {
map: HomeView, // 左侧显示地图视图
right: CarManager // 右侧显示车辆管理视图
}
},
{
path: '/Car/CarUseLog',
name: 'carUseLog',
components: {
map: HomeView, // 左侧显示地图视图
right: CarUseLog // 右侧显示车辆使用记录视图
}
},
{
path: '/Car/CarEntryExitLog',
name: 'carEntryExitLog',
components: {
map: HomeView, // 左侧显示地图视图
right: CarEntryExitLog // 右侧显示车辆进出记录视图
}
}
// 可以在这里添加更多子路由
]

5
src/style.css Normal file
View File

@@ -0,0 +1,5 @@
html,
body {
padding: 0;
margin: 0;
}

View File

@@ -0,0 +1,391 @@
<template>
<div class="car-entry-exit-log">
<el-card class="mb-4">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-blue-400">车辆出入营区记录</h2>
</div>
<!-- 搜索栏 -->
<div class="search-bar mb-4 p-3 bg-blue-900/20 rounded">
<el-row :gutter="20">
<el-col :span="8">
<el-input
v-model="searchForm.carId"
placeholder="车牌号"
prefix-icon="el-icon-search"
size="small"
/>
</el-col>
<el-col :span="8">
<el-select
v-model="searchForm.action"
placeholder="方向"
size="small"
>
<el-option
v-for="item in actionOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-col>
<el-col :span="8" class="text-right">
<el-button type="primary" size="small" @click="handleSearch">搜索</el-button>
<el-button size="small" @click="handleReset">重置</el-button>
</el-col>
</el-row>
</div>
<!-- 出入营区记录表格 -->
<el-table
:data="logData"
style="width: 100%"
stripe
:row-key="row => row.Id"
>
<el-table-column
v-for="column in tableColumns"
:key="column.field"
:prop="column.field"
:label="column.title"
:width="column.width"
:align="column.align"
v-if="!column.hidden"
>
<template v-if="column.field === 'Action'" slot-scope="scope">
<el-tag :type="getActionType(scope.row.Action)">{{ getActionLabel(scope.row.Action) }}</el-tag>
</template>
<template v-else-if="column.field === 'CreateDate'" slot-scope="scope">
{{ formatDateTime(scope.row.CreateDate) }}
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="mt-4 flex justify-end">
<el-pagination
layout="prev, pager, next, jumper, sizes, total"
:total="total"
:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</el-card>
<!-- 新增记录弹窗 -->
<el-dialog
title="新增出入营区记录"
:visible.sync="dialogVisible"
width="450px"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<el-form
:model="formData"
:rules="rules"
ref="formRef"
label-width="100px"
class="mt-4"
>
<el-form-item label="车牌号" prop="CarId">
<el-input v-model="formData.CarId" placeholder="请输入车牌号" />
</el-form-item>
<el-form-item label="方向" prop="Action">
<el-select v-model="formData.Action" placeholder="请选择方向">
<el-option
v-for="item in actionOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="时间" prop="CreateDate">
<el-date-picker
v-model="formData.CreateDate"
type="datetime"
placeholder="请选择时间"
style="width: 100%"
/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</div>
</el-dialog>
<!-- 操作按钮 -->
<div class="fixed bottom-4 right-4">
<el-button type="primary" icon="el-icon-plus" circle size="medium" @click="handleAdd" />
</div>
</div>
</template>
<script>
export default {
name: 'CarEntryExitLog',
data() {
return {
// 表格数据
logData: [],
total: 0,
pageSize: 10,
currentPage: 1,
// 搜索表单
searchForm: {
carId: '',
action: ''
},
// 新增记录对话框
dialogVisible: false,
formData: {
Id: '',
CarId: '',
Action: '',
CreateDate: ''
},
// 验证规则
rules: {
CarId: [
{ required: true, message: '请输入车牌号', trigger: 'blur' }
],
Action: [
{ required: true, message: '请选择方向', trigger: 'change' }
],
CreateDate: [
{ required: true, message: '请选择时间', trigger: 'change' }
]
},
// 出入方向选项
actionOptions: [
{ label: '出', value: '出' },
{ label: '入', value: '入' }
]
}
},
computed: {
tableColumns() {
return [
{field:'Id',title:'车辆记录ID',type:'int',width:110,hidden:true,require:true,align:'left'},
{field:'CarId',title:'车牌号',type:'string',link:true,width:150,align:'left'},
{field:'Action',title:'方向(出/入)',type:'string',bind:{ key:'出入方向',data:[]},width:150,align:'left'},
{field:'CreateDate',title:'时间',type:'datetime',width:180,align:'left'}
]
}
},
mounted() {
this.loadData()
},
methods: {
// 加载数据
loadData() {
// 模拟数据加载
this.logData = [
{
Id: 1,
CarId: '京A12345',
Action: '出',
CreateDate: '2024-01-20 08:30:00'
},
{
Id: 2,
CarId: '京B54321',
Action: '出',
CreateDate: '2024-01-20 09:15:00'
},
{
Id: 3,
CarId: '京A12345',
Action: '入',
CreateDate: '2024-01-20 17:45:00'
},
{
Id: 4,
CarId: '京C67890',
Action: '入',
CreateDate: '2024-01-20 18:20:00'
},
{
Id: 5,
CarId: '京B54321',
Action: '入',
CreateDate: '2024-01-20 19:00:00'
},
{
Id: 6,
CarId: '京D11223',
Action: '入',
CreateDate: '2024-01-21 08:00:00'
},
{
Id: 7,
CarId: '京D11223',
Action: '出',
CreateDate: '2024-01-21 16:30:00'
}
]
this.total = this.logData.length
},
// 搜索
handleSearch() {
// 模拟搜索逻辑
console.log('搜索条件:', this.searchForm)
this.loadData()
},
// 重置
handleReset() {
this.searchForm = {
carId: '',
action: ''
}
this.loadData()
},
// 新增
handleAdd() {
this.formData = {
Id: '',
CarId: '',
Action: '',
CreateDate: new Date()
}
this.dialogVisible = true
},
// 提交表单
handleSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
// 模拟提交操作
console.log('提交数据:', this.formData)
this.dialogVisible = false
this.loadData()
this.$message.success('记录添加成功')
}
})
},
// 分页处理
handleCurrentChange(val) {
this.currentPage = val
this.loadData()
},
handleSizeChange(val) {
this.pageSize = val
this.loadData()
},
// 格式化日期时间
formatDateTime(dateTime) {
if (!dateTime) return ''
const date = new Date(dateTime)
return date.toLocaleString('zh-CN')
},
// 获取动作标签
getActionLabel(value) {
const item = this.actionOptions.find(opt => opt.value === value)
return item ? item.label : value
},
// 获取动作类型(用于标签颜色)
getActionType(value) {
return value === '出' ? 'warning' : 'success'
}
}
}
</script>
<style scoped>
.car-entry-exit-log {
padding: 20px;
}
.search-bar {
margin-bottom: 20px;
}
/* 表格背景透明 */
:deep(.el-table) {
background-color: transparent !important;
}
:deep(.el-table__body-wrapper) {
background-color: transparent !important;
}
:deep(.el-table__row) {
background-color: transparent !important;
}
/* 表头透明 */
:deep(.el-table__header) {
background: transparent !important;
}
:deep(.el-table__header) th {
background: transparent !important;
background-color: transparent !important;
color: #68c9ff !important;
border-color: rgba(104, 201, 255, 0.3) !important;
}
/* 确保表头在各种状态下都保持透明 */
:deep(.el-table th.is-leaf) {
background: transparent !important;
background-color: transparent !important;
}
/* 表格边框 */
:deep(.el-table__row td) {
border-color: rgba(104, 201, 255, 0.2) !important;
color: #91d5ff !important;
}
/* 表格悬停效果 */
:deep(.el-table__row:hover td) {
background-color: rgba(104, 201, 255, 0.1) !important;
}
:deep(.el-dialog) {
background-color: rgba(0, 58, 97, 0.4) !important;
backdrop-filter: blur(8px);
border: 1px solid rgba(104, 201, 255, 0.3);
}
:deep(.el-dialog__header) {
background-color: rgba(0, 58, 97, 0.6) !important;
border-bottom: 1px solid rgba(104, 201, 255, 0.3);
}
:deep(.el-dialog__title) {
color: #91d5ff !important;
font-weight: 600;
}
:deep(.el-dialog__footer) {
background-color: rgba(0, 58, 97, 0.6) !important;
border-top: 1px solid rgba(104, 201, 255, 0.3);
padding-bottom: 20px;
}
:deep(.el-form-label) {
color: #68c9ff !important;
font-weight: 500;
}
</style>

View File

@@ -0,0 +1,742 @@
<template>
<div class="car-manager">
<el-card class="mb-4">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-blue-400">车辆信息管理</h2>
<el-button type="primary" @click="handleAdd" icon="el-icon-plus">新增车辆</el-button>
</div>
<!-- 搜索栏 -->
<div class="search-bar mb-4 p-3 bg-blue-900/20 rounded">
<el-row :gutter="20">
<el-col :span="6">
<el-input
v-model="searchForm.carNumber"
placeholder="车牌号"
prefix-icon="el-icon-search"
size="small"
/>
</el-col>
<el-col :span="6">
<el-select
v-model="searchForm.carType"
placeholder="车型"
size="small"
>
<el-option
v-for="item in carTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-col>
<el-col :span="6">
<el-select
v-model="searchForm.status"
placeholder="车辆状态"
size="small"
>
<el-option
v-for="item in carStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-col>
<el-col :span="6" class="text-right">
<el-button type="primary" size="small" @click="handleSearch">搜索</el-button>
<el-button size="small" @click="handleReset">重置</el-button>
</el-col>
</el-row>
</div>
<!-- 车辆信息表格 -->
<el-table
:data="carData"
style="width: 100%"
stripe
:row-key="row => row.CarId"
>
<el-table-column
v-for="column in tableColumns"
:key="column.field"
:prop="column.field"
:label="column.title"
:width="column.width"
:align="column.align"
:sortable="column.sort"
:show-overflow-tooltip="true"
>
<template v-if="column.bind && column.bind.key === '车辆类型'" slot-scope="scope">
<el-tag>{{ getCarTypeLabel(scope.row.CarType) }}</el-tag>
</template>
<template v-else-if="column.bind && column.bind.key === '车辆状态'" slot-scope="scope">
<el-tag>{{ getStatusLabel(scope.row.Status) }}</el-tag>
</template>
<template v-else-if="column.field === 'BuyTime'" slot-scope="scope">
{{ formatDateTime(scope.row.BuyTime) }}
</template>
</el-table-column>
<!-- 操作列 -->
<el-table-column label="操作" width="180" fixed="right">
<template slot-scope="scope">
<el-button size="small" type="primary" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
<el-button size="small" type="success" @click="handleApply(scope.row)">申请</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="mt-4 flex justify-end">
<el-pagination
layout="prev, pager, next, jumper, sizes, total"
:total="total"
:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</el-card>
<!-- 新增/编辑车辆弹窗 -->
<el-dialog
:title="dialogTitle"
:visible.sync="dialogVisible"
width="600px"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<el-form
:model="formData"
:rules="rules"
ref="formRef"
label-width="120px"
class="mt-4"
>
<el-form-item label="车牌号" prop="CarNumber">
<el-input v-model="formData.CarNumber" placeholder="请输入车牌号" />
</el-form-item>
<el-form-item label="车型" prop="CarType">
<el-select v-model="formData.CarType" placeholder="请选择车型">
<el-option
v-for="item in carTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="购买时间" prop="BuyTime">
<el-date-picker
v-model="formData.BuyTime"
type="datetime"
placeholder="请选择购买时间"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="使用年限" prop="ServiceLife">
<el-input v-model.number="formData.ServiceLife" placeholder="请输入使用年限" type="number" />
</el-form-item>
<el-form-item label="保养周期" prop="ServiceInterval">
<el-input v-model="formData.ServiceInterval" placeholder="请输入保养周期" />
</el-form-item>
<el-form-item label="车辆状态" prop="Status">
<el-select v-model="formData.Status" placeholder="请选择车辆状态">
<el-option
v-for="item in carStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="formData.Memo" placeholder="请输入备注信息" type="textarea" rows="3" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</div>
</el-dialog>
<!-- 车辆申请弹窗 -->
<el-dialog
title="车辆申请"
:visible.sync="applyDialogVisible"
width="500px"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<el-form
:model="applyForm"
:rules="applyRules"
ref="applyFormRef"
label-width="100px"
class="mt-4"
>
<el-form-item label="申请车辆">
<el-input v-model="applyForm.carNumber" disabled placeholder="车牌号" />
</el-form-item>
<el-form-item label="申请人" prop="applicant">
<el-input v-model="applyForm.applicant" placeholder="申请人" />
</el-form-item>
<el-form-item label="申请事由" prop="reason">
<el-input v-model="applyForm.reason" placeholder="请输入申请事由" type="textarea" rows="4" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="applyDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleApplySubmit">提交申请</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'CarManager',
data() {
return {
// 表格数据
carData: [],
total: 0,
pageSize: 10,
currentPage: 1,
// 搜索表单
searchForm: {
carNumber: '',
carType: '',
status: ''
},
// 新增/编辑对话框
dialogVisible: false,
dialogTitle: '',
formData: {
CarId: '',
CarNumber: '',
CarType: '',
BuyTime: '',
ServiceLife: '',
ServiceInterval: '',
Status: '',
Memo: ''
},
// 申请对话框
applyDialogVisible: false,
applyForm: {
carId: '',
carNumber: '',
applicant: '',
reason: ''
},
// 验证规则
rules: {
CarNumber: [
{ required: true, message: '请输入车牌号', trigger: 'blur' }
],
CarType: [
{ required: true, message: '请选择车型', trigger: 'change' }
],
BuyTime: [
{ required: true, message: '请选择购买时间', trigger: 'change' }
],
ServiceLife: [
{ required: true, message: '请输入使用年限', trigger: 'blur' },
{ type: 'number', message: '请输入有效的数字', trigger: 'blur' }
],
ServiceInterval: [
{ required: true, message: '请输入保养周期', trigger: 'blur' }
],
Status: [
{ required: true, message: '请选择车辆状态', trigger: 'change' }
]
},
applyRules: {
applicant: [
{ required: true, message: '请输入申请人', trigger: 'blur' }
],
reason: [
{ required: true, message: '请输入申请事由', trigger: 'blur' }
]
},
// 车型选项
carTypeOptions: [
{ label: '轿车', value: '轿车' },
{ label: 'SUV', value: 'SUV' },
{ label: '货车', value: '货车' },
{ label: '客车', value: '客车' },
{ label: '其他', value: '其他' }
],
// 车辆状态选项
carStatusOptions: [
{ label: '正常', value: 'normal' },
{ label: '维修中', value: 'repairing' },
{ label: '已报废', value: 'scrapped' },
{ label: '已借出', value: 'borrowed' }
]
}
},
computed: {
tableColumns() {
return [
{field:'CarId',title:'车辆ID',type:'int',width:110,hidden:true,require:true,align:'left'},
{field:'CarNumber',title:'车牌号',type:'string',link:true,sort:true,width:150,align:'left'},
{field:'CarType',title:'车型',type:'string',bind:{ key:'车辆类型',data:[]},width:150,align:'left'},
{field:'BuyTime',title:'购买时间',type:'datetime',width:180,align:'left'},
{field:'ServiceLife',title:'使用年限(年)',type:'int',width:130,align:'left'},
{field:'ServiceInterval',title:'保养周期',type:'string',width:150,align:'left'},
{field:'Status',title:'车辆状态',type:'string',bind:{ key:'车辆状态',data:[]},width:120,align:'left'},
{field:'Memo',title:'备注',type:'string',width:200,align:'left'}
]
}
},
mounted() {
this.loadData()
this.loadCurrentUser()
},
methods: {
// 加载数据
loadData() {
// 模拟数据加载
this.carData = [
{
CarId: 1,
CarNumber: '京A12345',
CarType: '轿车',
BuyTime: '2022-01-15 10:30:00',
ServiceLife: 5,
ServiceInterval: '每3个月',
Status: 'normal',
Memo: '公司行政用车'
},
{
CarId: 2,
CarNumber: '京B54321',
CarType: 'SUV',
BuyTime: '2023-03-20 14:20:00',
ServiceLife: 3,
ServiceInterval: '每6个月',
Status: 'repairing',
Memo: '项目组用车'
},
{
CarId: 3,
CarNumber: '京C67890',
CarType: '货车',
BuyTime: '2021-06-10 09:15:00',
ServiceLife: 4,
ServiceInterval: '每2个月',
Status: 'normal',
Memo: '物资运输用车'
}
]
this.total = this.carData.length
},
// 加载当前登录用户
loadCurrentUser() {
// 模拟获取当前登录用户
// 实际项目中应该从用户存储或接口获取
this.applyForm.applicant = '张三' // 默认申请人
},
// 搜索
handleSearch() {
// 模拟搜索逻辑
console.log('搜索条件:', this.searchForm)
this.loadData()
},
// 重置
handleReset() {
this.searchForm = {
carNumber: '',
carType: '',
status: ''
}
this.loadData()
},
// 新增
handleAdd() {
this.dialogTitle = '新增车辆'
this.formData = {
CarId: '',
CarNumber: '',
CarType: '',
BuyTime: '',
ServiceLife: '',
ServiceInterval: '',
Status: '',
Memo: ''
}
this.dialogVisible = true
},
// 编辑
handleEdit(row) {
this.dialogTitle = '编辑车辆'
this.formData = { ...row }
// 格式化日期时间
if (row.BuyTime) {
this.formData.BuyTime = new Date(row.BuyTime)
}
this.dialogVisible = true
},
// 删除
handleDelete(row) {
this.$confirm(`确定要删除车辆【${row.CarNumber}】吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 模拟删除操作
console.log('删除车辆:', row)
this.loadData()
this.$message.success('删除成功')
}).catch(() => {
this.$message.info('已取消删除')
})
},
// 申请
handleApply(row) {
this.applyForm = {
carId: row.CarId,
carNumber: row.CarNumber,
applicant: this.applyForm.applicant, // 保持当前用户
reason: ''
}
this.applyDialogVisible = true
},
// 提交表单
handleSubmit() {
// 确保表单引用存在
if (!this.$refs.formRef) {
console.warn('表单引用未找到')
return
}
this.$refs.formRef.validate((valid) => {
if (valid) {
// 模拟提交操作
console.log('提交数据:', this.formData)
this.dialogVisible = false
this.loadData()
// 模拟消息提示
alert('操作成功')
}
})
},
// 提交申请
handleApplySubmit() {
// 确保表单引用存在
if (!this.$refs.applyFormRef) {
console.warn('申请表单引用未找到')
return
}
this.$refs.applyFormRef.validate((valid) => {
if (valid) {
// 模拟申请提交操作
console.log('申请数据:', this.applyForm)
this.applyDialogVisible = false
// 模拟消息提示
alert('申请已提交')
}
})
},
// 分页处理
handleCurrentChange(val) {
this.currentPage = val
this.loadData()
},
handleSizeChange(val) {
this.pageSize = val
this.loadData()
},
// 格式化日期时间
formatDateTime(dateTime) {
if (!dateTime) return ''
const date = new Date(dateTime)
return date.toLocaleString('zh-CN')
},
// 获取车型标签
getCarTypeLabel(value) {
const item = this.carTypeOptions.find(opt => opt.value === value)
return item ? item.label : value
},
// 获取状态标签
getStatusLabel(value) {
const item = this.carStatusOptions.find(opt => opt.value === value)
return item ? item.label : value
},
// 获取状态类型(用于标签颜色)
getStatusType(value) {
const statusMap = {
'normal': 'success',
'repairing': 'warning',
'scrapped': 'danger',
'borrowed': 'info'
}
return statusMap[value] || 'info'
}
}
}
</script>
<style scoped>
.car-manager {
padding: 20px;
color: #fff;
}
.search-bar {
margin-bottom: 20px;
}
:deep(.el-card){
background: transparent !important;
border: 1px solid rgba(104, 201, 255, 0.3);
}
/* 表格背景透明 */
:deep(.el-table) {
background-color: transparent !important;
border: 1px solid rgba(104, 201, 255, 0.3);
}
:deep(.el-table tr){
background-color: transparent !important;
}
:deep(.el-table__body-wrapper) {
background-color: transparent !important;
}
:deep(.el-table__row) {
background-color: transparent !important;
}
/* 表头透明 */
:deep(.el-table__header) {
background: transparent !important;
}
:deep(.el-table__header) th {
background: transparent !important;
background-color: transparent !important;
color: #68c9ff !important;
border-color: rgba(104, 201, 255, 0.3) !important;
font-weight: 500;
}
/* 确保表头在各种状态下都保持透明 */
:deep(.el-table th.is-leaf) {
background: transparent !important;
background-color: transparent !important;
}
/* 表格边框 */
:deep(.el-table__row td) {
border-color: rgba(104, 201, 255, 0.2) !important;
color: #91d5ff !important;
}
/* 表格悬停效果 */
:deep(.el-table__row:hover td) {
background-color: rgba(104, 201, 255, 0.1) !important;
}
/* 操作按钮样式 */
:deep(.el-button) {
color: #fff !important;
border-color: rgba(104, 201, 255, 0.5) !important;
background-color: rgba(0, 89, 158, 0.3) !important;
}
:deep(.el-button:hover) {
background-color: rgba(0, 89, 158, 0.5) !important;
border-color: #68c9ff !important;
}
:deep(.el-button--primary) {
background-color: rgba(104, 201, 255, 0.3) !important;
border-color: #68c9ff !important;
}
:deep(.el-button--primary:hover) {
background-color: rgba(104, 201, 255, 0.5) !important;
}
:deep(.el-button--danger) {
background-color: rgba(245, 108, 108, 0.3) !important;
border-color: #f56c6c !important;
}
:deep(.el-button--danger:hover) {
background-color: rgba(245, 108, 108, 0.5) !important;
}
:deep(.el-button--success) {
background-color: rgba(103, 194, 58, 0.3) !important;
border-color: #67c23a !important;
}
:deep(.el-button--success:hover) {
background-color: rgba(103, 194, 58, 0.5) !important;
}
/* 对话框样式增强 */
:deep(.el-dialog) {
background-color: rgba(0, 58, 97, 0.95) !important; /* 增加不透明度,确保可见 */
backdrop-filter: blur(8px);
border: 1px solid rgba(104, 201, 255, 0.3);
z-index: 9999 !important; /* 使用更高的z-index确保显示在最上层 */
}
:deep(.el-dialog__header) {
background-color: rgba(0, 58, 97, 0.8) !important;
border-bottom: 1px solid rgba(104, 201, 255, 0.3);
padding-top: 15px;
padding-bottom: 15px;
}
:deep(.el-dialog__title) {
color: #91d5ff !important;
font-weight: 600;
font-size: 16px;
}
:deep(.el-dialog__footer) {
background-color: rgba(0, 58, 97, 0.8) !important;
border-top: 1px solid rgba(104, 201, 255, 0.3);
padding-top: 15px;
padding-bottom: 15px;
}
/* 表单样式 */
:deep(.el-form-label) {
color: #68c9ff !important;
font-weight: 500;
}
/* 输入框样式 */
:deep(.el-input__inner) {
background-color: rgba(0, 58, 97, 0.5) !important;
border-color: rgba(104, 201, 255, 0.3) !important;
color: #91d5ff !important;
}
:deep(.el-input__inner:focus) {
border-color: #68c9ff !important;
box-shadow: 0 0 0 2px rgba(104, 201, 255, 0.2) !important;
}
/* 选择框样式 */
:deep(.el-select .el-input__inner) {
background-color: rgba(0, 58, 97, 0.5) !important;
border-color: rgba(104, 201, 255, 0.3) !important;
color: #91d5ff !important;
}
:deep(.el-select-dropdown) {
background-color: rgba(0, 58, 97, 0.95) !important;
border-color: rgba(104, 201, 255, 0.3) !important;
}
:deep(.el-select-dropdown__item) {
color: #91d5ff !important;
}
:deep(.el-select-dropdown__item.hover) {
background-color: rgba(104, 201, 255, 0.1) !important;
}
/* 日期选择器样式 */
:deep(.el-date-editor .el-input__inner) {
background-color: rgba(0, 58, 97, 0.5) !important;
border-color: rgba(104, 201, 255, 0.3) !important;
color: #91d5ff !important;
}
/* 标签样式 */
:deep(.el-tag) {
background-color: rgba(0, 89, 158, 0.3) !important;
color: #91d5ff !important;
border-color: rgba(104, 201, 255, 0.3) !important;
}
/* 表格行样式确保内容可见 */
:deep(.el-table .cell) {
color: #91d5ff !important;
font-size: 14px;
}
/* 分页组件样式 */
:deep(.el-pagination button) {
background-color: rgba(0, 58, 97, 0.5) !important;
border-color: rgba(104, 201, 255, 0.3) !important;
color: #91d5ff !important;
}
:deep(.el-pagination__sizes .el-input__inner) {
background-color: rgba(0, 58, 97, 0.5) !important;
border-color: rgba(104, 201, 255, 0.3) !important;
color: #91d5ff !important;
}
:deep(.el-pagination__total) {
color: #91d5ff !important;
}
:deep(.el-pagination .el-pager li) {
color: #91d5ff !important;
}
:deep(.el-pagination .el-pager li.active) {
background-color: rgba(104, 201, 255, 0.3) !important;
color: #fff !important;
}
/* 确认对话框样式 */
:deep(.el-message-box) {
background-color: rgba(0, 58, 97, 0.95) !important;
border-color: rgba(104, 201, 255, 0.3) !important;
}
:deep(.el-message-box__title) {
color: #91d5ff !important;
}
:deep(.el-message-box__content) {
color: #91d5ff !important;
}
</style>

462
src/views/Car/CarUseLog.vue Normal file
View File

@@ -0,0 +1,462 @@
<template>
<div class="car-use-log">
<el-card class="mb-4">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-blue-400">车辆出入库记录</h2>
</div>
<!-- 搜索栏 -->
<div class="search-bar mb-4 p-3 bg-blue-900/20 rounded">
<el-row :gutter="20">
<el-col :span="6">
<el-select
v-model="searchForm.carId"
placeholder="选择车辆"
size="small"
>
<el-option
v-for="item in carListOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-col>
<el-col :span="6">
<el-select
v-model="searchForm.action"
placeholder="动作类型"
size="small"
>
<el-option
v-for="item in actionOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-col>
<el-col :span="6">
<el-select
v-model="searchForm.actionUser"
placeholder="动作执行人员"
size="small"
>
<el-option
v-for="item in userListOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-col>
<el-col :span="6" class="text-right">
<el-button type="primary" size="small" @click="handleSearch">搜索</el-button>
<el-button size="small" @click="handleReset">重置</el-button>
</el-col>
</el-row>
</div>
<!-- 出入库记录表格 -->
<el-table
:data="logData"
style="width: 100%"
stripe
:row-key="row => row.Id"
>
<el-table-column
v-for="column in tableColumns"
:key="column.field"
:prop="column.field"
:label="column.title"
:width="column.width"
:align="column.align"
v-if="!column.hidden"
>
<template v-if="column.field === 'CarId'" slot-scope="scope">
<el-tag>{{ getCarLabel(scope.row.CarId) }}</el-tag>
</template>
<template v-else-if="column.field === 'Action'" slot-scope="scope">
<el-tag :type="getActionType(scope.row.Action)">{{ getActionLabel(scope.row.Action) }}</el-tag>
</template>
<template v-else-if="column.field === 'ActionUser'" slot-scope="scope">
<el-tag>{{ getUserLabel(scope.row.ActionUser) }}</el-tag>
</template>
<template v-else-if="column.field === 'CreateDate'" slot-scope="scope">
{{ formatDateTime(scope.row.CreateDate) }}
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="mt-4 flex justify-end">
<el-pagination
layout="prev, pager, next, jumper, sizes, total"
:total="total"
:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</el-card>
<!-- 新增记录弹窗 -->
<el-dialog
title="新增出入库记录"
:visible.sync="dialogVisible"
width="500px"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<el-form
:model="formData"
:rules="rules"
ref="formRef"
label-width="120px"
class="mt-4"
>
<el-form-item label="车辆" prop="CarId">
<el-select v-model="formData.CarId" placeholder="请选择车辆">
<el-option
v-for="item in carListOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="动作" prop="Action">
<el-select v-model="formData.Action" placeholder="请选择动作类型">
<el-option
v-for="item in actionOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="动作执行人员" prop="ActionUser">
<el-select v-model="formData.ActionUser" placeholder="请选择执行人员">
<el-option
v-for="item in userListOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="动作时间" prop="CreateDate">
<el-date-picker
v-model="formData.CreateDate"
type="datetime"
placeholder="请选择动作时间"
style="width: 100%"
/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</div>
</el-dialog>
<!-- 操作按钮 -->
<div class="fixed bottom-4 right-4">
<el-button type="primary" icon="el-icon-plus" circle size="medium" @click="handleAdd" />
</div>
</div>
</template>
<script>
export default {
name: 'CarUseLog',
data() {
return {
// 表格数据
logData: [],
total: 0,
pageSize: 10,
currentPage: 1,
// 搜索表单
searchForm: {
carId: '',
action: '',
actionUser: ''
},
// 新增记录对话框
dialogVisible: false,
formData: {
Id: '',
CarId: '',
Action: '',
ActionUser: '',
CreateDate: ''
},
// 验证规则
rules: {
CarId: [
{ required: true, message: '请选择车辆', trigger: 'change' }
],
Action: [
{ required: true, message: '请选择动作类型', trigger: 'change' }
],
ActionUser: [
{ required: true, message: '请选择执行人员', trigger: 'change' }
],
CreateDate: [
{ required: true, message: '请选择动作时间', trigger: 'change' }
]
},
// 车辆列表选项
carListOptions: [
{ label: '京A12345', value: 1 },
{ label: '京B54321', value: 2 },
{ label: '京C67890', value: 3 }
],
// 动作类型选项
actionOptions: [
{ label: '出库', value: '出库' },
{ label: '入库', value: '入库' }
],
// 人员列表选项
userListOptions: [
{ label: '张三', value: '张三' },
{ label: '李四', value: '李四' },
{ label: '王五', value: '王五' },
{ label: '赵六', value: '赵六' }
]
}
},
computed: {
tableColumns() {
return [
{field:'Id',title:'车辆记录ID',type:'int',width:110,hidden:true,require:true,align:'left'},
{field:'CarId',title:'车辆',type:'int',bind:{ key:'车辆列表',data:[]},link:true,width:150,align:'left'},
{field:'Action',title:'动作(出库/入库)',type:'string',bind:{ key:'车辆出入状态',data:[]},width:150,align:'left'},
{field:'ActionUser',title:'动作执行人员',type:'string',bind:{ key:'人员列表',data:[]},width:150,align:'left'},
{field:'CreateDate',title:'动作时间',type:'datetime',width:180,align:'left'}
]
}
},
mounted() {
this.loadData()
},
methods: {
// 加载数据
loadData() {
// 模拟数据加载
this.logData = [
{
Id: 1,
CarId: 1,
Action: '出库',
ActionUser: '张三',
CreateDate: '2024-01-20 09:30:00'
},
{
Id: 2,
CarId: 1,
Action: '入库',
ActionUser: '张三',
CreateDate: '2024-01-20 18:45:00'
},
{
Id: 3,
CarId: 2,
Action: '出库',
ActionUser: '李四',
CreateDate: '2024-01-21 10:15:00'
},
{
Id: 4,
CarId: 3,
Action: '出库',
ActionUser: '王五',
CreateDate: '2024-01-21 14:20:00'
},
{
Id: 5,
CarId: 2,
Action: '入库',
ActionUser: '李四',
CreateDate: '2024-01-21 19:30:00'
}
]
this.total = this.logData.length
},
// 搜索
handleSearch() {
// 模拟搜索逻辑
console.log('搜索条件:', this.searchForm)
this.loadData()
},
// 重置
handleReset() {
this.searchForm = {
carId: '',
action: '',
actionUser: ''
}
this.loadData()
},
// 新增
handleAdd() {
this.formData = {
Id: '',
CarId: '',
Action: '',
ActionUser: '',
CreateDate: new Date()
}
this.dialogVisible = true
},
// 提交表单
handleSubmit() {
this.$refs.formRef.validate((valid) => {
if (valid) {
// 模拟提交操作
console.log('提交数据:', this.formData)
this.dialogVisible = false
this.loadData()
this.$message.success('记录添加成功')
}
})
},
// 分页处理
handleCurrentChange(val) {
this.currentPage = val
this.loadData()
},
handleSizeChange(val) {
this.pageSize = val
this.loadData()
},
// 格式化日期时间
formatDateTime(dateTime) {
if (!dateTime) return ''
const date = new Date(dateTime)
return date.toLocaleString('zh-CN')
},
// 获取车辆标签
getCarLabel(value) {
const item = this.carListOptions.find(opt => opt.value === value)
return item ? item.label : value
},
// 获取动作标签
getActionLabel(value) {
const item = this.actionOptions.find(opt => opt.value === value)
return item ? item.label : value
},
// 获取动作类型(用于标签颜色)
getActionType(value) {
return value === '出库' ? 'warning' : 'success'
},
// 获取用户标签
getUserLabel(value) {
const item = this.userListOptions.find(opt => opt.value === value)
return item ? item.label : value
}
}
}
</script>
<style scoped>
.car-use-log {
padding: 20px;
}
.search-bar {
margin-bottom: 20px;
}
/* 表格背景透明 */
:deep(.el-table) {
background-color: transparent !important;
}
:deep(.el-table__body-wrapper) {
background-color: transparent !important;
}
:deep(.el-table__row) {
background-color: transparent !important;
}
/* 表头透明 */
:deep(.el-table__header) {
background: transparent !important;
}
:deep(.el-table__header) th {
background: transparent !important;
background-color: transparent !important;
color: #68c9ff !important;
border-color: rgba(104, 201, 255, 0.3) !important;
}
/* 确保表头在各种状态下都保持透明 */
:deep(.el-table th.is-leaf) {
background: transparent !important;
background-color: transparent !important;
}
/* 表格边框 */
:deep(.el-table__row td) {
border-color: rgba(104, 201, 255, 0.2) !important;
color: #91d5ff !important;
}
/* 表格悬停效果 */
:deep(.el-table__row:hover td) {
background-color: rgba(104, 201, 255, 0.1) !important;
}
:deep(.el-dialog) {
background-color: rgba(0, 58, 97, 0.4) !important;
backdrop-filter: blur(8px);
border: 1px solid rgba(104, 201, 255, 0.3);
}
:deep(.el-dialog__header) {
background-color: rgba(0, 58, 97, 0.6) !important;
border-bottom: 1px solid rgba(104, 201, 255, 0.3);
}
:deep(.el-dialog__title) {
color: #91d5ff !important;
font-weight: 600;
}
:deep(.el-dialog__footer) {
background-color: rgba(0, 58, 97, 0.6) !important;
border-top: 1px solid rgba(104, 201, 255, 0.3);
padding-bottom: 20px;
}
:deep(.el-form-label) {
color: #68c9ff !important;
font-weight: 500;
}
</style>

234
src/views/Key/KeyApply.vue Normal file
View File

@@ -0,0 +1,234 @@
<template>
<!-- 钥匙使用申请组件模板 -->
<div class="p-4">
<h2 class="text-xl font-bold text-blue-400 mb-4">钥匙使用申请</h2>
<!-- 钥匙信息展示区域 -->
<el-form :model="formData" label-width="120px" class="mb-6">
<el-form-item label="槽位信息">
<el-input v-model="formData.keySlot" disabled></el-input>
</el-form-item>
<el-form-item label="钥匙名称">
<el-input v-model="formData.keyName" disabled></el-input>
</el-form-item>
<el-form-item label="当前状态">
<el-input v-model="formData.keyStatus" disabled></el-input>
</el-form-item>
<el-form-item label="申请人">
<el-input v-model="formData.applicant" disabled></el-input>
</el-form-item>
<el-form-item label="申请事由" required>
<el-input
v-model="formData.reason"
type="textarea"
:rows="4"
placeholder="请详细填写钥匙使用申请的具体事由..."
></el-input>
</el-form-item>
</el-form>
<!-- 操作按钮区域 -->
<div class="flex justify-end gap-4">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit">提交申请</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
/**
* 钥匙使用申请组件
* 用于用户提交钥匙使用申请,显示钥匙信息并收集申请事由
*/
import { ref, onMounted, reactive, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
// 路由相关
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
// 计算当前登录用户名称
const currentUserName = computed(() => {
if (userStore.userInfo && userStore.userInfo.name) {
return userStore.userInfo.name
}
return '管理员' // 默认用户名
})
// 表单数据
const formData = reactive({
keySlot: '', // 槽位信息
keyName: '', // 钥匙名称
keyStatus: '', // 当前状态
applicant: '', // 申请人,将从用户状态中获取
reason: '' // 申请事由
})
/**
* 组件挂载时初始化数据
* 从路由参数中获取钥匙信息(如果有)
*/
onMounted(() => {
// 设置当前登录用户为申请人
formData.applicant = currentUserName.value
console.log('当前登录用户:', formData.applicant)
// 从路由查询参数中获取钥匙信息
console.log('路由查询参数:', route.query)
const keyDataParam = route.query.keyData as string
console.log('从路由查询参数获取的钥匙数据:', keyDataParam)
if (keyDataParam) {
try {
const keyData = JSON.parse(keyDataParam)
// 直接使用提取好的钥匙信息
formData.keySlot = keyData.keySlot || ''
formData.keyName = keyData.keyName || ''
formData.keyStatus = keyData.keyStatus || ''
// 如果直接获取失败仍然保留从keyDatas中提取的逻辑作为后备
if (!formData.keyName || !formData.keyStatus) {
if (keyData.keyDatas && Array.isArray(keyData.keyDatas)) {
// 提取钥匙名称
if (!formData.keyName) {
const nameData = keyData.keyDatas.find((item: any) => item.infotype === '钥匙名称:')
if (nameData && nameData.status && nameData.status.length > 0) {
formData.keyName = nameData.status[0].value
}
}
// 提取钥匙状态
if (!formData.keyStatus) {
const statusData = keyData.keyDatas.find((item: any) => item.infotype === '钥匙状态:')
if (statusData && statusData.status && statusData.status.length > 0) {
formData.keyStatus = statusData.status[0].value
}
}
}
}
console.log('表单数据初始化完成:', formData)
} catch (error) {
console.error('解析钥匙数据失败:', error)
ElMessage.error('获取钥匙信息失败,请重试')
}
}
})
/**
* 提交申请处理函数
* 验证表单并提交数据
*/
const handleSubmit = () => {
// 验证申请事由是否填写
if (!formData.reason.trim()) {
ElMessage.warning('请填写申请事由')
return
}
// 模拟提交申请
console.log('提交申请数据:', formData)
// 显示成功提示
ElMessage.success('钥匙使用申请已提交成功')
// 跳转回钥匙管理页面
router.push('/Key/KeyManager')
}
/**
* 取消申请处理函数
*/
const handleCancel = () => {
// 返回上一页或跳转回钥匙管理页面
const fromParam = route.params.from as string
if (fromParam === 'keyManager') {
router.push('/Key/KeyManager')
} else {
router.back()
}
}
</script>
<style scoped>
/**
* 钥匙使用申请组件样式
* 保持与系统整体风格一致,使用空军蓝配色和透明背景
*/
/* 设置页面背景为透明 */
.p-4 {
background-color: transparent !important;
}
/* 表单样式 */
:deep(.el-form) {
background: rgba(12, 43, 77, 0.7) !important;
padding: 20px;
border-radius: 8px;
border: 1px solid #68c9ff;
backdrop-filter: blur(5px);
}
/* 表单标签样式 */
:deep(.el-form-item__label) {
color: #68c9ff !important;
font-weight: 500;
}
/* 输入框样式 */
:deep(.el-input__wrapper) {
background: rgba(255, 255, 255, 0.05) !important;
border-color: #68c9ff !important;
}
:deep(.el-input__inner) {
color: #b6dfff !important;
background: transparent !important;
}
:deep(.el-input.is-disabled .el-input__inner) {
color: #88b6d9 !important;
background: rgba(255, 255, 255, 0.03) !important;
}
/* 标题样式 */
.text-xl {
font-size: 20px;
}
.font-bold {
font-weight: 600;
}
.text-blue-400 {
color: #68c9ff;
}
.mb-4 {
margin-bottom: 16px;
}
.mb-6 {
margin-bottom: 24px;
}
/* 按钮组样式 */
.flex {
display: flex;
}
.justify-end {
justify-content: flex-end;
}
.gap-4 {
gap: 16px;
}
</style>

View File

@@ -34,7 +34,7 @@
<template #footer>
<div class="card-footer">
<el-button-group>
<el-button size="small" type="primary" :icon="Pointer">申请</el-button>
<el-button size="small" type="primary" :icon="Pointer" @click="handleApply(k)">申请</el-button>
<el-button size="small" type="success" :icon="Unlock">解锁</el-button>
<el-button size="small" type="danger" :icon="Lock">锁定</el-button>
</el-button-group>
@@ -52,7 +52,12 @@
* 负责钥匙柜的选择、钥匙状态的展示和操作
*/
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { Pointer, Unlock, Lock } from '@element-plus/icons-vue' // 导入操作按钮图标
import { ElMessage } from 'element-plus'
// 路由相关
const router = useRouter()
/**
* 状态定义
@@ -201,6 +206,52 @@ const keyList: KeyDatas[] =
]
}
]
/**
* 处理申请按钮点击事件
* @param keyData 钥匙数据
*/
const handleApply = (keyData: any) => {
// 检查钥匙状态是否允许申请
const statusData = keyData.keyDatas.find((item: any) => item.infotype === '钥匙状态:')
if (statusData && statusData.status.length > 0) {
const statusValue = statusData.status[0].value
if (statusValue.includes('离位')) {
ElMessage.warning('该钥匙当前不在位,无法申请')
return
}
}
// 从keyData中提取所需的钥匙信息
const extractedKeyInfo = {
keySlot: keyData.keySlot || '',
keyName: '',
keyStatus: '',
keyDatas: keyData.keyDatas
}
// 提取钥匙名称
const nameData = keyData.keyDatas.find((item: any) => item.infotype === '钥匙名称:')
if (nameData && nameData.status && nameData.status.length > 0) {
extractedKeyInfo.keyName = nameData.status[0].value
}
// 提取钥匙状态
if (statusData && statusData.status && statusData.status.length > 0) {
extractedKeyInfo.keyStatus = statusData.status[0].value
}
console.log('提取的钥匙信息:', extractedKeyInfo)
// 跳转到钥匙使用申请页面,并传递提取的钥匙数据
router.push({
path: '/Key/KeyApply',
query: {
keyData: JSON.stringify(extractedKeyInfo),
from: 'keyManager'
}
})
}
</script>
<style scoped>

378
src/views/Key/KeyRecord.vue Normal file
View File

@@ -0,0 +1,378 @@
<template>
<!-- 钥匙取用记录组件模板 -->
<div class="p-4">
<h2 class="text-xl font-bold text-blue-400 mb-4">钥匙取用记录</h2>
<!-- 筛选条件区域 -->
<div class="filter-container mb-4">
<el-form :inline="true" :model="searchForm" class="mb-4">
<el-form-item label="钥匙柜:">
<el-select v-model="searchForm.cabinetId" placeholder="请选择钥匙柜" style="width: 180px">
<el-option v-for="item in cabinetOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="钥匙名称:">
<el-input v-model="searchForm.keyName" placeholder="请输入钥匙名称" style="width: 180px" />
</el-form-item>
<el-form-item label="申请人:">
<el-input v-model="searchForm.applicant" placeholder="请输入申请人" style="width: 180px" />
</el-form-item>
<el-form-item label="日期范围:">
<el-date-picker
v-model="searchForm.dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 280px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 记录表格区域 -->
<el-table :data="recordList" style="width: 100%" border>
<el-table-column prop="id" label="记录ID" width="80" />
<el-table-column prop="cabinetName" label="钥匙柜" width="150" />
<el-table-column prop="keySlot" label="槽位" width="100" />
<el-table-column prop="keyName" label="钥匙名称" width="180" />
<el-table-column prop="keyType" label="钥匙类型" width="120" />
<el-table-column prop="applicant" label="申请人" width="120" />
<el-table-column prop="applyTime" label="申请时间" width="180" />
<el-table-column prop="applyReason" label="申请事由" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">{{ scope.row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="operation" label="操作人" width="120" />
<el-table-column prop="operationTime" label="操作时间" width="180" />
</el-table>
<!-- 分页组件 -->
<div class="pagination-container mt-4">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script lang="ts" setup>
/**
* 钥匙取用记录组件
* 用于展示和查询钥匙的使用申请和取用记录
*/
import { ref, reactive, onMounted } from 'vue'
// 搜索表单数据
const searchForm = reactive({
cabinetId: '', // 钥匙柜ID
keyName: '', // 钥匙名称
applicant: '', // 申请人
dateRange: null // 日期范围
})
// 钥匙柜选项列表
const cabinetOptions = ref([
{ value: '1', label: '指挥中心钥匙柜1#' },
{ value: '2', label: '指挥中心钥匙柜2#' }
])
// 记录列表数据
const recordList = ref([])
// 分页信息
const pagination = reactive({
currentPage: 1, // 当前页码
pageSize: 10, // 每页条数
total: 0 // 总记录数
})
/**
* 获取状态对应的标签类型
* @param status 状态字符串
* @returns 标签类型
*/
const getStatusType = (status: string): string => {
const statusMap: Record<string, string> = {
'申请中': 'info',
'已批准': 'primary',
'已领取': 'success',
'已归还': 'warning',
'已拒绝': 'danger'
}
return statusMap[status] || 'default'
}
/**
* 模拟获取记录数据
*/
const fetchRecords = () => {
// 这里应该是实际的API调用现在使用模拟数据
const mockData = [
{
id: '1',
cabinetName: '指挥中心钥匙柜1#',
keySlot: '1号槽位',
keyName: '苏A54321',
keyType: '车辆钥匙',
applicant: '值班员1',
applyTime: '2025-10-10 09:30:00',
applyReason: '外出执行任务需要用车',
status: '已归还',
operation: '管理员1',
operationTime: '2025-10-10 11:30:00'
},
{
id: '2',
cabinetName: '指挥中心钥匙柜1#',
keySlot: '3号槽位',
keyName: '1#仓库1号门1号钥匙',
keyType: '仓库钥匙',
applicant: '值班员1',
applyTime: '2025-10-12 10:00:00',
applyReason: '需要进入仓库领取物资',
status: '已领取',
operation: '管理员1',
operationTime: '2025-10-12 10:05:00'
},
{
id: '3',
cabinetName: '指挥中心钥匙柜2#',
keySlot: '2号槽位',
keyName: '苏A123456',
keyType: '车辆钥匙',
applicant: '值班员2',
applyTime: '2025-10-12 14:20:00',
applyReason: '外出巡逻需要用车',
status: '申请中',
operation: '',
operationTime: ''
},
{
id: '4',
cabinetName: '指挥中心钥匙柜1#',
keySlot: '3号槽位',
keyName: '1#仓库1号门2号钥匙',
keyType: '仓库钥匙',
applicant: '值班员1',
applyTime: '2025-10-12 09:15:00',
applyReason: '需要进入仓库盘点物资',
status: '已批准',
operation: '管理员1',
operationTime: '2025-10-12 09:20:00'
}
]
// 根据搜索条件过滤数据
let filteredData = [...mockData]
if (searchForm.cabinetId) {
filteredData = filteredData.filter(item => {
const cabinetMap: Record<string, string> = {
'1': '指挥中心钥匙柜1#',
'2': '指挥中心钥匙柜2#'
}
return item.cabinetName === cabinetMap[searchForm.cabinetId]
})
}
if (searchForm.keyName) {
filteredData = filteredData.filter(item =>
item.keyName.includes(searchForm.keyName)
)
}
if (searchForm.applicant) {
filteredData = filteredData.filter(item =>
item.applicant.includes(searchForm.applicant)
)
}
// 更新分页信息
pagination.total = filteredData.length
// 根据分页参数截取数据
const start = (pagination.currentPage - 1) * pagination.pageSize
const end = start + pagination.pageSize
recordList.value = filteredData.slice(start, end)
}
/**
* 查询按钮点击事件处理
*/
const handleSearch = () => {
pagination.currentPage = 1
fetchRecords()
}
/**
* 重置按钮点击事件处理
*/
const handleReset = () => {
// 重置搜索表单
Object.assign(searchForm, {
cabinetId: '',
keyName: '',
applicant: '',
dateRange: null
})
// 重置分页并重新获取数据
pagination.currentPage = 1
fetchRecords()
}
/**
* 每页条数变化处理
* @param size 新的每页条数
*/
const handleSizeChange = (size: number) => {
pagination.pageSize = size
fetchRecords()
}
/**
* 页码变化处理
* @param current 新的页码
*/
const handleCurrentChange = (current: number) => {
pagination.currentPage = current
fetchRecords()
}
/**
* 组件挂载时初始化数据
*/
onMounted(() => {
fetchRecords()
})
</script>
<style scoped>
/**
* 钥匙取用记录组件样式
* 保持与系统整体风格一致,使用空军蓝配色和透明背景
*/
/* 设置页面背景为透明 */
.p-4 {
background-color: transparent !important;
}
/* 筛选容器样式 */
.filter-container {
background: rgba(12, 43, 77, 0.7) !important;
padding: 16px;
border-radius: 8px;
border: 1px solid #68c9ff;
backdrop-filter: blur(5px);
}
/* 表格样式 */
:deep(.el-table) {
background: transparent !important;
border: 1px solid #68c9ff;
backdrop-filter: blur(5px);
}
:deep(.el-table__header th) {
background: rgba(12, 43, 77, 0.7) !important;
border-bottom: 1px solid #68c9ff !important;
color: #68c9ff !important;
font-weight: 500;
}
:deep(.el-table__row) {
color: #b6dfff !important;
border-bottom: 1px solid rgba(104, 201, 255, 0.3) !important;
}
:deep(.el-table__row:hover > td) {
background: rgba(104, 201, 255, 0.1) !important;
}
:deep(.el-table__row.el-table__row--striped > td) {
background: rgba(255, 255, 255, 0.02) !important;
}
/* 分页组件样式 */
.pagination-container {
background: rgba(12, 43, 77, 0.7) !important;
padding: 16px;
border-radius: 8px;
border: 1px solid #68c9ff;
backdrop-filter: blur(5px);
}
:deep(.el-pagination.is-background .el-pager li) {
background: rgba(255, 255, 255, 0.05);
color: #b6dfff;
border: 1px solid #68c9ff;
}
:deep(.el-pagination.is-background .el-pager li:hover:not(.disabled)) {
background: rgba(104, 201, 255, 0.2);
color: #68c9ff;
}
:deep(.el-pagination.is-background .el-pager li.active) {
background: #68c9ff;
color: #0c2b4d;
border-color: #68c9ff;
}
/* 输入框和下拉框样式 */
:deep(.el-input__wrapper) {
background: rgba(255, 255, 255, 0.05) !important;
border-color: #68c9ff !important;
}
:deep(.el-input__inner) {
color: #b6dfff !important;
background: transparent !important;
}
:deep(.el-select) {
background: rgba(255, 255, 255, 0.05) !important;
}
:deep(.el-date-editor) {
background: rgba(255, 255, 255, 0.05) !important;
}
/* 标题样式 */
.text-xl {
font-size: 20px;
}
.font-bold {
font-weight: 600;
}
.text-blue-400 {
color: #68c9ff;
}
.mb-4 {
margin-bottom: 16px;
}
.mt-4 {
margin-top: 16px;
}
</style>

View File

@@ -179,27 +179,25 @@ const menuList = ref([
icon: 'Key',
children: [
{ id: '2-1', name: '钥匙信息管理', path: '/Key/KeyManager' },
{ id: '2-2', name: '钥匙使用申请', path: '/warehouse/add' },
{ id: '2-3', name: '钥匙取用记录', path: '/warehouse/stats' }
{ id: '2-2', name: '钥匙取用记录', path: '/Key/KeyRecord' }
]
},
{ id: '3',
name: '巡检管理',
icon: 'View',
children: [
{ id: '3-1', name: '巡检路径管理', path: '/inventory/overview' },
{ id: '3-2', name: '巡检排班管理', path: '/inventory/detail' },
{ id: '3-3', name: '巡检记录', path: '/inventory/alerts' }
{ id: '3-1', name: '巡检路径管理', path: '/Path/PathManager' },
{ id: '3-2', name: '巡检排班管理', path: '/Path/PathSchedule' },
{ id: '3-3', name: '巡检记录', path: '/Path/PathLog' }
]
},
{ id: '4',
name: '车辆管理',
icon: 'Van',
children: [
{ id: '4-1', name: '车辆信息管理', path: '/inventory/overview' },
{ id: '4-2', name: '车辆使用申请', path: '/inventory/detail' },
{ id: '4-3', name: '车辆出入记录', path: '/inventory/alerts' },
{ id: '4-4', name: '车辆出入营区记录', path: '/inventory/alerts' }
{ id: '4-1', name: '车辆信息管理', path: '/Car/CarManager' },
{ id: '4-2', name: '车辆出入库记录', path: '/Car/CarUseLog' },
{ id: '4-3', name: '车辆出入营区记录', path: '/Car/CarEntryExitLog' }
]
},
{ id: '5',

View File

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

View File

@@ -0,0 +1,525 @@
<template>
<!-- 巡检路径编辑组件 -->
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑巡检路径' : '新增巡检路径'"
width="90%"
max-width="1200px"
destroy-on-close
@closed="handleClose"
>
<!-- 表单容器 -->
<el-form
ref="pathFormRef"
:model="pathForm"
label-width="100px"
:rules="formRules"
>
<!-- 基本信息区域 -->
<div class="mb-6">
<h3 class="text-lg font-medium text-blue-300 mb-4">基本信息</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<el-form-item label="路径名称" prop="name">
<el-input
v-model="pathForm.name"
placeholder="请输入路径名称"
maxlength="50"
show-word-limit
/>
</el-form-item>
</div>
<el-form-item label="路径说明" prop="description">
<el-input
v-model="pathForm.description"
type="textarea"
rows="3"
placeholder="请输入路径说明"
maxlength="200"
show-word-limit
/>
</el-form-item>
</div>
<!-- 地图展示区域 -->
<div class="mb-6">
<h3 class="text-lg font-medium text-blue-300 mb-4">路径地图</h3>
<div class="border border-blue-400 rounded-lg overflow-hidden relative">
<!-- 地图容器设置固定高度 -->
<div class="path-map-container">
<Map />
</div>
<!-- 地图操作提示 -->
<div class="map-tip">
<el-tooltip content="地图已加载,选择路径点后将显示路径连线" placement="top">
<el-button size="small" type="primary" plain>
<el-icon><HelpFilled /></el-icon> 地图使用提示
</el-button>
</el-tooltip>
</div>
</div>
</div>
<!-- 路径点表格区域 -->
<div>
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium text-blue-300">路径点配置</h3>
<el-button type="primary" @click="handleAddPathPoint" icon="Plus">新增路径点</el-button>
</div>
<el-table
v-loading="loading"
:data="pathForm.points"
style="width: 100%"
@sort-change="handleSortChange"
>
<el-table-column prop="index" label="序号" width="80" align="center" sortable="custom" />
<el-table-column prop="deviceName" label="路径点设备" min-width="200" align="center">
<template #default="scope">
<el-select
v-model="scope.row.deviceName"
placeholder="请选择设备"
style="width: 100%"
>
<el-option
v-for="device in deviceList"
:key="device.value"
:label="device.label"
:value="device.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center">
<template #default="scope">
<div class="flex gap-2 justify-center">
<el-button
size="small"
@click="handleMoveUp(scope.row, scope.$index)"
:disabled="scope.$index === 0"
icon="ArrowUp"
>上移</el-button>
<el-button
size="small"
@click="handleMoveDown(scope.row, scope.$index)"
:disabled="scope.$index === pathForm.points.length - 1"
icon="ArrowDown"
>下移</el-button>
<el-button
size="small"
type="danger"
@click="handleDeletePoint(scope.$index)"
icon="Delete"
>删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
</el-form>
<!-- 对话框底部按钮 -->
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave">保存</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
/**
* 巡检路径编辑组件
* 用于新增和编辑巡检路径信息,包含地图展示和路径点配置
*/
import { ref, reactive, watch, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import Map from '../Map.vue' // 导入地图组件
// Props定义
const props = defineProps({
visible: {
type: Boolean,
default: false
},
pathData: {
type: Object,
default: null
}
})
// Emits定义
const emit = defineEmits(['update:visible', 'save'])
// 响应式数据
const dialogVisible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
})
const pathFormRef = ref()
const loading = ref(false)
// 表单数据
const pathForm = reactive({
id: '',
name: '',
description: '',
points: []
})
// 表单验证规则
const formRules = reactive({
name: [
{ required: true, message: '请输入路径名称', trigger: 'blur' },
{ min: 2, max: 50, message: '路径名称长度应在2-50个字符之间', trigger: 'blur' }
],
description: [
{ max: 200, message: '路径说明长度不能超过200个字符', trigger: 'blur' }
]
})
// 模拟设备列表
const deviceList = ref([
{ value: '入口门禁', label: '入口门禁' },
{ value: '出口门禁', label: '出口门禁' },
{ value: '1号仓库温度传感器', label: '1号仓库温度传感器' },
{ value: '1号仓库湿度传感器', label: '1号仓库湿度传感器' },
{ value: '2号仓库温度传感器', label: '2号仓库温度传感器' },
{ value: '2号仓库湿度传感器', label: '2号仓库湿度传感器' },
{ value: '消防设施检查点', label: '消防设施检查点' },
{ value: 'UPS电源', label: 'UPS电源' },
{ value: '服务器机架1', label: '服务器机架1' },
{ value: '服务器机架2', label: '服务器机架2' },
{ value: '空调系统', label: '空调系统' },
{ value: '网络设备柜', label: '网络设备柜' },
{ value: '机房环境监控', label: '机房环境监控' },
{ value: '西区大门', label: '西区大门' },
{ value: '安全监控室', label: '安全监控室' },
{ value: '应急出口', label: '应急出口' },
{ value: '出口监控摄像头', label: '出口监控摄像头' }
])
// 计算属性
const isEdit = computed(() => !!props.pathData)
/**
* 重置表单
*/
const resetForm = () => {
pathForm.id = ''
pathForm.name = ''
pathForm.description = ''
pathForm.points = []
if (pathFormRef.value) {
pathFormRef.value.resetFields()
}
}
// 监听props变化
watch(() => props.pathData, (newVal) => {
if (newVal) {
// 编辑模式:填充表单数据
pathForm.id = newVal.id || ''
pathForm.name = newVal.name || ''
pathForm.description = newVal.description || ''
pathForm.points = newVal.points ? JSON.parse(JSON.stringify(newVal.points)) : []
} else {
// 新增模式:重置表单
resetForm()
}
}, { immediate: true })
/**
* 处理对话框关闭
*/
const handleClose = () => {
resetForm()
}
/**
* 新增路径点
*/
const handleAddPathPoint = () => {
const newIndex = pathForm.points.length + 1
pathForm.points.push({
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
index: newIndex,
deviceName: ''
})
// 更新序号
updatePointIndexes()
}
/**
* 删除路径点
*/
const handleDeletePoint = (index) => {
ElMessageBox.confirm(
'确定要删除这个路径点吗?',
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
pathForm.points.splice(index, 1)
updatePointIndexes()
ElMessage.success('路径点删除成功')
}).catch(() => {
// 用户取消删除
})
}
/**
* 上移路径点
*/
const handleMoveUp = (row, index) => {
if (index > 0) {
// 交换位置
const temp = pathForm.points[index]
pathForm.points[index] = pathForm.points[index - 1]
pathForm.points[index - 1] = temp
// 更新序号
updatePointIndexes()
}
}
/**
* 下移路径点
*/
const handleMoveDown = (row, index) => {
if (index < pathForm.points.length - 1) {
// 交换位置
const temp = pathForm.points[index]
pathForm.points[index] = pathForm.points[index + 1]
pathForm.points[index + 1] = temp
// 更新序号
updatePointIndexes()
}
}
/**
* 更新路径点序号
*/
const updatePointIndexes = () => {
pathForm.points.forEach((point, index) => {
point.index = index + 1
})
}
/**
* 处理排序变化
*/
const handleSortChange = ({ column, prop, order }) => {
if (prop === 'index') {
// 按序号排序
pathForm.points.sort((a, b) => {
return order === 'ascending' ? a.index - b.index : b.index - a.index
})
// 更新序号
updatePointIndexes()
}
}
/**
* 验证路径点
*/
const validatePoints = () => {
// 检查是否有空设备名称
for (let i = 0; i < pathForm.points.length; i++) {
const point = pathForm.points[i]
if (!point.deviceName || point.deviceName.trim() === '') {
ElMessage.warning(`请为第${point.index}个路径点选择设备`)
return false
}
}
return true
}
/**
* 处理保存
*/
const handleSave = async () => {
// 验证表单
if (!pathFormRef.value) {
return
}
try {
await pathFormRef.value.validate()
// 验证路径点
if (!validatePoints()) {
return
}
loading.value = true
// 准备保存数据
const saveData = {
...pathForm,
pointCount: pathForm.points.length,
updateTime: new Date().toLocaleString('zh-CN')
}
if (!isEdit.value) {
saveData.createTime = saveData.updateTime
}
// 模拟API调用延迟
await new Promise(resolve => setTimeout(resolve, 500))
// 触发保存事件
emit('save', saveData)
ElMessage.success('保存成功')
dialogVisible.value = false
} catch (error) {
console.error('保存失败:', error)
ElMessage.error('保存失败,请重试')
} finally {
loading.value = false
}
}
</script>
<style scoped>
/**
* 巡检路径编辑组件样式
*/
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* 标题样式 */
.text-lg {
font-size: 18px;
}
.font-medium {
font-weight: 500;
}
.text-blue-300 {
color: #91d5ff;
}
.text-blue-200 {
color: #b6dfff;
}
/* 间距样式 */
.mb-4 {
margin-bottom: 16px;
}
.mb-6 {
margin-bottom: 24px;
}
/* 网格布局 */
.grid {
display: grid;
}
.grid-cols-1 {
grid-template-columns: 1fr;
}
.md\:grid-cols-2 {
grid-template-columns: 1fr 1fr;
}
.gap-4 {
gap: 16px;
}
/* 地图容器样式 */
.path-map-container {
height: 400px;
width: 100%;
position: relative;
overflow: hidden;
}
/* 确保Map组件在容器内正确显示 */
:deep(.path-map-container .wrapper) {
width: 100% !important;
height: 100% !important;
position: relative;
}
:deep(.path-map-container #mapContainer) {
width: 100% !important;
height: 100% !important;
position: relative;
}
/* 地图提示样式 */
.map-tip {
position: absolute;
top: 10px;
right: 10px;
z-index: 10;
}
/* 表单样式 */
:deep(.el-form) {
background: transparent !important;
}
:deep(.el-form-item__label) {
color: #68c9ff !important;
font-weight: 500;
}
/* 输入框样式 */
:deep(.el-input__wrapper) {
background: rgba(255, 255, 255, 0.05) !important;
border-color: #68c9ff !important;
}
:deep(.el-input__inner) {
color: #b6dfff !important;
background: transparent !important;
}
/* 表格样式 */
:deep(.el-table) {
background: rgba(12, 43, 77, 0.7) !important;
border: 1px solid #68c9ff;
backdrop-filter: blur(5px);
}
:deep(.el-table__header-wrapper th) {
background: rgba(20, 60, 110, 0.8) !important;
color: #68c9ff !important;
border-bottom: 1px solid #68c9ff !important;
}
:deep(.el-table__body-wrapper tr) {
background: transparent !important;
}
:deep(.el-table__body-wrapper tr:nth-child(even)) {
background: rgba(20, 60, 110, 0.3) !important;
}
:deep(.el-table__body-wrapper tr:hover) {
background: rgba(104, 201, 255, 0.2) !important;
}
:deep(.el-table__body-wrapper td) {
color: #b6dfff !important;
border-bottom: 1px solid rgba(104, 201, 255, 0.3) !important;
}
</style>

562
src/views/Path/PathLog.vue Normal file
View File

@@ -0,0 +1,562 @@
<template>
<div class="p-4">
<h2 class="text-xl font-bold text-blue-400 mb-4">巡检日志管理</h2>
<!-- 工具栏 -->
<div class="flex justify-between items-center mb-4">
<div class="flex gap-4">
<el-button type="primary" @click="handleAddLog" icon="Plus">新增日志</el-button>
<el-button @click="handleRefresh" icon="Refresh">刷新</el-button>
</div>
<!-- 搜索框 -->
<el-input
v-model="searchKeyword"
placeholder="搜索日志信息"
prefix-icon="Search"
style="width: 240px"
@input="handleSearch"
/>
</div>
<!-- 日志列表表格 -->
<el-table
v-loading="loading"
:data="logList"
style="width: 100%"
@row-click="handleRowClick"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="CreateDate" label="记录日期时间" width="200" align="center" />
<el-table-column prop="PersonNames" label="巡检人员" min-width="200" align="center">
<template #default="scope">
<div v-for="name in scope.row.PersonNames" :key="name" class="text-sm">
{{ name }}
</div>
</template>
</el-table-column>
<el-table-column prop="DeviceName" label="记录点位设备" min-width="200" />
<el-table-column prop="ScheduleTitle" label="对应排班记录" min-width="200" />
<el-table-column label="操作" width="150" align="center">
<template #default="scope">
<div class="flex gap-2 justify-center">
<el-button
size="small"
@click="handleEdit(scope.row)"
icon="Edit"
>编辑</el-button>
<el-button
size="small"
type="danger"
@click="handleDelete(scope.row)"
icon="Delete"
>删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="flex justify-end mt-4">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 日志编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑巡检日志' : '新增巡检日志'"
width="70%"
max-width="900px"
destroy-on-close
@closed="handleClose"
>
<!-- 表单容器 -->
<el-form
ref="logFormRef"
:model="logForm"
label-width="120px"
:rules="formRules"
>
<!-- 第一行人员选择和日期时间 -->
<div class="grid grid-cols-1 gap-4 mb-4">
<el-form-item label="人员1" prop="UserID1">
<el-select
v-model="logForm.UserID1"
placeholder="选择巡检人员"
style="width: 100%"
>
<el-option
v-for="user in userList"
:key="user.value"
:label="user.label"
:value="user.value"
/>
</el-select>
</el-form-item>
<el-form-item label="人员2" prop="UserID2">
<el-select
v-model="logForm.UserID2"
placeholder="选择巡检人员(可选)"
style="width: 100%"
>
<el-option
v-for="user in userList"
:key="user.value"
:label="user.label"
:value="user.value"
/>
</el-select>
</el-form-item>
<el-form-item label="人员3" prop="UserID3">
<el-select
v-model="logForm.UserID3"
placeholder="选择巡检人员(可选)"
style="width: 100%"
>
<el-option
v-for="user in userList"
:key="user.value"
:label="user.label"
:value="user.value"
/>
</el-select>
</el-form-item>
<el-form-item label="记录日期时间" prop="CreateDate">
<el-date-picker
v-model="logForm.CreateDate"
type="datetime"
placeholder="选择日期时间"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</div>
<!-- 第二行设备和排班记录 -->
<div class="grid grid-cols-1 gap-4">
<el-form-item label="记录点位设备" prop="PointDeviceID">
<el-select
v-model="logForm.PointDeviceID"
placeholder="选择点位设备"
style="width: 100%"
>
<el-option
v-for="device in deviceList"
:key="device.value"
:label="device.label"
:value="device.value"
/>
</el-select>
</el-form-item>
<el-form-item label="对应排班记录" prop="PatrolScheduleID">
<el-select
v-model="logForm.PatrolScheduleID"
placeholder="选择对应排班记录"
style="width: 100%"
>
<el-option
v-for="schedule in scheduleList"
:key="schedule.value"
:label="schedule.label"
:value="schedule.value"
/>
</el-select>
</el-form-item>
</div>
</el-form>
<!-- 对话框底部按钮 -->
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave">保存</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
/**
* 巡检日志管理组件
* 用于管理巡检日志信息,包含人员、时间、设备和排班记录配置
*/
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
// 响应式数据
const loading = ref(false)
const logList = ref([])
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
const searchKeyword = ref('')
// 编辑对话框相关
const dialogVisible = ref(false)
const isEdit = ref(false)
const logFormRef = ref()
const logForm = reactive({
UserID1: '',
UserID2: '',
UserID3: '',
CreateDate: '',
PointDeviceID: '',
PatrolScheduleID: ''
})
// 表单验证规则
const formRules = {
UserID1: [
{ required: true, message: '请选择第一位巡检人员', trigger: 'blur' }
],
CreateDate: [
{ required: true, message: '请选择记录日期时间', trigger: 'blur' }
],
PointDeviceID: [
{ required: true, message: '请选择记录点位设备', trigger: 'blur' }
],
PatrolScheduleID: [
{ required: true, message: '请选择对应排班记录', trigger: 'blur' }
]
}
// 人员列表、设备列表和排班记录列表(模拟数据)
const userList = ref([
{ label: '张三', value: '1' },
{ label: '李四', value: '2' },
{ label: '王五', value: '3' },
{ label: '赵六', value: '4' },
{ label: '钱七', value: '5' }
])
const deviceList = ref([
{ label: '生产线A-设备1', value: '1' },
{ label: '生产线A-设备2', value: '2' },
{ label: '生产线B-设备1', value: '3' },
{ label: '仓库-设备1', value: '4' },
{ label: '办公区-设备1', value: '5' },
{ label: '办公区-设备2', value: '6' }
])
const scheduleList = ref([
{ label: '周一生产线A例行巡检', value: '1' },
{ label: '周三仓库安全巡检', value: '2' },
{ label: '周五办公区环境巡检', value: '3' },
{ label: '周二生产线B例行巡检', value: '4' },
{ label: '周四全厂区安全巡检', value: '5' }
])
// 获取选中人员的姓名
const getPersonNames = (userIds: string[]) => {
return userIds
.filter(id => id)
.map(id => {
const user = userList.value.find(u => u.value === id)
return user ? user.label : ''
})
.filter(name => name)
}
// 初始化数据
const initData = () => {
loading.value = true
// 模拟数据获取
setTimeout(() => {
logList.value = [
{
id: 1,
UserID1: '1',
UserID2: '2',
UserID3: '',
CreateDate: '2024-04-17 09:30:00',
PointDeviceID: '1',
PatrolScheduleID: '1',
PersonNames: ['张三', '李四'],
DeviceName: '生产线A-设备1',
ScheduleTitle: '周一生产线A例行巡检'
},
{
id: 2,
UserID1: '2',
UserID2: '',
UserID3: '',
CreateDate: '2024-04-17 14:15:00',
PointDeviceID: '3',
PatrolScheduleID: '4',
PersonNames: ['李四'],
DeviceName: '生产线B-设备1',
ScheduleTitle: '周二生产线B例行巡检'
},
{
id: 3,
UserID1: '3',
UserID2: '4',
UserID3: '5',
CreateDate: '2024-04-17 16:45:00',
PointDeviceID: '5',
PatrolScheduleID: '3',
PersonNames: ['王五', '赵六', '钱七'],
DeviceName: '办公区-设备1',
ScheduleTitle: '周五办公区环境巡检'
}
]
total.value = logList.value.length
loading.value = false
}, 500)
}
// 刷新数据
const handleRefresh = () => {
currentPage.value = 1
initData()
}
// 搜索功能
const handleSearch = () => {
// 简单的前端搜索实现
const filtered = logList.value.filter(item => {
const searchStr = searchKeyword.value.toLowerCase()
return item.DeviceName.toLowerCase().includes(searchStr) ||
item.ScheduleTitle.toLowerCase().includes(searchStr) ||
item.PersonNames.some(name => name.toLowerCase().includes(searchStr)) ||
item.CreateDate.toLowerCase().includes(searchStr)
})
logList.value = filtered
}
// 分页处理
const handleSizeChange = (size: number) => {
pageSize.value = size
}
const handleCurrentChange = (current: number) => {
currentPage.value = current
}
// 行点击事件
const handleRowClick = (row: any) => {
// 可以添加行点击后的处理逻辑
}
// 选择框变化事件
const handleSelectionChange = (selection: any[]) => {
// 可以添加选择框变化后的处理逻辑
}
// 新增日志
const handleAddLog = () => {
isEdit.value = false
// 重置表单
logForm.UserID1 = ''
logForm.UserID2 = ''
logForm.UserID3 = ''
logForm.CreateDate = ''
logForm.PointDeviceID = ''
logForm.PatrolScheduleID = ''
// 打开对话框
dialogVisible.value = true
}
// 编辑日志
const handleEdit = (row: any) => {
isEdit.value = true
// 填充表单数据
logForm.UserID1 = row.UserID1 || ''
logForm.UserID2 = row.UserID2 || ''
logForm.UserID3 = row.UserID3 || ''
logForm.CreateDate = row.CreateDate || ''
logForm.PointDeviceID = row.PointDeviceID || ''
logForm.PatrolScheduleID = row.PatrolScheduleID || ''
// 打开对话框
dialogVisible.value = true
}
// 删除日志
const handleDelete = (row: any) => {
ElMessageBox.confirm(
`确定要删除ID为${row.id}的巡检日志吗?`,
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
// 模拟删除操作
const index = logList.value.findIndex(item => item.id === row.id)
if (index !== -1) {
logList.value.splice(index, 1)
total.value = logList.value.length
ElMessage.success('删除成功')
}
}).catch(() => {
// 取消删除
})
}
// 保存日志
const handleSave = async () => {
if (!logFormRef.value) return
try {
await logFormRef.value.validate()
// 模拟保存操作
const userIds = [logForm.UserID1, logForm.UserID2, logForm.UserID3]
const personNames = getPersonNames(userIds)
const device = deviceList.value.find(d => d.value === logForm.PointDeviceID)
const schedule = scheduleList.value.find(s => s.value === logForm.PatrolScheduleID)
const newLog = {
id: isEdit.value ? logList.value.find(item =>
item.UserID1 === logForm.UserID1 &&
item.CreateDate === logForm.CreateDate
)?.id : Date.now(),
UserID1: logForm.UserID1,
UserID2: logForm.UserID2,
UserID3: logForm.UserID3,
CreateDate: logForm.CreateDate,
PointDeviceID: logForm.PointDeviceID,
PatrolScheduleID: logForm.PatrolScheduleID,
PersonNames: personNames,
DeviceName: device ? device.label : '',
ScheduleTitle: schedule ? schedule.label : ''
}
if (isEdit.value) {
// 编辑模式
const index = logList.value.findIndex(item =>
item.UserID1 === logForm.UserID1 &&
item.CreateDate === logForm.CreateDate
)
if (index !== -1) {
logList.value[index] = newLog
}
} else {
// 新增模式
logList.value.unshift(newLog)
total.value = logList.value.length
}
ElMessage.success(isEdit.value ? '编辑成功' : '新增成功')
dialogVisible.value = false
} catch (error) {
// 表单验证失败
}
}
// 对话框关闭处理
const handleClose = () => {
if (logFormRef.value) {
logFormRef.value.resetFields()
}
}
// 组件挂载时初始化数据
onMounted(() => {
initData()
})
</script>
<style scoped>
/* 表单样式 */
:deep(.el-form) {
background: transparent !important;
}
:deep(.el-form-item__label) {
color: #68c9ff !important;
font-weight: 500;
}
/* 表格样式 - 设置为透明背景 */
:deep(.el-table) {
background: transparent !important;
}
:deep(.el-table tr){
background: transparent !important;
}
:deep(.el-table__header-wrapper) {
background: transparent !important;
}
:deep(.el-table__header) {
background: transparent !important;
}
:deep(.el-table__header) th {
background: transparent !important;
background-color: transparent !important;
color: #68c9ff !important;
border-color: rgba(104, 201, 255, 0.3) !important;
}
/* 确保表头在各种状态下都保持透明 */
:deep(.el-table th.is-leaf) {
background: transparent !important;
background-color: transparent !important;
}
:deep(.el-table__body-wrapper) {
background: transparent !important;
}
:deep(.el-table__row) {
background: transparent !important;
}
:deep(.el-table__row:nth-child(2n)) {
background: rgba(0, 58, 97, 0.1) !important;
}
:deep(.el-table__row:hover) {
background: rgba(0, 58, 97, 0.2) !important;
}
:deep(.el-table__body) td {
border-color: rgba(104, 201, 255, 0.2) !important;
color: #91d5ff !important;
}
/* 对话框样式 */
:deep(.el-dialog) {
background-color: rgba(0, 58, 97, 0.4) !important; /* 空军蓝透明度40% */
backdrop-filter: blur(8px); /* 高斯模糊效果 */
border: 1px solid rgba(104, 201, 255, 0.3);
}
:deep(.el-dialog__header) {
background-color: rgba(0, 58, 97, 0.6) !important;
border-bottom: 1px solid rgba(104, 201, 255, 0.3);
}
:deep(.el-dialog__title) {
color: #91d5ff !important;
font-weight: 600;
}
:deep(.el-dialog__footer) {
background-color: rgba(0, 58, 97, 0.6) !important;
border-top: 1px solid rgba(104, 201, 255, 0.3);
padding-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,390 @@
<template>
<!-- 巡检路径管理组件模板 -->
<div class="p-4">
<h2 class="text-xl font-bold text-blue-400 mb-4">巡检路径管理</h2>
<!-- 工具栏 -->
<div class="flex justify-between items-center mb-4">
<div class="flex gap-4">
<el-button type="primary" @click="handleAddPath" icon="Plus">新增路径</el-button>
<el-button @click="handleRefresh" icon="Refresh">刷新</el-button>
</div>
<!-- 搜索框 -->
<el-input
v-model="searchKeyword"
placeholder="搜索路径名称"
prefix-icon="Search"
style="width: 240px"
@input="handleSearch"
/>
</div>
<!-- 路径列表表格 -->
<el-table
v-loading="loading"
:data="pathList"
style="width: 100%"
@row-click="handleRowClick"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column prop="id" label="路径ID" width="100" align="center" />
<el-table-column prop="name" label="路径名称" min-width="180" align="center" />
<el-table-column prop="description" label="路径说明" min-width="250" align="center">
<template #default="scope">
<el-tooltip :content="scope.row.description" placement="top">
<span>{{ scope.row.description.length > 20 ? scope.row.description.substring(0, 20) + '...' : scope.row.description }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="pointCount" label="路径点数量" width="120" align="center" />
<el-table-column prop="createTime" label="创建时间" width="180" align="center" />
<el-table-column prop="updateTime" label="更新时间" width="180" align="center" />
<el-table-column label="操作" width="180" align="center" fixed="right">
<template #default="scope">
<el-button size="small" @click="handleEditPath(scope.row)" icon="Edit">编辑</el-button>
<el-button size="small" type="danger" @click="handleDeletePath(scope.row)" icon="Delete">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<div class="mt-4 flex justify-end">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 路径编辑对话框 -->
<PathEditor
v-model:visible="editorVisible"
:path-data="currentPath"
@save="handleSavePath"
/>
</div>
</template>
<script lang="ts" setup>
/**
* 巡检路径管理组件
* 用于管理和维护巡检路径信息
*/
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import PathEditor from './PathEditor.vue'
// 路由相关
const router = useRouter()
// 状态管理
const loading = ref(false)
const searchKeyword = ref('')
const editorVisible = ref(false)
const selectedPaths = ref([])
const currentPath = ref(null)
// 分页数据
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 路径列表数据
const pathList = ref([])
/**
* 初始化数据
*/
onMounted(() => {
fetchPathList()
})
/**
* 获取路径列表数据
*/
const fetchPathList = async () => {
loading.value = true
try {
// 模拟API请求
// 实际应用中应替换为真实的API调用
const mockData = generateMockPaths()
// 应用搜索过滤
let filteredData = mockData
if (searchKeyword.value) {
filteredData = mockData.filter(item =>
item.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
)
}
// 更新分页数据
pagination.total = filteredData.length
// 应用分页
const start = (pagination.currentPage - 1) * pagination.pageSize
const end = start + pagination.pageSize
pathList.value = filteredData.slice(start, end)
} catch (error) {
console.error('获取路径列表失败:', error)
ElMessage.error('获取路径列表失败,请重试')
pathList.value = []
} finally {
loading.value = false
}
}
/**
* 生成模拟数据
* 实际应用中应通过API获取
*/
const generateMockPaths = () => {
return [
{
id: '1',
name: '东区仓库巡检路径',
description: '覆盖东区所有仓库区域的标准巡检路径',
pointCount: 5,
points: [
{ id: '1-1', index: 1, deviceName: '入口门禁' },
{ id: '1-2', index: 2, deviceName: '1号仓库温度传感器' },
{ id: '1-3', index: 3, deviceName: '消防设施检查点' },
{ id: '1-4', index: 4, deviceName: '2号仓库湿度传感器' },
{ id: '1-5', index: 5, deviceName: '出口监控摄像头' }
],
createTime: '2024-04-01 10:30:00',
updateTime: '2024-04-10 14:20:00'
},
{
id: '2',
name: '西区安全巡检路线',
description: '西区安全检查专用路线',
pointCount: 3,
points: [
{ id: '2-1', index: 1, deviceName: '西区大门' },
{ id: '2-2', index: 2, deviceName: '安全监控室' },
{ id: '2-3', index: 3, deviceName: '应急出口' }
],
createTime: '2024-04-05 09:15:00',
updateTime: '2024-04-05 09:15:00'
},
{
id: '3',
name: '设备机房巡检路线',
description: '针对设备机房的详细巡检路线,包括所有关键设备',
pointCount: 7,
points: [
{ id: '3-1', index: 1, deviceName: 'UPS电源' },
{ id: '3-2', index: 2, deviceName: '服务器机架1' },
{ id: '3-3', index: 3, deviceName: '服务器机架2' },
{ id: '3-4', index: 4, deviceName: '空调系统' },
{ id: '3-5', index: 5, deviceName: '消防系统' },
{ id: '3-6', index: 6, deviceName: '网络设备柜' },
{ id: '3-7', index: 7, deviceName: '机房环境监控' }
],
createTime: '2024-04-08 16:45:00',
updateTime: '2024-04-12 11:30:00'
}
]
}
/**
* 处理添加路径
*/
const handleAddPath = () => {
currentPath.value = null
editorVisible.value = true
}
/**
* 处理编辑路径
*/
const handleEditPath = (path) => {
currentPath.value = JSON.parse(JSON.stringify(path)) // 深拷贝
editorVisible.value = true
}
/**
* 处理删除路径
*/
const handleDeletePath = async (path) => {
try {
await ElMessageBox.confirm(
`确定要删除路径「${path.name}」吗?`,
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
// 模拟删除操作
// 实际应用中应调用API删除
ElMessage.success('路径删除成功')
fetchPathList() // 重新获取列表
} catch (error) {
// 用户取消删除
}
}
/**
* 处理保存路径
*/
const handleSavePath = (pathData) => {
// 模拟保存操作
// 实际应用中应调用API保存
ElMessage.success('路径保存成功')
editorVisible.value = false
fetchPathList() // 重新获取列表
}
/**
* 处理表格行点击
*/
const handleRowClick = (row) => {
// 可以在这里处理点击行的逻辑,比如查看详情
}
/**
* 处理选择变化
*/
const handleSelectionChange = (selection) => {
selectedPaths.value = selection
}
/**
* 处理搜索
*/
const handleSearch = () => {
pagination.currentPage = 1 // 重置到第一页
fetchPathList()
}
/**
* 处理刷新
*/
const handleRefresh = () => {
searchKeyword.value = ''
pagination.currentPage = 1
fetchPathList()
}
/**
* 处理分页大小变化
*/
const handleSizeChange = (size) => {
pagination.pageSize = size
fetchPathList()
}
/**
* 处理页码变化
*/
const handleCurrentChange = (current) => {
pagination.currentPage = current
fetchPathList()
}
</script>
<style scoped>
/**
* 巡检路径管理组件样式
* 保持与系统整体风格一致,使用空军蓝配色和透明背景
*/
/* 设置页面背景为透明 */
.p-4 {
background-color: transparent !important;
}
/* 标题样式 */
.text-xl {
font-size: 20px;
}
.font-bold {
font-weight: 600;
}
.text-blue-400 {
color: #68c9ff;
}
.mb-4 {
margin-bottom: 16px;
}
/* 表格样式 */
:deep(.el-table) {
background: rgba(12, 43, 77, 0.7) !important;
border: 1px solid #68c9ff;
backdrop-filter: blur(5px);
}
:deep(.el-table__header-wrapper th) {
background: rgba(20, 60, 110, 0.8) !important;
color: #68c9ff !important;
border-bottom: 1px solid #68c9ff !important;
}
:deep(.el-table__body-wrapper tr) {
background: transparent !important;
}
:deep(.el-table__body-wrapper tr:nth-child(even)) {
background: rgba(20, 60, 110, 0.3) !important;
}
:deep(.el-table__body-wrapper tr:hover) {
background: rgba(104, 201, 255, 0.2) !important;
}
:deep(.el-table__body-wrapper td) {
color: #b6dfff !important;
border-bottom: 1px solid rgba(104, 201, 255, 0.3) !important;
}
/* 分页组件样式 */
:deep(.el-pagination) {
color: #68c9ff !important;
}
:deep(.el-pagination button) {
background: transparent !important;
border-color: #68c9ff !important;
color: #68c9ff !important;
}
:deep(.el-pagination__sizes .el-input .el-input__wrapper) {
background: transparent !important;
border-color: #68c9ff !important;
}
:deep(.el-pagination__sizes .el-input .el-input__inner) {
background: transparent !important;
color: #68c9ff !important;
}
:deep(.el-pagination__jump .el-input .el-input__wrapper) {
background: transparent !important;
border-color: #68c9ff !important;
}
:deep(.el-pagination__jump .el-input .el-input__inner) {
background: transparent !important;
color: #68c9ff !important;
}
:deep(.el-pager li.active) {
color: #68c9ff !important;
}
</style>

View File

@@ -0,0 +1,707 @@
<template>
<div class="p-4">
<h2 class="text-xl font-bold text-blue-400 mb-4">巡检计划管理</h2>
<!-- 工具栏 -->
<div class="flex justify-between items-center mb-4">
<div class="flex gap-4">
<el-button type="primary" @click="handleAddSchedule" icon="Plus">新增计划</el-button>
<el-button @click="handleRefresh" icon="Refresh">刷新</el-button>
</div>
<!-- 搜索框 -->
<el-input
v-model="searchKeyword"
placeholder="搜索计划标题"
prefix-icon="Search"
style="width: 240px"
@input="handleSearch"
/>
</div>
<!-- 计划列表表格 -->
<el-table
v-loading="loading"
:data="scheduleList"
style="width: 100%"
@row-click="handleRowClick"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="PatrolDay" label="巡检周期" width="100" align="center">
<template #default="scope">
{{ getDayOfWeek(scope.row.PatrolDay) }}
</template>
</el-table-column>
<el-table-column prop="PatrolTitle" label="计划标题" min-width="200" />
<el-table-column prop="PatrolStartTime" label="开始时间" width="120" align="center" />
<el-table-column prop="PatrolEndTime" label="结束时间" width="120" align="center" />
<el-table-column prop="PatrolPersons" label="巡检人员" min-width="200" align="center">
<template #default="scope">
<div v-for="person in scope.row.PatrolPersons" :key="person.id" class="text-sm">
{{ person.name }}
</div>
</template>
</el-table-column>
<el-table-column prop="PatrolPathName" label="巡检路线" min-width="180" />
<el-table-column label="操作" width="150" align="center">
<template #default="scope">
<div class="flex gap-2 justify-center">
<el-button
size="small"
@click="handleEdit(scope.row)"
icon="Edit"
>编辑</el-button>
<el-button
size="small"
type="danger"
@click="handleDelete(scope.row)"
icon="Delete"
>删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="flex justify-end mt-4">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 计划编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑巡检计划' : '新增巡检计划'"
width="70%"
max-width="900px"
destroy-on-close
@closed="handleClose"
>
<!-- 表单容器 -->
<el-form
ref="scheduleFormRef"
:model="scheduleForm"
label-width="120px"
:rules="formRules"
>
<!-- 第一行周期和标题 -->
<div class="grid grid-cols-1 gap-4 mb-4">
<el-form-item label="每周第几天" prop="PatrolDay">
<el-input-number
v-model="scheduleForm.PatrolDay"
:min="1"
:max="7"
placeholder="请输入1-7"
:step-strictly="true"
>
<template #append>
<el-select v-model="scheduleForm.PatrolDay" placeholder="选择星期">
<el-option label="周一" :value="1" />
<el-option label="周二" :value="2" />
<el-option label="周三" :value="3" />
<el-option label="周四" :value="4" />
<el-option label="周五" :value="5" />
<el-option label="周六" :value="6" />
<el-option label="周日" :value="7" />
</el-select>
</template>
</el-input-number>
</el-form-item>
<el-form-item label="标题" prop="PatrolTitle">
<el-input
v-model="scheduleForm.PatrolTitle"
placeholder="请输入计划标题"
maxlength="50"
show-word-limit
/>
</el-form-item>
</div>
<!-- 第二行开始时间和结束时间 -->
<div class="grid grid-cols-1 gap-4 mb-4">
<el-form-item label="巡检开始时间" prop="PatrolStartTime">
<el-time-picker
v-model="scheduleForm.PatrolStartTime"
type="time"
placeholder="选择开始时间"
value-format="HH:mm"
/>
</el-form-item>
<el-form-item label="巡检结束时间" prop="PatrolEndTime">
<el-time-picker
v-model="scheduleForm.PatrolEndTime"
type="time"
placeholder="选择结束时间"
value-format="HH:mm"
/>
</el-form-item>
</div>
<!-- 第三行第一位和第二位巡检人员 -->
<div class="grid grid-cols-1 gap-4 mb-4">
<el-form-item label="第一位巡检人员" prop="UserId1">
<el-select
v-model="scheduleForm.UserId1"
placeholder="选择巡检人员"
style="width: 100%"
>
<el-option
v-for="user in userList"
:key="user.value"
:label="user.label"
:value="user.value"
/>
</el-select>
</el-form-item>
<el-form-item label="第二位巡检人员" prop="UserId2">
<el-select
v-model="scheduleForm.UserId2"
placeholder="选择巡检人员"
style="width: 100%"
>
<el-option
v-for="user in userList"
:key="user.value"
:label="user.label"
:value="user.value"
/>
</el-select>
</el-form-item>
</div>
<!-- 第四行第三位巡检人员和巡检路线 -->
<div class="grid grid-cols-1 gap-4">
<el-form-item label="第三位巡检人员" prop="UserId3">
<el-select
v-model="scheduleForm.UserId3"
placeholder="选择巡检人员"
style="width: 100%"
>
<el-option
v-for="user in userList"
:key="user.value"
:label="user.label"
:value="user.value"
/>
</el-select>
</el-form-item>
<el-form-item label="巡检路线" prop="PatrolPathId">
<el-select
v-model="scheduleForm.PatrolPathId"
placeholder="选择巡检路线"
style="width: 100%"
>
<el-option
v-for="path in pathList"
:key="path.value"
:label="path.label"
:value="path.value"
/>
</el-select>
</el-form-item>
</div>
</el-form>
<!-- 对话框底部按钮 -->
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave">保存</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
/**
* 巡检计划管理组件
* 用于管理巡检计划信息,包含计划的周期、时间、人员和路线配置
*/
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
// 响应式数据
const loading = ref(false)
const scheduleList = ref([])
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
const searchKeyword = ref('')
// 编辑对话框相关
const dialogVisible = ref(false)
const isEdit = ref(false)
const scheduleFormRef = ref()
const scheduleForm = reactive({
PatrolDay: null,
PatrolTitle: '',
PatrolStartTime: '',
PatrolEndTime: '',
UserId1: '',
UserId2: '',
UserId3: '',
PatrolPathId: ''
})
// 表单验证规则
const formRules = {
PatrolDay: [
{ required: true, message: '请输入每周第几天', trigger: 'blur' },
{ type: 'number', min: 1, max: 7, message: '请输入1-7之间的数字', trigger: 'blur' }
],
PatrolTitle: [
{ required: true, message: '请输入计划标题', trigger: 'blur' }
],
PatrolStartTime: [
{ required: true, message: '请选择巡检开始时间', trigger: 'blur' }
],
PatrolEndTime: [
{ required: true, message: '请选择巡检结束时间', trigger: 'blur' }
],
UserId1: [
{ required: true, message: '请选择第一位巡检人员', trigger: 'blur' }
],
PatrolPathId: [
{ required: true, message: '请选择巡检路线', trigger: 'blur' }
]
}
// 人员列表和路线列表(模拟数据)
const userList = ref([
{ label: '张三', value: '1' },
{ label: '李四', value: '2' },
{ label: '王五', value: '3' },
{ label: '赵六', value: '4' },
{ label: '钱七', value: '5' }
])
const pathList = ref([
{ label: '生产线A巡检路线', value: '1' },
{ label: '生产线B巡检路线', value: '2' },
{ label: '仓库巡检路线', value: '3' },
{ label: '办公区巡检路线', value: '4' }
])
// 获取星期几的中文名称
const getDayOfWeek = (day: number): string => {
const days = ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日']
return days[day] || ''
}
// 初始化数据
const initData = () => {
loading.value = true
// 模拟数据获取
setTimeout(() => {
scheduleList.value = [
{
id: 1,
PatrolDay: 1,
PatrolTitle: '周一生产线A例行巡检',
PatrolStartTime: '09:00',
PatrolEndTime: '10:30',
UserId1: '1',
UserId2: '2',
UserId3: '3',
PatrolPathId: '1',
PatrolPersons: [
{ id: '1', name: '张三' },
{ id: '2', name: '李四' },
{ id: '3', name: '王五' }
],
PatrolPathName: '生产线A巡检路线'
},
{
id: 2,
PatrolDay: 3,
PatrolTitle: '周三仓库安全巡检',
PatrolStartTime: '14:00',
PatrolEndTime: '15:30',
UserId1: '2',
UserId2: '',
UserId3: '',
PatrolPathId: '3',
PatrolPersons: [
{ id: '2', name: '李四' }
],
PatrolPathName: '仓库巡检路线'
},
{
id: 3,
PatrolDay: 5,
PatrolTitle: '周五办公区环境巡检',
PatrolStartTime: '15:00',
PatrolEndTime: '16:00',
UserId1: '3',
UserId2: '4',
UserId3: '',
PatrolPathId: '4',
PatrolPersons: [
{ id: '3', name: '王五' },
{ id: '4', name: '赵六' }
],
PatrolPathName: '办公区巡检路线'
}
]
total.value = scheduleList.value.length
loading.value = false
}, 500)
}
// 处理新增计划
const handleAddSchedule = () => {
isEdit.value = false
// 重置表单
Object.keys(scheduleForm).forEach(key => {
scheduleForm[key] = ''
})
scheduleForm.PatrolDay = null
dialogVisible.value = true
}
// 处理编辑计划
const handleEdit = (row: any) => {
isEdit.value = true
// 填充表单数据
Object.keys(scheduleForm).forEach(key => {
if (key in row) {
scheduleForm[key] = row[key]
}
})
dialogVisible.value = true
}
// 处理删除计划
const handleDelete = async (row: any) => {
try {
await ElMessageBox.confirm(
`确定要删除计划「${row.PatrolTitle}」吗?`,
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
// 模拟删除操作
loading.value = true
setTimeout(() => {
const index = scheduleList.value.findIndex(item => item.id === row.id)
if (index > -1) {
scheduleList.value.splice(index, 1)
total.value = scheduleList.value.length
ElMessage.success('删除成功')
}
loading.value = false
}, 300)
} catch {
// 取消删除
}
}
// 保存计划
const handleSave = async () => {
if (!scheduleFormRef.value) return
try {
await scheduleFormRef.value.validate()
// 构建人员信息
const personIds = [scheduleForm.UserId1, scheduleForm.UserId2, scheduleForm.UserId3].filter(id => id)
const PatrolPersons = personIds.map(id => {
const user = userList.value.find(u => u.value === id)
return { id, name: user?.label || '' }
})
// 获取路线名称
const path = pathList.value.find(p => p.value === scheduleForm.PatrolPathId)
// 保存数据
loading.value = true
setTimeout(() => {
if (isEdit.value) {
// 编辑模式
const index = scheduleList.value.findIndex(item => item.id === scheduleForm.id)
if (index > -1) {
scheduleList.value[index] = {
...scheduleForm,
PatrolPersons,
PatrolPathName: path?.label || ''
}
}
} else {
// 新增模式
const newSchedule = {
id: Date.now(), // 模拟生成ID
...scheduleForm,
PatrolPersons,
PatrolPathName: path?.label || ''
}
scheduleList.value.unshift(newSchedule)
total.value = scheduleList.value.length
}
ElMessage.success(isEdit.value ? '修改成功' : '新增成功')
dialogVisible.value = false
loading.value = false
}, 300)
} catch (error) {
// 表单验证失败
}
}
// 处理搜索
const handleSearch = () => {
// 这里可以根据关键词过滤数据
// 目前简单处理,实际项目中可能需要重新请求数据
}
// 处理刷新
const handleRefresh = () => {
searchKeyword.value = ''
currentPage.value = 1
initData()
}
// 分页处理
const handleSizeChange = (size: number) => {
pageSize.value = size
}
const handleCurrentChange = (current: number) => {
currentPage.value = current
}
// 处理行点击
const handleRowClick = (row: any) => {
// 可以在这里实现行点击后的操作
}
// 处理选择变化
const handleSelectionChange = (selection: any[]) => {
// 可以在这里处理选中的数据
}
// 处理对话框关闭
const handleClose = () => {
if (scheduleFormRef.value) {
scheduleFormRef.value.resetFields()
}
}
// 组件挂载时初始化数据
onMounted(() => {
initData()
})
</script>
<style scoped>
/**
* 巡检计划管理组件样式
*/
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* 标题样式 */
.text-xl {
font-size: 20px;
}
.text-lg {
font-size: 18px;
}
.font-bold {
font-weight: 700;
}
.font-medium {
font-weight: 500;
}
.text-blue-400 {
color: #69b1ff;
}
.text-blue-300 {
color: #91d5ff;
}
.text-blue-200 {
color: #b6dfff;
}
/* 间距样式 */
.mb-4 {
margin-bottom: 16px;
}
.mb-6 {
margin-bottom: 24px;
}
.mt-4 {
margin-top: 16px;
}
/* 网格布局 */
.grid {
display: grid;
}
.grid-cols-1 {
grid-template-columns: 1fr;
}
.grid-cols-2 {
grid-template-columns: 1fr 1fr;
}
.gap-4 {
gap: 16px;
}
/* 对话框样式 */
:deep(.el-dialog) {
background-color: rgba(0, 58, 97, 0.4) !important; /* 空军蓝透明度40% */
backdrop-filter: blur(8px); /* 高斯模糊效果 */
border: 1px solid rgba(104, 201, 255, 0.3);
}
:deep(.el-dialog__header) {
background-color: rgba(0, 58, 97, 0.6) !important;
border-bottom: 1px solid rgba(104, 201, 255, 0.3);
}
:deep(.el-dialog__title) {
color: #91d5ff !important;
font-weight: 600;
}
:deep(.el-dialog__footer) {
background-color: rgba(0, 58, 97, 0.6) !important;
border-top: 1px solid rgba(104, 201, 255, 0.3);
padding-bottom: 20px;
}
/* 表单样式 */
:deep(.el-form) {
background: transparent !important;
}
:deep(.el-form-item__label) {
color: #68c9ff !important;
font-weight: 500;
}
/* 输入框样式 */
:deep(.el-input__wrapper) {
background: rgba(255, 255, 255, 0.05) !important;
border-color: #68c9ff !important;
}
:deep(.el-input__inner) {
color: #b6dfff !important;
background: transparent !important;
}
/* 表格样式 */
:deep(.el-table) {
background: rgba(12, 43, 77, 0.7) !important;
border: 1px solid #68c9ff;
backdrop-filter: blur(5px);
}
:deep(.el-table__header-wrapper th) {
background: rgba(20, 60, 110, 0.8) !important;
color: #68c9ff !important;
border-bottom: 1px solid #68c9ff !important;
}
:deep(.el-table__body-wrapper tr) {
background: transparent !important;
}
:deep(.el-table__body-wrapper tr:nth-child(even)) {
background: rgba(20, 60, 110, 0.3) !important;
}
:deep(.el-table__body-wrapper tr:hover) {
background: rgba(104, 201, 255, 0.2) !important;
}
:deep(.el-table__body-wrapper td) {
color: #b6dfff !important;
border-bottom: 1px solid rgba(104, 201, 255, 0.3) !important;
}
/* 选择器样式 */
:deep(.el-select) {
width: 100%;
}
:deep(.el-select__wrapper) {
background: rgba(255, 255, 255, 0.05) !important;
border-color: #68c9ff !important;
}
:deep(.el-select__placeholder) {
color: #68c9ff !important;
}
:deep(.el-select__input) {
color: #b6dfff !important;
}
:deep(.el-select-dropdown) {
background: rgba(12, 43, 77, 0.95) !important;
border: 1px solid #68c9ff !important;
}
:deep(.el-select-dropdown__item) {
color: #b6dfff !important;
}
:deep(.el-select-dropdown__item:hover) {
background: rgba(104, 201, 255, 0.2) !important;
}
/* 时间选择器样式 */
:deep(.el-time-picker) {
width: 100%;
}
:deep(.el-time-panel) {
background: rgba(12, 43, 77, 0.95) !important;
border: 1px solid #68c9ff !important;
}
:deep(.el-time-spinner__item) {
color: #b6dfff !important;
}
:deep(.el-time-spinner__item:hover:not(.disabled)) {
background: rgba(104, 201, 255, 0.2) !important;
}
:deep(.el-time-spinner__item.active:not(.disabled)) {
color: #68c9ff !important;
background: rgba(104, 201, 255, 0.3) !important;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 KiB