1
0
forked from dyf/dyf-vue-ui
Files
dyf-vue-ui/src/views/controlCenter/controlPanel/index.vue

565 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="p-2">
<el-row :gutter="20">
<!-- 部门树 -->
<el-col :lg="4" :xs="24" style="" class="main-tree">
<el-input v-model="deptName" placeholder="输入分组名称" prefix-icon="Search" clearable />
<el-tree ref="deptTreeRef" class="mt-2" node-key="id" :data="deptOptions"
:props="{ label: 'groupName', children: 'children' }" :expand-on-click-node="false"
:filter-node-method="filterNode" highlight-current default-expand-all @node-click="handleNodeClick"></el-tree>
</el-col>
<el-col :lg="20" :xs="24">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter"
:leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card>
<!-- =========搜索按钮操作======= -->
<div class="btn_search">
<el-button :type="isListView ? 'primary' : ''" @click="switchView('list')">
{{ isListView ? '列表显示' : '列表显示' }}
</el-button>
<el-button :type="!isListView ? 'primary' : ''" @click="switchView('map')">
{{ !isListView ? '地图显示' : '地图显示' }}
</el-button>
<el-button type="primary" plain @click="sendTextMessage">发送消息</el-button>
<el-button type="primary" plain>电子围栏</el-button>
<el-button type="danger" plain @click="forceAlarm" :loading="forceAlarmLoading"
:loading-text="forceAlarmLoading ? '报警中...' : '强制报警'"> {{
forceAlarmLoading ? '报警中' : '强制报警' }}</el-button>
<div style="position: absolute; right:30px; top:20px">
<el-input v-model="queryParams.content" placeholder="MAC/IMEI" clearable
style="width: 200px; margin-right: 20px;" @keyup.enter="handleQuery" @input="handleInput" />
<el-button type="primary" plain @click="toggleFilter">高级筛选</el-button>
</div>
</div>
<el-collapse accordion v-model="activeNames">
<el-collapse-item name="1">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" class="queryFormRef">
<el-form-item label="设备类型" prop="deviceType">
<el-select v-model="queryParams.deviceType" placeholder="设备类型" clearable>
<el-option v-for="item in deviceTypeOptions" :key="item.value" :label="item.typeName"
:value="item.deviceTypeId" />
</el-select>
</el-form-item>
<el-form-item label="设备名称" prop="deviceName">
<el-input v-model="queryParams.deviceName" placeholder="请输入设备名称" clearable />
</el-form-item>
<el-form-item label="设备状态" prop="onlineStatus">
<el-select v-model="queryParams.onlineStatus" placeholder="设备状态" clearable>
<el-option label="在线" value="1"></el-option>
<el-option label="离线" value="0"></el-option>
<el-option label="故障" value="2"></el-option>
</el-select>
</el-form-item>
<el-form-item label="设备MAC" prop="deviceMac">
<el-input v-model="queryParams.deviceMac" placeholder="请输入设备MAC" clearable />
</el-form-item>
<el-form-item label="设备IMEI" prop="deviceImei">
<el-input v-model="queryParams.deviceImei" placeholder="请输入设备IMEI" clearable
@keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="使用人员" prop="personnelBy">
<el-input v-model="queryParams.personnelBy" placeholder="请输入使用人员姓名" clearable
@keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="通信方式" prop="communicationMode">
<el-select v-model="queryParams.communicationMode" placeholder="请选择通信方式" clearable>
<el-option label="4G" value="0"></el-option>
<el-option label="蓝牙" value="1"></el-option>
<el-option label="4G&蓝牙" value="2"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-collapse-item>
</el-collapse>
</el-card>
</div>
</transition>
<el-card class="Maplist">
<div v-if="isListView" key="list">
<el-table v-loading="loading" border :data="deviceList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" :selectable="isSelectable" />
<el-table-column label="设备名称" align="center" prop="deviceName" />
<el-table-column label="设备图片" align="center" prop="devicePic">
<template #default="scope">
<el-popover placement="right" trigger="click">
<template #reference>
<img :src="scope.row.devicePic"
style="width: 40px; height: 40px; cursor: pointer; object-fit: contain"
class="hover:opacity-80 transition-opacity" />
</template>
<img :src="scope.row.devicePic" style="max-width: 600px; max-height: 600px; object-fit: contain" />
</el-popover>
</template>
</el-table-column>
<el-table-column label="设备类型" align="center" prop="typeName" />
<el-table-column label="设备MAC" align="center" prop="deviceMac">
<template #default="scope">
<div>{{ scope.row.deviceMac || '/' }}</div>
</template>
</el-table-column>
<el-table-column label="设备IMEI" align="center" prop="deviceImei" />
<el-table-column label="使用人员" align="center" prop="personnelBy" />
<el-table-column label="设备状态" align="center" prop="onlineStatus">
<template #default="scope">
<div class="normal green" v-if="scope.row.onlineStatus == 1">在线</div>
<div class="normal red" v-if="scope.row.onlineStatus == 0">离线</div>
<div class="normal red" v-if="scope.row.onlineStatus == 2">故障</div>
</template>
</el-table-column>
<el-table-column label="电量" align="center" prop="battery">
<template #default="scope">
<div :class="{
'battery-red': Number(scope.row.battery) < 20,
'battery-yellow': Number(scope.row.battery) >= 20 && Number(scope.row.battery) < 80,
'battery-green': Number(scope.row.battery) >= 80 && Number(scope.row.battery) <= 100
}">
{{ scope.row.battery || '0' }}%
</div>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" @click="handleControl(scope.row)">控制面板</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize"
:total="total" @pagination="getList" />
</div>
<div v-else key="map">
<Amap :deviceList="deviceList"></Amap>
</div>
</el-card>
</el-col>
</el-row>
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
<div class="title_ps">{{ `${arrayDeviceName.join('、')}设备发送消息` }}</div>
<el-form ref="postFormRef" :model="form" label-width="80px">
<el-input type="textarea" v-model="form.messageToSend" placeholder="请输入消息内容" />
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm" :loading="sendTextLoading"
:loading-text="sendTextLoading ? '发送中...' : ' '"> {{
sendTextLoading ? '发送中' : '确 定' }}</el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="User" lang="ts">
import api from '@/api/controlCenter/controlPanel/index'
import apiTypeAll from '@/api/equipmentManagement/device/index';
import { deviceQuery, deviceVO } from '@/api/controlCenter/controlPanel/types';
import Amap from "./components/map.vue";
import { generateShortId, getDeviceStatus } from '@/utils/function';
const router = useRouter();
const route = useRoute(); // 新增用于获取URL中的query参数
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const deviceList = ref<deviceVO[]>();
const loading = ref(true);
const showSearch = ref(true);
const ids = ref<Array<number | string>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const deptName = ref();
const deptOptions = ref([])
const deptTreeRef = ref<ElTreeInstance>();
const queryFormRef = ref<ElFormInstance>();
const isListView = ref(true);
const activeNames = ref([]);
const deviceTypeOptions = ref([]); //设备类型
const enabledDeptOptions = ref();
const forceAlarmLoading = ref(false) //强制报警
const sendTextLoading = ref(false)
const debounceTimer = ref(null) // 用于防抖的定时器
const form = ref({
messageToSend: ''
})
const initData: PageData<'', deviceQuery> = {
queryParams: {
pageNum: 1,
pageSize: 10,
deviceId: '',
deviceName: '',
onlineStatus: '',
deviceMac: '',
deviceImei: '',
personnelBy: '',
communicationMode: '',
groupId: '',
deviceType: '',
content: ''
},
rules: undefined,
form: ''
};
const data = reactive<PageData<'', deviceQuery>>(initData);
const { queryParams } = toRefs<PageData<'', deviceQuery>>(data);
const switchView = (view: 'list' | 'map') => {
isListView.value = (view === 'list');
router.push({
path: route.path,
query: {
...(view == '' ? { view: '' } : { view: undefined })
}
});
};
const dialog = reactive<DialogOption>({
visible: false,
title: '发送信息'
});
const isSelectable = (row: any) => {
// 仅当在线状态onlineStatus == 1时允许选中
return row.onlineStatus === 1;
}
/** 通过条件过滤节点 */
const filterNode = (value: string, data: any) => {
if (!value) return true;
return data.groupName.indexOf(value) !== -1;
};
// 设备类型
const getDeviceType = () => {
apiTypeAll.deviceTypeAll().then(res => {
if (res.code == 200) {
deviceTypeOptions.value = res.data
}
}).catch(err => {
})
};
/** 根据名称筛选部门树 */
watchEffect(
() => {
deptTreeRef.value?.filter(deptName.value);
},
{
flush: 'post' // watchEffect会在DOM挂载或者更新之前就会触发此属性控制在DOM元素更新后运行
}
);
const toggleFilter = () => {
if (activeNames.value.length > 0) {
activeNames.value = [];
} else {
activeNames.value = ['1'];
}
};
/** 过滤禁用的部门 */
const filterDisabledDept = (deptList: any[]) => {
return deptList.filter((dept) => {
if (dept.disabled) {
return false;
}
if (dept.children && dept.children.length) {
dept.children = filterDisabledDept(dept.children);
}
return true;
});
};
/** 节点单击事件 */
const handleNodeClick = (data: any) => {
queryParams.value.groupId = data.id;
handleQuery();
};
const handleInput = () => {
if (debounceTimer.value) {
clearTimeout(debounceTimer.value)
}
// 300ms后执行查询避免输入过程中频繁调用接口
debounceTimer.value = setTimeout(() => {
handleQuery() // 调用查询接口的方法
}, 300)
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
queryParams.value.pageNum = 1;
queryParams.value.groupId = undefined;
deptTreeRef.value?.setCurrentKey(undefined);
handleQuery();
};
/** 设备控制跳转动态取detailPageUrl字段值跳转 */
const handleControl = (row: any) => {
const deviceId = row.id;
const detailPageUrl = row.detailPageUrl;
const basePath = detailPageUrl.replace(/\/index$/, '');
const dynamicPath = `/${basePath}/${deviceId}`;
router.push(dynamicPath); // 跳转路由
};
/** 选择条数 */
const arrayDeviceName = ref([])
const handleSelectionChange = (selection: deviceVO[]) => {
console.log(selection, 'selectionselection');
arrayDeviceName.value = selection.map(item => item.deviceName);
ids.value = selection;
single.value = selection.length != 1;
multiple.value = !selection.length;
};
// 3. 初始化视图状态关键根据地址栏view参数赋值
const initViewStatus = () => {
// 打印参数确认是否获取到view=map调试用
console.log('地址栏view参数', route.query.view);
// 逻辑地址栏有view=map → 地图视图isListView=false否则列表视图
const currentView = route.query.view as string;
isListView.value = currentView !== 'map'; // 关键判断!
};
onMounted(() => {
getDeptTree(); // 初始化部门数据
getList(); // 初始化列表数据
getDeviceType() //设备类型
initViewStatus()
});
/** 查询用户列表 */
const getList = async () => {
loading.value = false;
const res = await api.deviceControlCenterList(queryParams.value);
loading.value = false;
deviceList.value = res.rows;
total.value = res.total;
};
/** 查询部门下拉树结构 */
const getDeptTree = async () => {
const res = await api.devicegroupList('');
const allDeviceOption = {
id: '',
groupName: '全部设备',
disabled: false,
children: []
};
deptOptions.value = [allDeviceOption, ...res.data]
enabledDeptOptions.value = filterDisabledDept(res.data);
};
const sendTextMessage = () => {
// 防重复提交
if (!queryParams.value.deviceType) {
ElMessage.closeAll();
proxy?.$modal.msgWarning('请先选择设备类型');
return;
}
if (ids.value.length == 0) {
ElMessage.closeAll();
proxy?.$modal.msgWarning('请先选中一条设备');
return;
}
dialog.visible = true
}
// 发送文本消息确认
const submitForm = async () => {
if (!form.value.messageToSend) {
ElMessage.closeAll();
proxy?.$modal.msgWarning('发送消息不能为空');
return
}
try {
sendTextLoading.value = true;
// 2. 准备请求数据
const deviceIds = ids.value.map(item => item.id);
const deviceImeiList = ids.value.map(item => item.deviceImei);
const firstDevice = ids.value[0];
const typeName = firstDevice.typeName; // 取第一个设备的typeName为空时给默认值
const batchId = generateShortId();
const data = {
deviceIds: deviceIds,
typeName: typeName,
batchId: batchId,
deviceImeiList: deviceImeiList,
sendMsg: form.value.messageToSend
};
// 3.人员信息
const registerRes = await api.deviceSendMessage(data);
if (registerRes.code !== 200) {
proxy?.$modal.msgWarning(registerRes.msg)
return
}
// 4. 获取设备状态
// 获取实时状态类型:FunctionAccessBatchstatusRule 批量 ,FunctionAccessStatusRule 单个
const statusRes = await getDeviceStatus({
functionMode: 2,
batchId,
typeName: 'FunctionAccessBatchStatusRule',
deviceImeiList,
deviceIds,
interval: 500
},
api.deviceRealTimeStatus
);
// 只有当状态为'OK'时才显示成功弹窗
if (statusRes.data.functionAccess === 'OK') {
proxy?.$modal.msgSuccess(statusRes.msg);
cancel()
}
} catch (error: any) {
proxy?.$modal.msgWarning(error.msg)
} finally {
sendTextLoading.value = false;
}
};
// 取消
const cancel = () => {
dialog.visible = false;
form.value.messageToSend = ''
}
// 强制报警
const forceAlarm = async () => {
if (!queryParams.value.deviceType) {
ElMessage.closeAll();
proxy?.$modal.msgWarning('请先选择设备类型');
return;
}
if (ids.value.length == 0) {
ElMessage.closeAll();
proxy?.$modal.msgWarning('请先选中一条设备');
return;
}
try {
await proxy?.$modal.confirm('确定要对所选设备开启强制报警?', '强制报警');
forceAlarmLoading.value = true
// 2. 准备请求数据
const batchId = generateShortId();
const deviceIds = ids.value.map(item => item.id);
const deviceImeiList = ids.value.map(item => item.deviceImei);
const firstDevice = ids.value[0];
const typeName = firstDevice.typeName; // 取第一个设备的typeName为空时给默认值
let data = {
deviceIds: deviceIds,
typeName: typeName,
batchId: batchId,
deviceImeiList: deviceImeiList,
instructValue: '1', //强制报警1解除报警0
}
const registerRes = await api.sendAlarmMessage(data);
if (registerRes.code !== 200) {
proxy?.$modal.msgWarning(registerRes.msg)
return
}
// 4. 获取设备状态
const statusRes = await getDeviceStatus({
functionMode: 2,
batchId,
typeName: 'FunctionAccessBatchStatusRule',
deviceImeiList,
deviceIds,
interval: 500
},
api.deviceRealTimeStatus
);
// 只有当状态为'OK'时才显示成功弹窗
if (statusRes.data.functionAccess === 'OK') {
proxy?.$modal.msgSuccess(statusRes.msg);
}
} catch (error: any) {
proxy?.$modal.msgWarning(error.msg)
} finally {
forceAlarmLoading.value = false;
}
}
</script>
<style lang="scss" scoped>
.p-2 {
background: rgba(247, 248, 252, 1);
min-height: 100vh;
box-sizing: border-box;
padding: 15px;
}
:deep .el-collapse {
border-top: none
}
:deep .el-collapse-item__header {
display: none;
}
:deep .el-tree--highlight-current .el-tree-node.is-current>.el-tree-node__content {
color: rgba(2, 124, 251, 1);
background: transparent;
}
.battery-red {
color: rgba(224, 52, 52, 1);
}
/* 20%~80% 黄色(或橙色,根据设计图调整) */
.battery-yellow {
color: rgba(234, 152, 0, 1);
}
/* 80%~100% 绿色 */
.battery-green {
color: rgba(234, 152, 0, 1);
}
.title_ps {
color: rgba(224, 52, 52, 1);
padding: 0px 0 20px 0;
}
.main-tree {
border-radius: 4px;
box-shadow: 0px 0px 6px 0px rgba(0, 34, 96, 0.1);
background: rgba(255, 255, 255, 1);
width: 212px;
border: none;
padding-top: 10px;
}
.el-card {
border: none
}
.btn_search {
padding: 0px 15px 15px 0px;
// border-bottom: 1px solid rgba(235, 238, 248, 1);
}
.queryFormRef {
margin-top: 20px;
}
.green {
color: rgba(0, 165, 82, 1);
}
.red {
color: rgba(224, 52, 52, 1);
}
.Maplist {
height: 680px;
overflow: auto;
}
</style>