100j控制面包页面开发

This commit is contained in:
fengerli
2026-03-10 18:03:33 +08:00
parent 15719b4a27
commit d6675050e6
8 changed files with 946 additions and 13 deletions

View File

@ -6,8 +6,8 @@ VITE_APP_ENV = 'development'
# 开发环境
# VITE_APP_BASE_API = 'http://139.224.253.23:8000'
# VITE_APP_BASE_API = 'https://www.cnxhyc.com/jq'
VITE_APP_BASE_API = 'http://192.168.110.57:8000'
VITE_APP_BASE_API = 'https://www.cnxhyc.com/jq'
#VITE_APP_BASE_API = 'http://192.168.110.57:8000'
#代永飞接口
# VITE_APP_BASE_API = 'http://457102h2d6.qicp.vip:24689'

View File

@ -0,0 +1,51 @@
import request from '@/utils/request';
// 详情信息
export const deviceDeatil = (id: string) => {
return request({
url: `/api/hby100j/device/${id}`,
method: 'get',
});
};
// 灯光模式
function lightModeSettings (data: any) {
return request({
url: `/app/hby100j/device/lightAdjustment`,
method: 'post',
data: data
});
};
//频率调节
function staticPowerSetting (data: any) {
return request({
url: `/app/hby100j/device/strobeFrequency`,
method: 'post',
data: data
});
};
// 修改音量
function settingUpdateVolume (data: any) {
return request({
url: `/app/hby100j/device/updateVolume`,
method: 'post',
data: data
});
};
// 强制报警
function SosSetting (data: any) {
return request({
url: `/app/hby100j/device/forceAlarmActivation`,
method: 'post',
data: data
});
};
export default {
deviceDeatil,
lightModeSettings:lightModeSettings,
SosSetting:SosSetting,
staticPowerSetting:staticPowerSetting,
settingUpdateVolume:settingUpdateVolume
};

View File

