设备加载 #91

Merged
pc8xi2fbj merged 1 commits from zhanghongwei_branch into develop 4 weeks ago

@ -20,12 +20,19 @@ import java.util.List;
@RequestMapping("/api/alerts")
@RequiredArgsConstructor
@Tag(name = "告警管理接口")
public class AlertController {
private final AlertRepository alertRepository;
@GetMapping("/test")
@PreAuthorize("hasAnyRole('SUPER_ADMIN','AREA_ADMIN', 'REPAIRMAN')")
public ResultVO<String> testAuth() {
return ResultVO.success("权限验证通过");
}
@GetMapping("/history")
@PreAuthorize("hasAnyRole('ADMIN', 'REPAIRMAN')")
@PreAuthorize("hasAnyRole('SUPER_ADMIN','AREA_ADMIN', 'REPAIRMAN')")
@Operation(summary = "分页查询告警历史(支持多条件筛选)")
public ResultVO<List<Alert>> getAlertHistory(
@Parameter(description = "设备ID可选") @RequestParam(required = false) String deviceId,
@ -58,7 +65,7 @@ public class AlertController {
*
*/
@GetMapping("/pending")
@PreAuthorize("hasAnyRole('ADMIN', 'REPAIRMAN')")
@PreAuthorize("hasAnyRole('SUPER_ADMIN','AREA_ADMIN', 'REPAIRMAN')")
public ResultVO<List<Alert>> getPendingAlerts(
@Parameter(description = "区域ID可选") @RequestParam(required = false) String areaId) {
List<Alert> pendingAlerts = areaId != null

@ -142,7 +142,7 @@ public class DeviceController {
*
* /
*/
@GetMapping("/by-status")
/* @GetMapping("/by-status")
@Operation(summary = "按状态查询设备", description = "根据设备状态筛选设备列表,可选区域筛选")
public ResponseEntity<ResultVO<List<Device>>> getDevicesByStatus(
@RequestParam String status,
@ -155,13 +155,13 @@ public class DeviceController {
} catch (Exception e) {
return ResponseEntity.ok(ResultVO.error(500, "按状态查询设备失败: " + e.getMessage()));
}
}
}*/
/**
*
* /
*/
@GetMapping("/by-type")
/* @GetMapping("/by-type")
@Operation(summary = "按类型查询设备", description = "根据设备类型筛选设备列表,可选区域筛选")
public ResponseEntity<ResultVO<List<Device>>> getDevicesByType(
@RequestParam String deviceType,
@ -174,6 +174,6 @@ public class DeviceController {
} catch (Exception e) {
return ResponseEntity.ok(ResultVO.error(500, "按类型查询设备失败: " + e.getMessage()));
}
}
}*/
}

@ -22,6 +22,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@ -104,6 +105,7 @@ public class DeviceStatusController {
*
*/
@GetMapping("/by-status")
@PreAuthorize("hasAnyRole('SUPER_ADMIN','AREA_ADMIN')")
@Operation(summary = "按状态查询设备", description = "根据设备状态筛选设备列表,可选区域筛选")
public ResponseEntity<ResultVO<List<Device>>> getDevicesByStatus(
@RequestParam String status,
@ -122,6 +124,7 @@ public class DeviceStatusController {
*
*/
@GetMapping("/by-type")
@PreAuthorize("hasAnyRole('SUPER_ADMIN','AREA_ADMIN')")
@Operation(summary = "按类型查询设备", description = "根据设备类型筛选设备列表,可选区域筛选")
public ResponseEntity<ResultVO<List<Device>>> getDevicesByType(
@RequestParam String deviceType,
@ -137,6 +140,7 @@ public class DeviceStatusController {
}
@GetMapping("/status-count")
@PreAuthorize("hasAnyRole('SUPER_ADMIN','AREA_ADMIN')")
@Operation(summary = "设备状态数量统计", description = "统计各状态设备数量")
public ResponseEntity<ResultVO<Map<String, Object>>> getDeviceStatusCount(
@RequestParam(required = false) String areaId,

@ -1,4 +1,3 @@
// com/campus/water/service/DeviceStatusServiceImpl.java
package com.campus.water.service;
import com.campus.water.entity.Device;
@ -27,6 +26,7 @@ public class DeviceStatusServiceImpl implements DeviceStatusService {
return false;
}
device.setStatus(request.getStatus());
device.setRemark(request.getRemark());
deviceRepository.save(device);
return true;
@ -81,7 +81,7 @@ public class DeviceStatusServiceImpl implements DeviceStatusService {
@Override
public List<Device> getDevicesByStatusWithArea(String status, String areaId) {
try {
Device.DeviceStatus targetStatus = Device.DeviceStatus.valueOf(status.toUpperCase());
Device.DeviceStatus targetStatus = Device.DeviceStatus.valueOf(status);
if (areaId != null && !areaId.isEmpty()) {
return deviceRepository.findByStatusAndAreaId(targetStatus, areaId);
} else {
@ -101,7 +101,7 @@ public class DeviceStatusServiceImpl implements DeviceStatusService {
@Override
public List<Device> getDevicesByTypeWithArea(String deviceType, String areaId) {
try {
Device.DeviceType targetType = Device.DeviceType.valueOf(deviceType.toUpperCase());
Device.DeviceType targetType = Device.DeviceType.valueOf(deviceType);
if (areaId != null && !areaId.isEmpty()) {
return deviceRepository.findByDeviceTypeAndAreaId(targetType, areaId);
} else {
@ -115,25 +115,26 @@ public class DeviceStatusServiceImpl implements DeviceStatusService {
@Override
public Map<String, Object> getDeviceStatusCount(String areaId, String deviceType) {
Device.DeviceType targetType = Device.DeviceType.valueOf(deviceType);
return Map.of(
"online", deviceRepository.countByStatusAndAreaIdAndDeviceType(Device.DeviceStatus.online, areaId, targetType),
"offline", deviceRepository.countByStatusAndAreaIdAndDeviceType(Device.DeviceStatus.offline, areaId, targetType),
"fault", deviceRepository.countByStatusAndAreaIdAndDeviceType(Device.DeviceStatus.fault, areaId, targetType)
);
try {
Device.DeviceType targetType = Device.DeviceType.valueOf(deviceType);
return Map.of(
"online", deviceRepository.countByStatusAndAreaIdAndDeviceType(Device.DeviceStatus.online, areaId, targetType),
"offline", deviceRepository.countByStatusAndAreaIdAndDeviceType(Device.DeviceStatus.offline, areaId, targetType),
"fault", deviceRepository.countByStatusAndAreaIdAndDeviceType(Device.DeviceStatus.fault, areaId, targetType)
);
} catch (IllegalArgumentException e) {
log.error("设备类型枚举转换失败,类型值:{}", deviceType, e);
throw new RuntimeException("无效的设备类型:" + deviceType);
}
}
@Override
public List<Device> getOfflineDevicesExceedThreshold(Integer thresholdMinutes, String areaId) {
// 由于没有last_active_time此处逻辑需调整
// 方案1若设备有最近操作时间可用作替代
// 方案2仅返回状态为offline的设备不判断时间
return deviceRepository.findByAreaIdAndStatus(areaId, Device.DeviceStatus.offline);
}
@Override
public void autoDetectOfflineDevices(Integer thresholdMinutes) {
// 同理无last_active_time时无法通过时间判断可注释或简化逻辑
log.info("自动检测离线设备(不执行时间判断,仅依赖手动标记)");
}
}

@ -3,18 +3,31 @@ import axios from 'axios'
export const DeviceStatusApi = {
// 获取设备状态列表 - 修改为匹配后端实际接口
getDevicesByStatus: async (status: string, areaId?: string, deviceType?: string) => {
try {
const params: any = { status }
if (areaId) params.areaId = areaId
if (deviceType) params.deviceType = deviceType
getDevicesByType: async (deviceType: string, areaId?: string) => {
try {
const params: any = { deviceType }
if (areaId) params.areaId = areaId
const response = await axios.get('/api/web/device-status/by-status', { params })
return response.data
} catch (error) {
throw new Error(`获取设备列表失败: ${error}`)
}
},
// 注意:这里是调用设备状态管理的接口
const response = await axios.get('/api/web/device-status/by-type', { params })
return response.data
} catch (error) {
throw new Error(`获取设备列表失败: ${error}`)
}
},
// 按状态查询设备
getDevicesByStatus: async (status: string, areaId?: string) => {
try {
const params: any = { status }
if (areaId) params.areaId = areaId
const response = await axios.get('/api/web/device-status/by-status', { params })
return response.data
} catch (error) {
throw new Error(`获取设备列表失败: ${error}`)
}
},
// 标记设备在线
markDeviceOnline: async (deviceId: string) => {

@ -61,7 +61,7 @@ export async function request<T>(
let responseText = ''
try {
responseText = await response.text()
console.log('📥 响应内容:', responseText)
//console.log('📥 响应内容:', responseText)
} catch (e) {
console.log('📥 无法读取响应文本')
}

@ -10,91 +10,65 @@
<div class="stats-grid">
<div class="stats-card">
<div class="stats-icon">📊</div>
<div class="stats-number">156</div>
<div class="stats-number">{{ stats.totalDevices }}</div>
<div class="stats-label">设备总数</div>
</div>
<div class="stats-card">
<div class="stats-icon">🟢</div>
<div class="stats-number">142</div>
<div class="stats-number">{{ stats.onlineDevices }}</div>
<div class="stats-label">在线数量</div>
</div>
<div class="stats-card">
<div class="stats-icon"></div>
<div class="stats-number">8</div>
<div class="stats-number">{{ stats.alertDevices }}</div>
<div class="stats-label">告警设备</div>
</div>
<div class="stats-card">
<div class="stats-icon">📋</div>
<div class="stats-number">12</div>
<div class="stats-number">{{ stats.pendingWorkOrders }}</div>
<div class="stats-label">待处理工单</div>
</div>
<div class="stats-card">
<div class="stats-icon">🔴</div>
<div class="stats-number">1</div>
<div class="stats-number">{{ stats.offlineDevices }}</div>
<div class="stats-label">离线设备</div>
</div>
</div>
<!-- 警告通知 -->
<div class="alert-warning">
<div v-if="latestAlert" class="alert-warning">
<div class="alert-icon"></div>
<div class="alert-content">
<span>设备 A08制水机 异常请关注</span>
<span>设备 {{ latestAlert.deviceId }} 异常请关注</span>
</div>
<div class="alert-close">×</div>
</div>
<!-- 主要内容区域 -->
<div class="content-grid">
<!-- 左侧设备状态分布 -->
<!-- 主要内容区域 - 修改为单列布局 -->
<div class="content-single-column">
<!-- 最新告警 - 居中显示 -->
<div class="content-card">
<h3 class="card-title">设备状态分布</h3>
<div class="chart-container">
<div class="pie-chart">
<div class="pie-background"></div>
<div class="pie-segment" style="--percentage: 75;"></div>
<div class="pie-center">
<span class="pie-value">75</span>
</div>
</div>
<div class="chart-info">
<div class="chart-title">矿化水优良情况</div>
<div class="chart-subtitle">所有设备总体情况</div>
<h3 class="card-title">最新告警</h3>
<div class="alert-list">
<div v-for="alert in recentAlerts" :key="alert.id" class="alert-item">
<div class="alert-text">{{ alert.deviceId }}{{ alert.message }}</div>
<div :class="['alert-level', alert.level.toLowerCase()]">{{ formatAlertLevel(alert.level) }}</div>
</div>
</div>
</div>
<!-- 右侧最新告警和工单统计 -->
<div class="right-column">
<!-- 最新告警 -->
<div class="content-card">
<h3 class="card-title">最新告警</h3>
<div class="alert-list">
<div class="alert-item">
<div class="alert-text">A08制水机TDS值超标</div>
<div class="alert-level critical">紧急</div>
<!-- 工单状态统计 -->
<div class="content-card">
<h3 class="card-title">工单状态统计</h3>
<div class="chart-placeholder">
<div class="placeholder-text">
<div style="font-size: 16px; margin-bottom: 8px;">Axhub Charts</div>
<div style="font-size: 24px; margin-bottom: 12px;">柱状图</div>
<div style="color: #666; font-size: 12px;">
通过Group内data和config中继器可更改数据及配置
</div>
<div class="alert-divider"></div>
<div class="alert-item">
<div class="alert-text">C11制水机离线</div>
<div class="alert-level warning">警告</div>
</div>
</div>
</div>
<!-- 工单状态统计 -->
<div class="content-card">
<h3 class="card-title">工单状态统计</h3>
<div class="chart-placeholder">
<div class="placeholder-text">
<div style="font-size: 16px; margin-bottom: 8px;">Axhub Charts</div>
<div style="font-size: 24px; margin-bottom: 12px;">柱状图</div>
<div style="color: #666; font-size: 12px;">
通过Group内data和config中继器可更改数据及配置
</div>
<div style="color: #666; font-size: 12px;">
详情访问https://axhub.im/charts
</div>
<div style="color: #666; font-size: 12px;">
详情访问https://axhub.im/charts
</div>
</div>
</div>
@ -104,6 +78,171 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { request } from '@/api/request'
import { useAuthStore } from '@/stores/auth'
import type { ResultVO } from '@/api/types/auth'
//
interface StatsData {
totalDevices: number
onlineDevices: number
offlineDevices: number
alertDevices: number
pendingWorkOrders: number
}
interface Alert {
id: string
deviceId: string
message: string
level: string
timestamp: string
}
//
const stats = ref<StatsData>({
totalDevices: 0,
onlineDevices: 0,
offlineDevices: 0,
alertDevices: 0,
pendingWorkOrders: 0
})
const recentAlerts = ref<Alert[]>([])
const latestAlert = ref<Alert | null>(null)
const onlinePercentage = ref<number>(0)
// store
const router = useRouter()
const authStore = useAuthStore()
//
const formatAlertLevel = (level: string): string => {
const levelMap: Record<string, string> = {
'CRITICAL': '紧急',
'ERROR': '错误',
'WARNING': '警告'
}
return levelMap[level] || level
}
//
const fetchStatsData = async () => {
try {
const token = authStore.token
// token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
//
const statusCountResult = await request<ResultVO<Record<string, number>>>(
'/api/web/device-status/status-count',
{ method: 'GET' }
)
if (statusCountResult.code === 200 && statusCountResult.data) {
const data = statusCountResult.data
stats.value.onlineDevices = data.online || 0
stats.value.offlineDevices = data.offline || 0
stats.value.alertDevices = data.fault || 0
stats.value.totalDevices = (data.online || 0) + (data.offline || 0) + (data.fault || 0)
// 线
if (stats.value.totalDevices > 0) {
onlinePercentage.value = Math.round((stats.value.onlineDevices / stats.value.totalDevices) * 100)
} else {
onlinePercentage.value = 0
}
}
//
const workOrderResult = await request<ResultVO<any[]>>(
'/api/work-orders/by-status?status=pending',
{ method: 'GET' }
)
if (workOrderResult.code === 200 && workOrderResult.data) {
stats.value.pendingWorkOrders = workOrderResult.data.length
}
} catch (error: any) {
console.error('获取统计数据失败:', error)
const errorMsg = error.message?.includes('401')
? '登录已过期,请重新登录'
: error.message?.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '获取数据失败,请稍后重试'
//
if (error.message?.includes('401')) {
authStore.logout()
router.push('/login')
}
}
}
//
const fetchAlertData = async () => {
try {
const token = authStore.token
// token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
//
const alertResult = await request<ResultVO<Alert[]>>(
'/api/alerts/pending',
{ method: 'GET' }
)
if (alertResult.code === 200 && alertResult.data) {
recentAlerts.value = alertResult.data.slice(0, 5) // 5
if (recentAlerts.value.length > 0) {
latestAlert.value = recentAlerts.value[0] || null;
}
}
} catch (error: any) {
console.error('获取告警数据失败:', error)
// 403
if (error.message?.includes('403')) {
console.warn('当前用户无权限访问告警数据')
//
recentAlerts.value = []
latestAlert.value = null
return
}
const errorMsg = error.message?.includes('401')
? '登录已过期,请重新登录'
: error.message?.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '获取数据失败,请稍后重试'
//
if (error.message?.includes('401')) {
authStore.logout()
router.push('/login')
}
}
}
//
onMounted(() => {
fetchStatsData()
fetchAlertData()
})
</script>
<style scoped>
@ -127,7 +266,6 @@
font-size: 14px;
}
/* 其他样式保持不变,但移除最外层的 padding */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
@ -165,5 +303,117 @@
color: #666;
}
/* 其他样式... 保持原有的所有样式 */
</style>
.alert-warning {
background: #fff8e6;
border-left: 4px solid #ffc107;
padding: 16px;
display: flex;
align-items: center;
margin-bottom: 24px;
border-radius: 4px;
}
.alert-icon {
font-size: 20px;
margin-right: 12px;
color: #ff9800;
}
.alert-content {
flex: 1;
font-size: 14px;
color: #333;
}
.alert-close {
font-size: 20px;
cursor: pointer;
color: #999;
}
/* 修改为单列布局 */
.content-single-column {
display: flex;
flex-direction: column;
gap: 24px;
}
.content-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.card-title {
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin-bottom: 16px;
text-align: center; /* 标题居中 */
}
.alert-list {
display: flex;
flex-direction: column;
}
.alert-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
}
.alert-text {
font-size: 14px;
color: #333;
}
.alert-level {
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
font-weight: 500;
}
.alert-level.critical {
background: #ffebee;
color: #c62828;
}
.alert-level.error {
background: #ffebee;
color: #c62828;
}
.alert-level.warning {
background: #fff8e1;
color: #ff8f00;
}
.alert-divider {
height: 1px;
background: #f0f0f0;
}
.chart-placeholder {
height: 200px;
display: flex;
align-items: center;
justify-content: center;
background: #f8f9fa;
border-radius: 4px;
}
.placeholder-text {
text-align: center;
color: #666;
}
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>

@ -204,8 +204,9 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth' // auth store
import { request } from '@/api/request' //
import { useAuthStore } from '@/stores/auth'
import { request } from '@/api/request'
import type { ResultVO } from '@/api/types/auth'
//
type DeviceStatus = 'online' | 'offline' | 'fault'
@ -230,7 +231,7 @@ const selectedStatus = ref('') // 状态筛选值
const currentPage = ref(1)
const pageSize = 10 //
const router = useRouter()
const authStore = useAuthStore() // auth store
const authStore = useAuthStore() // auth store
//
const showAddModal = ref(false)
@ -246,7 +247,7 @@ const newDevice = ref({
deviceName: '',
areaId: '市区',
installLocation: '',
deviceType: 'WATER_MAKER'
deviceType: 'water_maker'
})
const offlineReason = ref('')
@ -256,48 +257,48 @@ const faultInfo = ref({
})
//
const loadDevices = async (): Promise<WaterMakerDevice[]> => {
const loadDevices = async (): Promise<void> => {
try {
//
if (!authStore.isLoggedIn) {
// tokenisLoggedIn
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return []
return
}
const statuses = ['online', 'offline', 'fault']
const allDevices: WaterMakerDevice[] = []
for (const status of statuses) {
// 使token
const result = await request<{
code: number
message: string
data: any[]
}>(`/api/device/status/${status}?deviceType=water_maker`)
if (result.code === 200 && result.data && Array.isArray(result.data)) {
allDevices.push(...result.data.map((item: any) => ({
deviceId: item.deviceId,
deviceName: item.deviceName,
deviceType: item.deviceType,
areaId: item.areaId,
installLocation: item.installLocation,
status: item.status,
lastHeartbeatTime: item.lastHeartbeatTime
})))
}
console.log('开始加载制水机设备数据...')
//
const result = await request<ResultVO<WaterMakerDevice[]>>(
`/api/web/device-status/by-type?deviceType=water_maker`,
{ method: 'GET' }
)
console.log('制水机设备请求结果:', result)
if (result.code === 200 && result.data && Array.isArray(result.data)) {
console.log(`获取到${result.data.length}个制水机设备`)
devices.value = result.data.map((item: any) => ({
deviceId: item.deviceId,
deviceName: item.deviceName,
deviceType: item.deviceType,
areaId: item.areaId,
installLocation: item.installLocation,
status: item.status,
lastHeartbeatTime: item.lastHeartbeatTime
}))
}
devices.value = allDevices
return allDevices
if (devices.value.length === 0) {
console.log('提示:未找到任何制水机设备,请确认是否已添加设备')
}
} catch (error) {
console.error('加载设备数据失败:', error)
//
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
return []
}
}
@ -368,10 +369,15 @@ const showFaultModalFunc = (deviceId: string) => {
// 线
const confirmOffline = async () => {
try {
const result = await request<{
code: number
message: string
}>(`/api/device/${currentDeviceId.value}/offline`, {
// token
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
const result = await request<ResultVO<boolean>>(`/api/web/device-status/${currentDeviceId.value}/offline`, {
method: 'POST',
body: JSON.stringify({ reason: offlineReason.value })
})
@ -383,9 +389,13 @@ const confirmOffline = async () => {
}
showOfflineReasonModal.value = false
offlineReason.value = ''
alert('设备已标记为离线')
} else {
alert(`设置设备离线失败: ${result.message}`)
}
} catch (error) {
console.error('设置设备离线失败:', error)
alert('设置设备离线失败')
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
@ -396,10 +406,15 @@ const confirmOffline = async () => {
//
const confirmFault = async () => {
try {
const result = await request<{
code: number
message: string
}>(`/api/device/${currentDeviceId.value}/fault`, {
// token
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
const result = await request<ResultVO<boolean>>(`/api/web/device-status/${currentDeviceId.value}/fault`, {
method: 'POST',
body: JSON.stringify(faultInfo.value)
})
@ -411,9 +426,13 @@ const confirmFault = async () => {
}
showFaultModal.value = false
faultInfo.value = { faultType: '', description: '' }
alert('设备已标记为故障')
} else {
alert(`设置设备故障失败: ${result.message}`)
}
} catch (error) {
console.error('设置设备故障失败:', error)
alert('设置设备故障失败')
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
@ -421,68 +440,72 @@ const confirmFault = async () => {
}
}
//
const updateDeviceStatus = async (deviceId: string, status: string, remark: string = '') => {
// 线
const updateDeviceStatus = async (deviceId: string, status: string) => {
try {
let result;
// token
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
let url = ''
let body = {}
switch (status) {
case 'online':
result = await request<{
code: number
message: string
}>(`/api/device/${deviceId}/online`, {
method: 'POST'
})
url = `/api/web/device-status/${deviceId}/online`
break
case 'offline':
result = await request<{
code: number
message: string
}>(`/api/device/${deviceId}/offline`, {
method: 'POST',
body: JSON.stringify({ reason: remark })
})
url = `/api/web/device-status/${deviceId}/offline`
body = { reason: '手动设置为离线' }
break
case 'fault':
result = await request<{
code: number
message: string
}>(`/api/device/${deviceId}/fault`, {
method: 'POST',
body: JSON.stringify({
faultType: 'MANUAL_FAULT',
description: remark || '手动设置故障'
})
})
url = `/api/web/device-status/${deviceId}/fault`
body = { faultType: 'MANUAL', description: '手动设置为故障' }
break
default:
throw new Error('不支持的状态类型')
}
const result = await request<ResultVO<boolean>>(url, {
method: 'POST',
body: JSON.stringify(body)
})
if (result.code === 200) {
//
const device = devices.value.find(d => d.deviceId === deviceId)
if (device) {
device.status = status as DeviceStatus
}
return true
alert(`设备已标记为${formatStatus(status as DeviceStatus)}`)
} else {
throw new Error(result.message || '操作失败')
alert(`更新设备状态失败: ${result.message}`)
}
} catch (error) {
console.error('更新设备状态失败:', error)
alert('更新设备状态失败')
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
throw error
}
}
//
const addDevice = async () => {
try {
// token
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
//
const deviceToAdd = {
deviceId: newDevice.value.deviceId,
@ -490,66 +513,46 @@ const addDevice = async () => {
areaId: newDevice.value.areaId,
installLocation: newDevice.value.installLocation,
deviceType: newDevice.value.deviceType
};
}
// 使token
const result = await request<{
code: number
message: string
}>('/api/web/device/add', {
const result = await request<ResultVO<any>>('/api/web/device/add', {
method: 'POST',
body: JSON.stringify(deviceToAdd)
})
if (result.code === 200) {
//
await loadDevices();
await loadDevices()
//
showAddModal.value = false;
showAddModal.value = false
newDevice.value = {
deviceId: '',
deviceName: '',
areaId: '市区',
installLocation: '',
deviceType: 'water_maker'
};
}
console.log('设备添加成功');
alert('设备添加成功')
} else {
console.error('设备添加失败:', result.message);
alert(`设备添加失败: ${result.message}`)
}
} catch (error) {
console.error('添加设备失败:', error);
console.error('添加设备失败:', error)
alert('添加设备失败')
if ((error as Error).message.includes('401')) {
authStore.logout()
router.push('/login')
}
}
};
}
//
onMounted(async () => {
console.log('🚀 开始加载设备数据...')
//
if (!authStore.isLoggedIn) {
router.push('/login')
return
}
try {
const result = await loadDevices()
console.log('🌐 API返回 data:', result)
if (result.length === 0) {
console.log('⚠️ 数据库中无设备数据')
} else {
console.log('✅ 成功加载设备数据:', result)
}
} catch (error) {
console.error('❌ 加载设备数据失败:', error)
}
await loadDevices()
})
</script>
@ -850,4 +853,4 @@ onMounted(async () => {
min-width: auto;
}
}
</style>
</style>

@ -63,7 +63,7 @@
</tr>
</thead>
<tbody>
<tr v-for="device in filteredDevices" :key="device.id">
<tr v-for="device in paginatedDevices" :key="device.id">
<td>{{ device.id }}</td>
<td>供水机</td> <!-- 固定显示供水机机型 -->
<td>{{ device.area }}</td>
@ -78,7 +78,7 @@
<button class="btn-view" @click="viewDevice(device.id)"></button>
</td>
</tr>
<tr v-if="filteredDevices.length === 0">
<tr v-if="paginatedDevices.length === 0">
<td colspan="7" class="no-data">暂无设备数据</td> <!-- colspan从6改为7 -->
</tr>
</tbody>
@ -95,7 +95,7 @@
上一页
</button>
<span class="page-info">
{{ currentPage }} / {{ totalPages }}
{{ currentPage }} / {{ totalPages }} ( {{ filteredDevices.length }} 条记录)
</span>
<button
class="page-btn"
@ -111,7 +111,9 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { DeviceStatusApi } from '@/api/deviceStatus'
import { request } from '@/api/request'
import type { ResultVO } from '@/api/types/auth'
import { useAuthStore } from '@/stores/auth'
//
type DeviceStatus = 'online' | 'offline' | 'warning' | 'error'
@ -133,33 +135,54 @@ const selectedStatus = ref('') // 状态筛选值
const currentPage = ref(1)
const pageSize = 10 //
const router = useRouter()
const authStore = useAuthStore()
//
const loadWaterSuppliers = async () => {
try {
const params = {
status: selectedStatus.value,
areaId: selectedArea.value,
deviceType: 'water_supply' //
// token
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
const response = await DeviceStatusApi.getDevicesByStatus(
params.status || 'all',
params.areaId,
params.deviceType
)
//
devices.value = response.data.map((device: any) => ({
id: device.deviceId,
area: device.areaId,
location: device.installLocation,
status: device.status,
lastUploadTime: device.lastUpdateTime
}))
//
const params = new URLSearchParams();
if (selectedStatus.value && selectedStatus.value !== '') {
params.append('status', selectedStatus.value);
}
if (selectedArea.value && selectedArea.value !== '') {
params.append('areaId', selectedArea.value);
}
params.append('deviceType', 'water_supply'); //
const queryString = params.toString();
const url = `/api/web/device-status/by-type${queryString ? `?${queryString}` : ''}`;
//
const response = await request<ResultVO<any[]>>(url, { method: 'GET' });
if (response.code === 200 && response.data && Array.isArray(response.data)) {
devices.value = response.data.map((device: any) => ({
id: device.deviceId,
area: device.areaId,
location: device.installLocation,
status: device.status,
lastUploadTime: device.lastHeartbeatTime || device.lastUpdateTime || '-'
}));
} else {
console.error('获取供水机列表失败:', response.message);
alert('获取供水机列表失败: ' + response.message);
}
} catch (error) {
console.error('加载供水机列表失败:', error)
alert('获取供水机列表失败,请检查网络连接')
console.error('加载供水机列表失败:', error);
alert('获取供水机列表失败,请检查网络连接');
if (error instanceof Error && error.message.includes('401')) {
authStore.logout();
router.push('/login');
}
}
}
@ -178,6 +201,12 @@ const filteredDevices = computed(() => {
})
//
const paginatedDevices = computed(() => {
const start = (currentPage.value - 1) * pageSize
const end = start + pageSize
return filteredDevices.value.slice(start, end)
})
const totalPages = computed(() => {
return Math.ceil(filteredDevices.value.length / pageSize)
})

@ -12,9 +12,9 @@
<!-- 工单号/设备ID搜索 -->
<div class="filter-item search-item">
<label>搜索</label>
<input
type="text"
v-model="searchKeyword"
<input
type="text"
v-model="searchKeyword"
class="search-input"
placeholder="输入工单号或设备ID搜索"
@input="handleSearch"
@ -34,9 +34,9 @@
<!-- 日期筛选 -->
<div class="filter-item">
<label>创建日期</label>
<input
type="date"
v-model="filterForm.createDate"
<input
type="date"
v-model="filterForm.createDate"
class="filter-input"
@change="handleFilter"
>
@ -61,7 +61,7 @@
</tr>
</thead>
<tbody>
<tr v-for="order in filteredOrders" :key="order.id">
<tr v-for="order in paginatedOrders" :key="order.id">
<td>{{ order.orderNo }}</td>
<td>
<div class="device-info">
@ -81,8 +81,10 @@
<button class="btn-assign" @click="openAssignDialog(order)"></button>
</td>
</tr>
<tr v-if="filteredOrders.length === 0">
<td colspan="7" class="no-data">暂无超时未抢工单</td>
<tr v-if="paginatedOrders.length === 0">
<td colspan="7" class="no-data">
{{ loading ? '正在加载数据...' : '暂无超时未抢工单' }}
</td>
</tr>
</tbody>
</table>
@ -90,18 +92,18 @@
<!-- 分页控件 -->
<div class="pagination">
<button
class="page-btn"
<button
class="page-btn"
:disabled="currentPage === 1"
@click="currentPage--"
>
上一页
</button>
<span class="page-info">
{{ currentPage }} / {{ totalPages }}
{{ currentPage }} / {{ totalPages }} ( {{ filteredOrders.length }} 条记录)
</span>
<button
class="page-btn"
<button
class="page-btn"
:disabled="currentPage === totalPages"
@click="currentPage++"
>
@ -153,15 +155,15 @@
</div>
<div class="form-group">
<label class="form-label required">选择维修人员</label>
<select
v-model="selectedStaffId"
<select
v-model="selectedStaffId"
class="form-select"
@change="handleStaffChange"
>
<option value="">请选择维修人员</option>
<option
v-for="staff in filteredStaff"
:key="staff.id"
<option
v-for="staff in filteredStaff"
:key="staff.id"
:value="staff.id"
>
{{ staff.name }} ({{ staff.phone }})
@ -170,8 +172,8 @@
</div>
<div class="form-group">
<label class="form-label">派单备注</label>
<textarea
v-model="assignRemark"
<textarea
v-model="assignRemark"
class="form-textarea"
placeholder="请输入派单备注信息"
rows="3"
@ -180,8 +182,8 @@
</div>
<div class="dialog-footer">
<button class="btn-cancel" @click="closeAssignDialog"></button>
<button
class="btn-confirm"
<button
class="btn-confirm"
@click="confirmAssign"
:disabled="!selectedStaffId"
>
@ -194,8 +196,10 @@
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { request } from '@/api/request'
import { useAuthStore } from '@/stores/auth'
//
type OrderStatus = 'timeout' | 'pending' | 'processing' | 'reviewing' | 'completed'
@ -223,47 +227,13 @@ interface MaintenanceStaff {
status: 'active' | 'disabled' //
}
//
const orderList: TimeoutOrder[] = [
{
id: '5',
orderNo: 'ORD-20231024-001',
deviceType: '制水机',
deviceId: 'WM-2023-001',
area: '市区',
problemDesc: '设备漏水,需要更换密封垫',
status: 'timeout',
createTime: '2023-10-24 09:10:05',
lastUploadTime: '2023-10-24 08:50:12',
location: '市区图书馆一楼大厅'
},
{
id: '6',
orderNo: 'ORD-20231024-002',
deviceType: '供水机',
deviceId: 'WS-2023-001',
area: '校区',
problemDesc: '出水口感异常,需要检测水质',
status: 'timeout',
createTime: '2023-10-24 10:20:33',
lastUploadTime: '2023-10-24 10:15:30',
location: '校区宿舍3号楼一层'
}
]
//
const staffList: MaintenanceStaff[] = [
{ id: 's1', name: '张三', phone: '13800138000', area: '市区', status: 'active' },
{ id: 's2', name: '李四', phone: '13900139000', area: '校区', status: 'active' },
{ id: 's3', name: '王五', phone: '13700137000', area: '市区', status: 'active' },
{ id: 's4', name: '赵六', phone: '13600136000', area: '校区', status: 'disabled' }
]
//
const orders = ref<TimeoutOrder[]>(orderList)
const orders = ref<TimeoutOrder[]>([])
const currentPage = ref(1)
const pageSize = 10 //
const router = useRouter()
const authStore = useAuthStore()
const loading = ref(false)
// /ID
const searchKeyword = ref('')
@ -279,7 +249,117 @@ const assignDialogVisible = ref(false)
const currentOrder = ref<TimeoutOrder | null>(null)
const selectedStaffId = ref('')
const assignRemark = ref('')
const allStaff = ref<MaintenanceStaff[]>(staffList)
const allStaff = ref<MaintenanceStaff[]>([])
//
const loadTimeoutOrders = async () => {
loading.value = true
try {
// Token
const token = authStore.token
if (!token) {
console.warn('未获取到 Token跳转到登录页')
router.push('/login')
return
}
console.log('当前 Token:', token.substring(0, 20) + '...')
//
let url = '/api/work-orders/by-status?status=timeout'
const params = new URLSearchParams()
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
const areaId = filterForm.value.area || userInfo.areaId || ''
if (areaId) {
params.append('areaId', areaId)
}
//
const queryString = params.toString()
if (queryString) {
url += `&${queryString}`
}
// 使 request
const response = await request<{
code: number
msg: string
data: any[]
}>(url, {
method: 'GET',
})
//
if (response.code === 200) {
orders.value = (response.data || []).map((order: any) => ({
id: order.orderId || '',
orderNo: order.orderId || '',
deviceType: order.deviceType || '未知设备',
deviceId: order.deviceId || '',
area: order.areaId || '',
problemDesc: order.description || '暂无描述',
status: order.status || 'timeout',
createTime: order.createdTime ? new Date(order.createdTime).toLocaleString('zh-CN') : '未知时间',
lastUploadTime: order.lastUploadTime ? new Date(order.lastUploadTime).toLocaleString('zh-CN') : '未知',
location: order.location || '未知位置'
}))
} else {
const errorMsg = response.msg || `获取失败(错误码:${response.code}`
console.error('获取超时工单失败:', errorMsg)
alert(`获取超时工单失败:${errorMsg}`)
}
} catch (error: any) {
console.error('请求异常:', error)
console.error('错误详情:', {
message: error.message,
status: error.status,
response: error.response
})
const errorMsg = error.message.includes('401') || error.message.includes('403')
? '权限不足或登录已过期,请重新登录'
: error.message.includes('Network')
? '网络连接失败,请检查网络'
: error.message || '获取数据失败,请稍后重试'
alert(`获取超时工单失败:${errorMsg}`)
if (error.message.includes('401') || error.message.includes('403')) {
authStore.logout()
router.push('/login')
}
} finally {
loading.value = false
}
}
//
const loadMaintenanceStaff = async () => {
try {
// ID
if (!currentOrder.value) return
const areaId = currentOrder.value.area
//
const response = await request<{
code: number
msg: string
data: MaintenanceStaff[]
}>('/api/web/repairman/by-area/' + areaId, {
method: 'GET',
})
if (response.code === 200) {
allStaff.value = response.data || []
} else {
console.error('获取维修人员失败:', response.msg)
alert('获取维修人员失败:' + response.msg)
}
} catch (error: any) {
console.error('请求异常:', error)
alert('获取维修人员失败:' + (error.message || '网络错误'))
}
}
//
const formatStatus = (status: OrderStatus): string => {
@ -300,22 +380,29 @@ const filteredOrders = computed(() => {
const keywordMatch = searchKeyword.value.trim() === '' ||
order.orderNo.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
order.deviceId.toLowerCase().includes(searchKeyword.value.toLowerCase())
//
const areaMatch = filterForm.value.area === '' || order.area === filterForm.value.area
//
const dateMatch = filterForm.value.createDate === '' ||
const dateMatch = filterForm.value.createDate === '' ||
order.createTime.split(' ')[0] === filterForm.value.createDate
return keywordMatch && areaMatch && dateMatch
})
})
//
const paginatedOrders = computed(() => {
const start = (currentPage.value - 1) * pageSize
const end = start + pageSize
return filteredOrders.value.slice(start, end)
})
//
const filteredStaff = computed(() => {
if (!currentOrder.value) return []
return allStaff.value.filter(staff =>
return allStaff.value.filter(staff =>
staff.area === currentOrder.value!.area && staff.status === 'active'
)
})
@ -333,6 +420,7 @@ const handleSearch = () => {
// /
const handleFilter = () => {
currentPage.value = 1 //
loadTimeoutOrders() //
}
//
@ -343,6 +431,7 @@ const resetFilter = () => {
createDate: ''
}
currentPage.value = 1
loadTimeoutOrders()
}
//
@ -367,21 +456,46 @@ const handleStaffChange = () => {
}
//
const confirmAssign = () => {
const confirmAssign = async () => {
if (!currentOrder.value || !selectedStaffId.value) return
// API
console.log('派单信息:', {
orderId: currentOrder.value.id,
staffId: selectedStaffId.value,
remark: assignRemark.value,
assignTime: new Date().toLocaleString()
})
//
closeAssignDialog()
//
try {
// API
const response = await request<{
code: number
msg: string
data: boolean
}>('/api/work-orders/assign', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
orderId: currentOrder.value.id,
repairmanId: selectedStaffId.value
})
})
if (response.code === 200 && response.data) {
alert('派单成功')
closeAssignDialog()
loadTimeoutOrders() //
} else {
const errorMsg = response.msg || '派单失败'
alert(errorMsg)
}
} catch (error: any) {
console.error('派单请求异常:', error)
alert('派单失败:' + (error.message || '网络错误'))
}
}
//
onMounted(() => {
console.log('Token:', authStore.token)
loadTimeoutOrders()
loadMaintenanceStaff()
})
</script>
<style scoped>
@ -752,28 +866,28 @@ const confirmAssign = () => {
flex-direction: column;
align-items: flex-start;
}
.filter-item {
width: 100%;
}
.search-input, .filter-select, .filter-input {
width: 100%;
}
.dialog-container {
width: 90%;
max-width: 500px;
}
.form-group {
flex-direction: column;
}
.form-label {
width: 100%;
margin-bottom: 8px;
padding-top: 0;
}
}
</style>
</style>

Loading…
Cancel
Save