1
0
forked from dyf/dyf-vue-ui
This commit is contained in:
liub
2025-10-10 12:02:54 +08:00
13 changed files with 292 additions and 89 deletions

View File

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

View File

@ -42,7 +42,8 @@ export interface DeviceDetail {
name: string; // 姓名
code: string; // ID身份证/工号)
};
chargeState: string
chargeState: string;
alarmStatus:number
}
// 定义灯光模式的类型接口
export interface LightMode {

View File

@ -5,6 +5,7 @@ export interface deviceQuery extends PageQuery {
deviceType: string;
deviceStatus: string;
bluetoothName?: string; // 蓝牙名称查询字段
onlineStatus?: string;
}
export interface deviceForm {

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 17.99 18">
<path id="_矢量_42" d="M17.82,5.81L9.5.08c-.16-.11-.38-.11-.54,0L.2,5.81c-.13.08-.2.21-.2.35v11.41c0,.24.21.43.46.43h5.54c.25,0,.46-.19.46-.43v-2.48c0-1.42,1.17-2.65,2.68-2.69,1.57-.04,2.85,1.13,2.85,2.58v2.59c0,.24.21.43.46.43h5.08c.25,0,.46-.19.46-.43V6.15c0-.14-.06-.26-.18-.35h.01Z"/>
</svg>

After

Width:  |  Height:  |  Size: 417 B

View File

@ -1,13 +1,16 @@
<template>
<div v-if="!item.hidden">
<template v-if="hasOneShowingChild(item, item.children) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
<template
v-if="hasOneShowingChild(item, item.children) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
<span @click="handleMenuClick(onlyOneChild, $event)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
<svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" />
<template #title>
<span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span>
</template>
</el-menu-item>
</span>
</app-link>
</template>
@ -17,14 +20,8 @@
<span class="menu-title" :title="hasTitle(item.meta?.title)">{{ item.meta?.title }}</span>
</template>
<sidebar-item
v-for="(child, index) in item.children"
:key="child.path + index"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
<sidebar-item v-for="(child, index) in item.children" :key="child.path + index" :is-nest="true" :item="child"
:base-path="resolvePath(child.path)" class="nest-menu" />
</el-sub-menu>
</div>
</template>
@ -77,7 +74,23 @@ const hasOneShowingChild = (parent: RouteRecordRaw, children?: RouteRecordRaw[])
return false;
};
const router = useRouter();
// 处理菜单点击,完全控制跳转行为
const handleMenuClick = (route, event) => {
console.log(route, 'route');
if (route.meta.openInNewTab) {
// 完全阻止默认行为和事件冒泡
event.preventDefault();
event.stopPropagation();
console.log('Opening in new tab:', route);
const resolvedRoute = router.resolve({
name: route.name || route.path
});
const fullUrl = new URL(resolvedRoute.href, window.location.origin).href;
window.open(fullUrl, '_blank');
} else {
}
};
const resolvePath = (routePath: string, routeQuery?: string): any => {
if (isExternal(routePath)) {

View File

@ -43,7 +43,7 @@ export const constantRoutes: RouteRecordRaw[] = [
path: "/homeIndex",
name: "HomeIndex",
component: () => import("@/views/homeIndex/index.vue"),
meta: {title: '数据大屏', icon: 'dashboard', preload: true, keepAlive: true },
meta: { title: '数据大屏', icon: '首页1.1', preload: true, keepAlive: true, openInNewTab: true },
},
{
path: '',
@ -54,7 +54,7 @@ export const constantRoutes: RouteRecordRaw[] = [
path: '/index',
component: () => import('@/views/index.vue'),
name: 'Index',
meta: { title: '首页', icon: 'dashboard', affix: true, keepAlive: false }
meta: { title: '首页', icon: '首页1.1', affix: true, keepAlive: false }
}
]
},

View File

@ -6,7 +6,7 @@
<div>设备型号{{ deviceDetail.deviceImei }}</div>
<div class="device-status">设备状态
<span :class="{ online: deviceDetail.onlineStatus === 1, offline: deviceDetail?.onlineStatus === 0 }">
{{ deviceDetail.onlineStatus === 1 ? "在线" : "离线" }}
{{ deviceDetail.onlineStatus === 1 ? '在线' : (deviceDetail.onlineStatus === 2 ? '故障' : '离线') }}
</span>
</div>
<div>电量{{ deviceDetail.batteryPercentage || 0 }}%</div>
@ -15,6 +15,14 @@
<!-- 主体内容区域 -->
<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">
@ -50,8 +58,8 @@
:loading-text="lightModesLoading ? '保存中...' : '保存'"> {{
lightModesLoading ? '保存中' : '保存' }}</el-button>
</div>
<el-button type="danger" class="alarm-btn" @click="forceAlarm" :loading="forceAlarmLoading"
:loading-text="forceAlarmLoading ? '报警中...' : '强制报警'"> {{
<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">
@ -248,7 +256,8 @@ const deviceDetail = ref<DeviceDetail & { typeName: string }>({
address: '',
sendMsg: '',
chargeState: '0',
typeName: ''
typeName: '',
alarmStatus: 0
});
// 保留原有的操作中标志位
const isUpdatingStatus = ref(false);
@ -357,7 +366,7 @@ const handleLaserClick = async () => {
laserMode.value.switchStatus = !targetStatus;
}
} catch (error: any) {
proxy?.$modal.msgError(error.msg) ;
// proxy?.$modal.msgError(error.msg);
// 恢复之前的状态
laserMode.value.switchStatus = !laserMode.value.switchStatus;
} finally { }
@ -420,11 +429,56 @@ const saveBtn = () => {
} else {
lightModesLoading.value = false
proxy?.$modal.msgError(res.msg);
//proxy?.$modal.msgError(res.msg);
}
})
}
// 解除报警
const showClose = async () => {
try {
await proxy?.$modal.confirm('确定要对该设备解除报警?', '提示');
// 2. 准备请求数据
const batchId = generateShortId();
let data = {
deviceIds: [route.params.deviceId],
typeName: deviceDetail.value.typeName,
deviceImeiList: [deviceDetail.value.deviceImei],
batchId: batchId,
instructValue: '0', //强制报警1解除报警0
}
const registerRes = await api.sendAlarmMessage(data);
if (registerRes.code !== 200) {
proxy?.$modal.msgWarning(registerRes.msg)
return
}
// 4. 获取设备状态
let deviceImei = deviceDetail.value.deviceImei
const statusRes = await getDeviceStatus({
functionMode: 2,
batchId,
typeName: 'FunctionAccessBatchStatusRule',
deviceImei,
interval: 500
},
api.deviceRealTimeStatus
);
// 只有当状态为'OK'时才显示成功弹窗
if (statusRes.data.functionAccess === 'OK') {
proxy?.$modal.msgSuccess(statusRes.msg);
await getList();
}
} catch (error: any) {
}
}
// 强制报警
const forceAlarm = async () => {
try {
@ -458,10 +512,12 @@ const forceAlarm = async () => {
// 只有当状态为'OK'时才显示成功弹窗
if (statusRes.data.functionAccess === 'OK') {
proxy?.$modal.msgSuccess(statusRes.msg);
await getList();
}
} catch (error: any) {
proxy?.$modal.msgWarning(error.msg)
// proxy?.$modal.msgWarning(error.msg)
forceAlarmLoading.value = false;
} finally {
forceAlarmLoading.value = false;
@ -586,8 +642,12 @@ const handleDeviceMessage = (msg: any) => {
deviceDetail.value.batteryRemainingTime = deviceState[5]; //续航时间
// getList(); // 重新获取设备详情
if (deviceDetail.value.batteryPercentage < 20 && Number(deviceDetail.value.chargeState) == 0) {
centerDialogVisible.value=true
centerDialogVisible.value = true
}
break;
case 7:
deviceDetail.value.alarmStatus = deviceState[1];
break;
default:
// 其他类型消息(不处理,仅打印)
@ -638,12 +698,13 @@ onUnmounted(() => {
</script>
<style lang="scss" scoped>
.p-2{
.p-2 {
background: rgba(247, 248, 252, 1);
min-height: 100vh;
box-sizing: border-box;
padding: 15px;
}
.device-page {
.header-bar {
border-radius: 8px;
@ -948,4 +1009,23 @@ onUnmounted(() => {
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

@ -130,6 +130,12 @@
<div class="normal red" v-if="scope.row.bindingStatus == 0">未绑定</div>
</template>
</el-table-column>
<el-table-column label="报警状态" align="center" prop="alarmStatus">
<template #default="scope">
<div class="normal red" v-if="scope.row.alarmStatus == 1">报警中</div>
<div class="normal green" v-if="scope.row.alarmStatus == 0">未报警</div>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width">
<template #default="scope">

View File

@ -562,7 +562,7 @@ function SaveMultiData() {
// return api.uploadBoot(formData);
// }
// 上传开机画面根据类型适配不同的上传接口其他类型暂且默认670
function updaeLogo(ids, file, deviceType?: number,) {
function updaeLogo(ids, file, deviceType?: number) {
const selectedRows = getSelectionRows(grid);
let realDeviceType = 670; // 默认670
if (selectedRows.length > 0) {

View File

@ -131,7 +131,11 @@
<div>{{ scope.row.deviceImei }} {{ scope.row.deviceMac }}</div>
</template>
</el-table-column>
<el-table-column label="报警地点" align="center" prop="location" show-overflow-tooltip/>
<el-table-column label="报警地点" align="center" prop="location" show-overflow-tooltip>
<template #default="scope">
<div>{{ scope.row.location && scope.row.location !== '[]' ? scope.row.location : '无' }}</div>
</template>
</el-table-column>
<el-table-column label="报警事项" align="center" prop="deviceAction">
<template #default="scope">
<el-tag type="danger" v-if="scope.row.deviceAction == 0">强制报警</el-tag>

View File

@ -12,7 +12,7 @@
<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-form-item label="设备IMEI" prop="deviceImei">
<el-input v-model="queryParams.deviceImei" placeholder="请输入设备IMEI" clearable />
</el-form-item>
<el-form-item label="设备类型" prop="deviceType">
@ -27,6 +27,13 @@
<el-option label="失效" value="0" />
</el-select>
</el-form-item>
<el-form-item label="在线状态" prop="onlineStatus">
<el-select v-model="queryParams.onlineStatus" placeholder="在线状态" style="margin-left: 10px">
<el-option label="在线" value="1" />
<el-option label="离线" value="0" />
<el-option label="故障" value="2" />
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker v-model="dateRange" value-format="YYYY-MM-DD HH:mm:ss" type="daterange"
range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"
@ -88,7 +95,7 @@
<el-popover placement="right" trigger="click">
<template #reference>
<img v-if="scope.row.devicePic" :src="scope.row.devicePic"
style="width: 40px; height: 40px; cursor: pointer; object-fit: contain"
style="width: 50px; height: 50px; cursor: pointer; object-fit: contain"
class="hover:opacity-80 transition-opacity" />
<img v-else src="@/assets/index/IMG.png" alt="" style="width: 40px; height: 40px;">
</template>
@ -110,7 +117,7 @@
<el-table-column prop="onlineStatus" label="设备状态">
<template #default="scope">
<el-tag :type="scope.row.onlineStatus === 1 ? 'success' : 'info'">
{{ scope.row.onlineStatus === 1 ? '在线' : '离线' }}
{{ scope.row.onlineStatus === 1 ? '在线' : (scope.row.onlineStatus === 2 ? '故障' : '离线') }}
</el-tag>
</template>
</el-table-column>
@ -451,7 +458,8 @@ const initData: PageData<deviceForm, deviceQuery> = {
deviceMac: '',
deviceImei: '',
deviceType: '',
deviceStatus: ''
deviceStatus: '',
onlineStatus:''
},
rules: {
deviceName: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
@ -711,17 +719,17 @@ const httpRequestImg = (parm): Promise<any> => {
return Promise.resolve();
};
const beforeUpload = (file) => {
const isLt2M = file.size / 1024 / 1024 < 2;
//const isLt2M = file.size / 1024 / 1024 < 2;
const isJPG = file.type === 'image/jpeg' || file.type === 'image/png';
if (!isJPG) {
ElMessage.warning('请上传jpg、png格式大小不超过2M的照片');
ElMessage.warning('请上传jpg、png格式');
return false;
}
if (!isLt2M) {
ElMessage.warning('大小不超过2M的照片片');
return false;
}
return isJPG && isLt2M;
// if (!isLt2M) {
// ElMessage.warning('大小不超过2M的照片片');
// return false;
// }
return isJPG;
};
// 文件上传状态改变时触发
const fileUploadChange = (files, fileList) => {
@ -924,7 +932,7 @@ const handleImportSuccess = (response: any) => {
importResult.value.isShow = true;
if (response.data) {
console.log(response.data,'response.data');
console.log(response.data, 'response.data');
importResult.value.succeed = response.data.successCount;
importResult.value.errorSun = response.data.failureCount;

View File

@ -4,9 +4,10 @@
<div class="btn_mounth" :class="{ cur: activeTab == 'month' }" @click="switchTab('month')">近一月</div>
<div class="btn_mounth" :class="{ cur: activeTab === 'halfYear' }" @click="switchTab('halfYear')">近半年</div>
</div>
<div ref="chartContainerRef" class="chartContainer" :class="{ 'show-scroll': showScroll }">
<div ref="chartRef" class="chartRef"></div>
</div>
</div>
</template>
<script setup lang="ts">
@ -14,19 +15,46 @@ import * as echarts from 'echarts';
import { getDeviceUsageFrequency } from '@/api/homeIndex/index';
const chartRef = ref<HTMLDivElement | null>(null);
const chartContainerRef = ref<HTMLDivElement | null>(null);
const activeTab = ref('month');
let myChart: echarts.ECharts | null = null; // 保存图表实例
let dataTimer: NodeJS.Timeout | null = null; // 数据更新定时器
const showScroll = ref(false); // 控制是否显示滚动条
let myChart: echarts.ECharts | null = null;
let dataTimer: NodeJS.Timeout | null = null;
// 根据天数获取数据并更新图表
const fetchDataAndUpdate = (days: number) => {
getDeviceUsageFrequency({ days }).then((res) => {
if (res.code === 200 && res.data && myChart) {
//(转换为图表所需格式
// 模拟数据(根据需求调整数量
const dataCount = activeTab.value === 'month' ? 8 : 25; // 一月8条半年25条
const mockData = Array.from({ length: dataCount }, (_, index) => ({
deviceName: `设备${index + 1}`,
frequency: Math.floor(Math.random() * 100)
}));
const chartData = res.data.map(item => ({
name: item.deviceName,
value: item.frequency
}));
const scrollThreshold = 20;
showScroll.value = chartData.length > scrollThreshold;
// 动态计算图表高度
const baseItemHeight = 20;
const minHeight = 200;
const maxHeight = 600;
let chartHeight;
if (showScroll.value) {
chartHeight = Math.min(chartData.length * baseItemHeight, maxHeight);
} else {
chartHeight = Math.max(chartData.length * baseItemHeight, minHeight);
}
if (chartRef.value) {
chartRef.value.style.height = `${chartHeight}px`;
}
// 更新图表
myChart.setOption({
yAxis: {
@ -36,21 +64,30 @@ const fetchDataAndUpdate = (days: number) => {
data: chartData.map(item => item.value)
}]
});
// 数据更新后,重新调整图表尺寸
setTimeout(() => {
if (myChart) {
myChart.resize();
}
}, 0);
}
}).catch(err => {
console.error('获取数据失败', err);
console.error(err);
});
};
// 切换标签逻辑
const switchTab = (tab: string) => {
activeTab.value = tab;
// 根据标签切换天数(近一月=30天近半年=180天
const days = tab === 'month' ? 30 : 180;
fetchDataAndUpdate(days);
// 切换标签后重新启动定时器
startDataTimer();
// 重置滚动位置
if (chartContainerRef.value) {
chartContainerRef.value.scrollTop = 10;
}
};
// 开始数据定时器
@ -58,7 +95,6 @@ const startDataTimer = () => {
if (dataTimer) {
clearInterval(dataTimer);
}
// 每300秒5分钟更新一次数据
dataTimer = setInterval(() => {
const days = activeTab.value === 'month' ? 30 : 180;
fetchDataAndUpdate(days);
@ -81,18 +117,19 @@ const initChart = () => {
const option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' } // 柱状图建议使用阴影指示器
axisPointer: { type: 'shadow' }
},
grid: {
left: '5%',
right: '10%',
bottom: '3%',
top: '20%',
right: '5%',
bottom: '2%',
top: '12%',
containLabel: true
},
yAxis: {
type: 'category',
data: [], // 初始空数据
inverse: true,
data: [],
axisLine: {
lineStyle: {
color: '#1e3a8a',
@ -100,26 +137,37 @@ const initChart = () => {
}
},
axisLabel: {
color: '#DEEFFF'
color: '#DEEFFF',
fontSize: 12
},
axisTick: {
alignWithLabel: true
}
},
xAxis: {
type: 'value',
axisLine: { show: false },
axisLabel: { show: false },
splitLine: { show: false }
splitLine: {
show: true,
lineStyle: {
color: 'rgba(30, 58, 138, 0.3)',
type: 'dashed'
}
}
},
series: [{
name: '使用频次',
type: 'bar',
data: [], // 初始空数据
barWidth: '9px',
data: [],
barWidth: '12px',
stack: 'total',
label: {
show: true,
position: 'right',
valueAnimation: true,
color: '#DEEFFF'
color: '#DEEFFF',
fontSize: 11
},
itemStyle: {
color: new echarts.graphic.LinearGradient(
@ -130,15 +178,14 @@ const initChart = () => {
]
),
borderRadius: 4
}
},
barGap: '30%',
barCategoryGap: '40%'
}]
};
myChart.setOption(option);
// 初始化数据
fetchDataAndUpdate(30);
// 启动定时器
startDataTimer();
};
@ -155,11 +202,8 @@ onMounted(() => {
});
onUnmounted(() => {
// 清除定时器
clearDataTimer();
// 移除事件监听
window.removeEventListener('resize', handleResize);
// 销毁图表实例
if (myChart) {
myChart.dispose();
myChart = null;
@ -170,7 +214,8 @@ onUnmounted(() => {
<style scoped lang="scss">
.vchartPage {
margin-top: 4.9vh;
position: relative; // 确保按钮定位正确
position: relative;
height: 100%;
}
.btn_mounth_box {
@ -182,8 +227,8 @@ onUnmounted(() => {
}
.btn_mounth {
width:4.5vw;
height:4.5vh;
width: 4.5vw;
height: 4.5vh;
background: url(@/assets/homeIndex/btn.png) no-repeat;
background-size: 100% 100%;
text-align: center;
@ -191,7 +236,7 @@ onUnmounted(() => {
color: #fff;
font-size: 0.8vw;
cursor: pointer;
margin-left: 0.5vw; // 按钮间距
margin-left: 0.5vw;
}
.cur {
@ -199,8 +244,49 @@ onUnmounted(() => {
background-size: 100% 100%;
}
.chartContainer {
width: 100%;
height: auto;
overflow-y: hidden;
overflow-x: hidden;
position: relative;
// margin-top: 2vh;
transition: all 0.3s ease;
// 当需要显示滚动条时的样式
&.show-scroll {
height: 24vh;
overflow-y: auto;
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(7, 104, 212, 0.8);
border-radius: 4px;
border: 2px solid transparent;
background-clip: content-box;
}
&::-webkit-scrollbar-thumb:hover {
background-color: rgba(7, 104, 212, 1);
}
&::-webkit-scrollbar-track {
background-color: rgba(30, 58, 138, 0.1);
border-radius: 4px;
}
&::-webkit-scrollbar-track:hover {
background-color: rgba(30, 58, 138, 0.2);
}
}
}
.chartRef {
width: 100%;
height: 24vh;
min-height: 100px;
box-sizing: border-box;
}
</style>

View File

@ -16,7 +16,7 @@
<div class="item-cell alarm-event">
{{ getEventName(item.deviceAction) }}
</div>
<div class="item-cell loaction">{{ item.location }}</div>
<div class="item-cell loaction"> {{ item.location && item.location !== '[]' ? item.location : '无' }}</div>
</div>
<div v-for="(item, index) in displayData" :key="`second-${getKey(item, index)}`" class="alarm-item">
<div class="item-cell">{{ item.startTime }}</div>
@ -25,7 +25,7 @@
<div class="item-cell alarm-event">
{{ getEventName(item.deviceAction) }}
</div>
<div class="item-cell loaction">{{ item.location }}</div>
<div class="item-cell loaction"> {{ item.location && item.location !== '[]' ? item.location : '无' }}</div>
</div>
</div>
</div>