@ -36,14 +36,10 @@ export interface DeviceDetail {
currentLightMode?: string;// 当前选中的灯光模式(如"strong",对应强光)
sendMsg: string;
lightBrightness: string;
personnelInfo: { // 人员信息(嵌套对象,根据接口调整)
unitName: string; // 单位
position: string; // 职位
name: string; // 姓名
code: string; // ID身份证/工号)
};
strobeFrequency: string;
volume: string;
chargeState: string;
alarmStatus:number
alarmStatus: number
}
// 定义灯光模式的类型接口
export interface LightMode {

View File

@ -28,7 +28,6 @@ function copyTextToClipboard(input: string, { target = document.body } = {}) {
element.value = input;
// Prevent keyboard from showing on mobile
element.setAttribute('readonly', '');
element.style.contain = 'strict';
element.style.position = 'absolute';
element.style.left = '-9999px';

View File

@ -0,0 +1,886 @@
<template>
<div class="device-page p-2">
<!-- 头部信息栏 -->
<div class="header-bar">
<div>设备名称{{ deviceDetail.deviceName }}</div>
<div>设备型号{{ deviceDetail.deviceImei }}</div>
<div class="device-status">设备状态
<span :class="{ online: deviceDetail.onlineStatus === 1, offline: deviceDetail?.onlineStatus === 0 }">
{{ deviceDetail.onlineStatus === 1 ? '在线' : '离线' }}
</span>
</div>
<div>电量{{ deviceDetail.batteryPercentage || 0 }}%</div>
<div>续航{{ deviceDetail.batteryRemainingTime || "0" }} 分钟</div>
</div>
<!-- 主体内容区域 -->
<div class="content-wrapper">
<el-row :gutter="20" class="content-row" :class="deviceDetail.alarmStatus == 1 ? '' : 'displayNone'">
<el-col :lg="24" :xs="24">
<div class="staticRwo" :class="deviceDetail.alarmStatus == 1 ? '' : 'displayNone'"
@click="showClose()">
设备强制报警中!
</div>
</el-col>
</el-row>
<!-- 第一行灯光模式 + 灯光亮度强制报警位置信息 -->
<el-row :gutter="20" class="content-row">
<el-col :lg="16" :xs="24">
<div class="content-card">
<h4 class="section-title">报警模式</h4>
<div class="light-mode">
<!-- 使用v-for循环渲染灯光模式卡片 -->
<div class="mode-card" :class="{ 'active': mode.active }"
@click.stop="handleVoiceType(mode.id)" v-for="mode in sta_VoiceType" :key="mode.id">
<img :src="mode.active ? mode.activeIcon : mode.icon" :alt="mode.name"
class="mode-icon" />
<div class="mode-name">{{ mode.name }}</div>
<el-switch v-model="mode.switchStatusVioice" />
</div>
</div>
</div>
</el-col>
<el-col :lg="8" :xs="24">
<div class="brightness-alarm">
<el-button type="danger" class="alarm-btn" @click="forceAlarm" :loading="forceAlarmLoading"
v-if="deviceDetail.alarmStatus === 0 || deviceDetail.alarmStatus === null"
:loading-text="forceAlarmLoading ? '报警中...' : '强制报警'"> {{
forceAlarmLoading ? '报警中' : '强制报警' }}</el-button>
</div>
<div class="content-card_gps">
<h4 class="section-title">位置信息</h4>
<div class="location-info">
<div class="location-item">
<span class="location-icon"></span>
<span>经纬度 {{ deviceDetail && deviceDetail.longitude ?
Number(deviceDetail.longitude).toFixed(4) : '无' }}
{{ deviceDetail && deviceDetail.latitude ? Number(deviceDetail.latitude).toFixed(4)
: '无' }} </span>
</div>
<div class="location-item">
<div>地址 <span class="lacatin_gps">{{ deviceDetail.address || "未获取到地址" }}</span></div>
<el-button link type="primary" class="view-btn"
@click="lookMap(deviceDetail)">查看</el-button>
</div>
</div>
</div>
</el-col>
</el-row>
<!-- 第二行人员信息登记 + 发送信息 -->
<el-row :gutter="20" class="content-row">
<el-col :lg="16" :xs="24">
<div class="content-card">
<h4 class="section-title">警示灯模式</h4>
<div class="light-mode">
<!-- 使用v-for循环渲染灯光模式卡片 -->
<div class="mode-card" :class="{ 'active': mode.active }"
@click.stop="handleModeClick(mode.id)" v-for="mode in lightModes" :key="mode.id">
<img :src="mode.active ? mode.activeIcon : mode.icon" :alt="mode.name"
class="mode-icon" />
<div class="mode-name">{{ mode.name }}</div>
<el-switch v-model="mode.switchStatus" />
</div>
</div>
</div>
</el-col>
<el-col :lg="8" :xs="24">
<div class="content-card ">
<div class="brightness-alarm">
<div class="brightness-control">
<span class="brightness-label">亮度</span>
<el-input class="inputTFT" v-model="deviceDetail.lightBrightness" :min="10" :max="100"
:step="1" size="small" />
<span class="brightness-value">%</span>
<el-button type="primary" class="save-btn" v-loading="lightModesLoading"
@click="saveBtnlight">保存 </el-button>
</div>
</div>
<div class="brightness-alarm">
<div class="brightness-control">
<span class="brightness-label">频率</span>
<el-input class="inputTFT" v-model="deviceDetail.strobeFrequency" :min="0.5" :max="10"
:step="1" size="small" />
<span class="brightness-value">HZ</span>
<el-button type="primary" class="save-btn" @click="saveBtnstrobe">保存 </el-button>
</div>
</div>
<div class="brightness-alarm">
<div class="brightness-control">
<span class="brightness-label">音量</span>
<el-input class="inputTFT" v-model="deviceDetail.volume" :min="10" :max="100" :step="1"
size="small" />
<span class="brightness-value">%</span>
<el-button type="primary" class="save-btn" @click="saveBtnVolume">保存 </el-button>
</div>
</div>
</div>
</el-col>
</el-row>
</div>
<!-- ===========充电提示框====== -->
<el-dialog title="充电提示" v-model="centerDialogVisible" width="15%">
<div style="display: flex; align-items: center;">
<h3 style="color: rgba(224, 52, 52, 1)">设备电量低于20%</h3>
</div>
<div>请及时充电</div>
<span slot="footer" class="dialog-footer" style="text-align: right;display: block;">
<el-button type="primary" @click="centerDialogVisible = false"> </el-button>
</span>
</el-dialog>
</div>
</template>
<script setup name="DeviceControl" lang="ts">
const route = useRoute();
import { useMqtt } from '@/utils/mqtt';
import api from '@/api/controlCenter/controlPanel/100J'
import { DeviceDetail, LightMode } from '@/api/controlCenter/controlPanel/types';
import {getDeviceStatus } from '@/utils/function';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const router = useRouter();
// 导入图片资源(确保路径正确)
import closeDefault from '@/assets/images/close.png';
import closeActive from '@/assets/images/close_HL.png';
import rb from '@/assets/images/rb.png';
import rbAc from '@/assets/images/rbAc.png';
import sg from '@/assets/images/sg.png';
import sgAc from '@/assets/images/sgAc.png';
const forceAlarmLoading = ref(false) //强制报警
const lightModesLoading = ref(false)
const centerDialogVisible = ref(false)
const {
connected,
connect,
subscribe,
onConnect,
onError,
onMessage,
disconnect
} = useMqtt();
// 报警模式
const sta_VoiceType = ref([
{
id: 'fire',
name: '消防',
icon: sg, // 直接使用导入的变量
activeIcon: sgAc,
switchStatusVioice: true,
active: true
},
{
id: '0',
name: '公安',
icon: sg, // 直接使用导入的变量
activeIcon: sgAc,
active: false,
switchStatusVioice: false,
},
{
id: '3',
name: '交警',
icon: sg, // 直接使用导入的变量
activeIcon: sgAc,
switchStatusVioice: false,
active: false
},
{
id: '4',
name: '市政',
icon: sg, // 直接使用导入的变量
activeIcon: sgAc,
switchStatusVioice: false,
active: false
},
{
id: '2',
name: '应急',
icon: sg, // 直接使用导入的变量
activeIcon: sgAc,
switchStatusVioice: false,
active: false
},
{
id: '6',
name: '医疗',
icon: sg, // 直接使用导入的变量
activeIcon: sgAc,
switchStatusVioice: false,
active: false
},
{
id: '5',
name: '铁道',
icon: sg, // 直接使用导入的变量
activeIcon: sgAc,
switchStatusVioice: false,
active: false
},
{
id: '7',
name: 'app语音',
icon: sg, // 直接使用导入的变量
activeIcon: sgAc,
switchStatusVioice: false,
active: false
},
{
id: '-1',
name: '关闭',
icon: closeDefault, // 直接使用导入的变量
activeIcon: closeActive,
switchStatusVioice: false,
active: false
},
])
// 警示灯灯光模式数据(引用导入的图片)
const lightModes = ref<LightMode[]>([
{
id: 'redBlueAlternate', // 红蓝交替
name: '红蓝交替',
icon: rb,
activeIcon: rbAc,
switchStatus: true,
instructValue: '6',
active: true,
},
{
id: 'redFlash', // 红闪
name: '红闪',
icon: rb,
activeIcon: rbAc,
switchStatus: false,
instructValue: '0',
active: false,
},
{
id: 'yellowFlash', // 黄闪
name: '黄闪',
icon: rb,
activeIcon: rbAc,
switchStatus: false,
instructValue: '2',
active: false
},
{
id: 'blueFlash', // 蓝闪
name: '蓝闪',
icon: rb,
activeIcon: rbAc,
switchStatus: false,
instructValue: '1',
active: false
},
{
id: 'redClockwise', // 红色顺时针
name: '红色顺时针',
icon: rb,
activeIcon: rbAc,
switchStatus: false,
instructValue: '3',
active: false
},
{
id: 'yellowClockwise', // 黄色顺时针
name: '黄色顺时针',
icon: rb,
activeIcon: rbAc,
switchStatus: false,
instructValue: '4',
active: false
},
{
id: 'redBlueClockwise', // 红蓝顺时针
name: '红蓝顺时针',
icon: rb,
activeIcon: rbAc,
switchStatus: false,
instructValue: '5',
active: false
},
{
id: 'close', // 关闭
name: '关闭',
icon: closeDefault,
activeIcon: closeActive,
switchStatus: false,
instructValue: '-1',
active: false
},
]);
const deviceDetail = ref<DeviceDetail & { typeName: string }>({
lightBrightness: '',
deviceName: '',
deviceImei: '',
onlineStatus: 0,
batteryPercentage: 0,
batteryRemainingTime: '',
longitude: '',
latitude: '',
address: '',
sendMsg: '',
chargeState: '0',
typeName: '',
alarmStatus: 0,
strobeFrequency: '0.5',
volume: '10'
});
const isUpdatingStatus = ref(false);
// 报警模式// 2. 点击卡片事件
const handleVoiceType = async (targetId: string) => {
const deviceId = route.params.deviceId as string;
if (!deviceId) return;
const targetMode = sta_VoiceType.value.find(mode => mode.id === targetId);
if (!targetMode) return;
if (targetMode.active) return;
sta_VoiceType.value.forEach(mode => {
mode.active = mode.id === targetId;
if (mode.active) {
mode.switchStatusVioice = true;
} else {
mode.switchStatusVioice = false;
}
});
// 2. 调用强制报警接口
const params = {
deviceIds: [deviceId],
voiceStrobeAlarm: targetId === '-1' ? 0 : 1,
mode: targetId == '-1' ? 0 : targetId
};
try {
const res = await api.SosSetting(params);
if (res.code == 200) {
proxy?.$modal.msgSuccess(res.msg);
}
} catch (error) {
await getList()
}
};
// 警示灯模式
const handleModeClick = async (modeId: string) => {
if (isUpdatingStatus.value || isSyncingStatus.value) return;
try {
const deviceId = route.params.deviceId as string;
if (!deviceId) return;
const targetMode = lightModes.value.find(m => m.id === modeId);
if (!targetMode || !targetMode.instructValue) return;
isUpdatingStatus.value = true;
const res = await api.lightModeSettings({
deviceId,
instructValue: targetMode.instructValue,
deviceImei: deviceDetail.value.deviceImei,
typeName: deviceDetail.value.typeName,
});
if (res.code === 200) {
ElMessage.closeAll();
proxy?.$modal.msgSuccess(res.msg);
setActiveLightMode(modeId);
} else {
proxy?.$modal.msgError(res.msg);
const prevActiveMode = lightModes.value.find(m => m.active);
if (prevActiveMode) {
setActiveLightMode(prevActiveMode.id);
}
}
} catch (error) {
// 异常时恢复状态
const prevActiveMode = lightModes.value.find(m => m.active);
if (prevActiveMode) {
setActiveLightMode(prevActiveMode.id);
}
} finally {
isUpdatingStatus.value = false;
}
};
const isSyncingStatus = ref(false);
// 警示灯接口
const setActiveLightMode = (targetModeId: string) => {
isSyncingStatus.value = true;
lightModes.value.forEach(mode => {
const isActive = mode.id === targetModeId;
mode.active = isActive;
mode.switchStatus = isActive;
});
isSyncingStatus.value = false;
};
const getList = async () => {
try {
const deviceId = route.params.deviceId;
if (!deviceId) return;
const res = await api.deviceDeatil(deviceId as string);
deviceDetail.value = res.data;
// ==========灯光模式逻辑==========
let targetModeId = "redBlueAlternate";
const mainLightMode = String(res.data.strobeMode);
const matchedMode = lightModes.value.find(
mode => mode.instructValue === mainLightMode
);
if (matchedMode) {
targetModeId = matchedMode.id;
}
setActiveLightMode(targetModeId);
// 报警模式
const alarmMode = String(res.data.alarmMode); // 报警模式ID
const voiceStrobeAlarm = String(res.data.voiceStrobeAlarm); // 报警开关状态1开启0关闭
// 先重置所有报警模式的状态
sta_VoiceType.value.forEach(mode => {
mode.active = false;
mode.switchStatusVioice = false;
});
if (voiceStrobeAlarm === '1') {
const matchedVoiceMode = sta_VoiceType.value.find(
mode => mode.id === alarmMode
);
if (matchedVoiceMode) {
matchedVoiceMode.active = true;
matchedVoiceMode.switchStatusVioice = true;
} else {
console.warn('未找到对应的报警模式:', alarmMode);
}
} else {
const closeMode = sta_VoiceType.value.find(mode => mode.id === '-1');
if (closeMode) {
closeMode.active = true;
closeMode.switchStatusVioice = true;
}
}
} catch (error) {
console.error('获取设备详情失败:', error);
}
};
// 灯光亮度
const saveBtnlight = () => {
lightModesLoading.value = true
let data = {
deviceId: route.params.deviceId,
brightness: deviceDetail.value.lightBrightness,
}
api.lightModeSettings(data).then((res) => {
if (res.code === 200) {
lightModesLoading.value = false
proxy?.$modal.msgSuccess(res.msg);
} else {
lightModesLoading.value = false
proxy?.$modal.msgError(res.msg);
}
}).catch((error) => {
lightModesLoading.value = false
});
}
// 爆闪频率
const saveBtnstrobe = () => {
let data = {
deviceId: route.params.deviceId,
frequency: deviceDetail.value.strobeFrequency,
}
api.staticPowerSetting(data).then((res) => {
if (res.code === 200) {
proxy?.$modal.msgSuccess(res.msg);
}
})
}
// 修改音量
const saveBtnVolume = () => {
let data = {
deviceId: route.params.deviceId,
volume: deviceDetail.value.volume,
}
api.settingUpdateVolume(data).then((res) => {
if (res.code === 200) {
proxy?.$modal.msgSuccess(res.msg);
}
})
}
// 解除报警
const showClose = async () => {
try {
await proxy?.$modal.confirm('确定要对该设备解除报警?', '提示');
// 2. 准备请求数据
let data = {
deviceIds: [route.params.deviceId],
typeName: deviceDetail.value.typeName,
deviceImeiList: [deviceDetail.value.deviceImei],
instructValue: '0', //强制报警1解除报警0
}
} catch (error: any) {
}
}
// 强制报警
const forceAlarm = async () => {
try {
await proxy?.$modal.confirm('确定要对该设备开启强制报警?', '提示');
forceAlarmLoading.value = true
// 2. 准备请求数据
let data = {
deviceIds: [route.params.deviceId],
typeName: deviceDetail.value.typeName,
deviceImeiList: [deviceDetail.value.deviceImei],
instructValue: '1', //强制报警1解除报警0
}
} catch (error: any) {
// proxy?.$modal.msgWarning(error.msg)
forceAlarmLoading.value = false;
} finally {
forceAlarmLoading.value = false;
}
}
const lookMap = (row: any) => {
console.log(row, 'rowrowrowrowrworowrowrowrowrowrow');
router.push({
path: '/controlCenter/controlPanel',
query: {
view: 'map',
deviceId: row.deviceId
}
});
}
onMounted(async () => {
await getList();
});
onUnmounted(() => {
});
</script>
<style lang="scss" scoped>
.p-2 {
background: rgba(247, 248, 252, 1);
min-height: 100vh;
box-sizing: border-box;
padding: 15px;
}
.device-page {
.header-bar {
border-radius: 8px;
background: linear-gradient(135deg, #3400e7, #009bff);
color: white;
padding: 20px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
.device-status {
.online {
color: #00ff00;
}
.offline {
color: rgb(224, 52, 52);
}
}
}
.content-wrapper {
.content-row {
margin-bottom: 10px;
}
.content-card {
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 34, 96, 0.1);
background: white;
padding: 0px 20px 50px;
border: 1px solid #ebeef5;
height: 289px;
position: relative;
}
.content-card_gps {
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 34, 96, 0.1);
background: white;
padding: 0px 20px 50px;
border: 1px solid #ebeef5;
height: 78%;
}
.section-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 20px;
color: #303133;
}
.light-mode {
display: flex;
justify-content: space-between;
}
.lacatin_gps {
height: 70px;
border-radius: 4px;
background: rgba(247, 248, 252, 1);
display: inline-block;
width: 400px;
padding: 10px;
}
.mode-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 15px;
border: 1px solid #dcdfe6;
border-radius: 8px;
width: 107px;
cursor: pointer;
transition: all 0.3s ease;
&.active {
border-color: #409eff;
background-color: rgba(64, 158, 255, 0.05);
}
.mode-icon {
width: 48px;
margin-bottom: 10px;
transition: all 0.3s ease;
object-fit: scale-down;
height: 50px;
}
.mode-name {
font-size: 14px;
margin-bottom: 12px;
color: #606266;
}
.el-switch {
--el-switch-on-color: #409eff;
--el-switch-off-color: #dcdfe6;
}
}
.brightness-alarm {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
.brightness-control {
display: flex;
align-items: center;
gap: 10px;
border-radius: 39px;
// box-shadow: 0px 0px 6px 0px rgba(0, 34, 96, 0.1);
background: rgba(255, 255, 255, 1);
width: 100%;
// height: 54px;
padding: 10px 20px;
margin-top: 20px;
.brightness-label {
font-size: 14px;
color: #606266;
min-width: 70px;
}
.brightness-value {
font-size: 14px;
color: #409eff;
font-weight: 500;
}
.save-btn {
padding: 6px 20px;
border-radius: 29px;
background: rgba(2, 124, 251, 1);
border: none
}
.inputTFT {
width: 130px;
height: 40px;
}
}
.alarm-btn {
background-color: rgba(224, 52, 52, 1);
border-color: rgba(224, 52, 52, 1);
padding: 8px 20px;
border-radius: 27px;
}
}
.location-info {
.location-item {
display: flex;
align-items: center;
margin-bottom: 15px;
font-size: 14px;
color: #606266;
.location-icon {
margin-right: 8px;
font-weight: bold;
color: #409eff;
}
.view-btn {
margin: 0 8px;
padding: 0;
font-size: 13px;
}
}
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-bottom: 15px;
text-align: right;
.form-item {
display: flex;
align-items: center;
.form-label {
min-width: 35px;
font-size: 14px;
color: #606266;
margin-right: 10px;
}
.el-input {
flex: 1;
}
}
.register-btn {
grid-column: 1 / 3;
justify-self: start;
padding: 6px 20px;
position: absolute;
right: 19px;
bottom: 30px;
border-radius: 29px;
background: rgba(2, 124, 251, 1);
border: none
}
}
.message-content {
// display: flex;
// flex-direction: column;
// gap: 10px;
.el-textarea {
border: 1px solid rgba(29, 111, 255, 1);
border-radius: 4px;
background: rgba(247, 248, 252, 1);
border-radius: 6px;
padding: 10px;
}
.el-textarea__inner {
background: rgba(247, 248, 252, 1);
}
.send-btn {
align-self: flex-end;
padding: 10px 20px;
border-radius: 29px;
background: rgba(2, 124, 251, 1);
border: none;
margin: 20px 0px 30px 0;
}
}
.upload-area {
display: flex;
justify-content: center;
align-items: center;
height: 100px;
border: 1px dashed #dcdfe6;
border-radius: 6px;
cursor: pointer;
}
.video-input {
display: flex;
gap: 10px;
align-items: center;
.el-input {
flex: 1;
}
.save-video-btn {
padding: 6px 12px;
}
}
}
// 响应式调整
@media (max-width: 768px) {
.header-bar {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.form-grid {
grid-template-columns: 1fr;
}
.form-grid .register-btn {
grid-column: 1;
}
.light-mode .mode-group {
gap: 10px;
}
.light-mode .mode-item {
width: 60px;
}
.light-mode .mode-icon {
width: 30px;
height: 30px;
}
}
}
.path-img {
width: 52px;
height: 28px;
}
.staticRwo {
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 34, 96, 0.1);
background: white;
border: 1px solid #ebeef5;
height: auto;
line-height: 36px;
box-sizing: border-box;
text-indent: 15px;
color: #ff0000;
font-weight: bold;
font-size: 17px;
margin-bottom: 5px;
}
.displayNone {
display: none !important;
}
</style>

View File

@ -6,3 +6,4 @@ export default () => {
autoInstall: true
});
};