Merge remote-tracking branch 'origin/develop' into wanglei_branch

pull/109/head
wanglei 2 months ago
commit 7def7464cc

@ -0,0 +1,66 @@
package com.campus.water.controller;
import com.campus.water.entity.Notification;
import com.campus.water.service.NotificationService;
import com.campus.water.util.ResultVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
*
* APP
*/
@RestController
@RequestMapping("/api/app/repairman/notification")
@RequiredArgsConstructor
@Tag(name = "维修人员通知接口", description = "维修人员APP端通知查询/已读标记接口")
public class RepairmanNotificationController {
private final NotificationService notificationService;
/**
*
*/
@GetMapping("/unread")
@Operation(summary = "获取未读通知", description = "查询维修人员所有未读的派单/系统通知")
public ResultVO<List<Notification>> getUnreadNotifications(@RequestParam String repairmanId) {
try {
List<Notification> unreadNotifications = notificationService.getUnreadNotifications(repairmanId);
return ResultVO.success(unreadNotifications, "获取未读通知成功");
} catch (Exception e) {
return ResultVO.error(500, "获取未读通知失败:" + e.getMessage());
}
}
/**
*
*/
@GetMapping("/all")
@Operation(summary = "获取所有通知", description = "查询维修人员所有通知(已读+未读)")
public ResultVO<List<Notification>> getAllNotifications(@RequestParam String repairmanId) {
try {
List<Notification> allNotifications = notificationService.getAllNotifications(repairmanId);
return ResultVO.success(allNotifications, "获取所有通知成功");
} catch (Exception e) {
return ResultVO.error(500, "获取所有通知失败:" + e.getMessage());
}
}
/**
*
*/
@PostMapping("/read")
@Operation(summary = "标记通知为已读", description = "将指定通知标记为已读状态")
public ResultVO<Boolean> markNotificationAsRead(@RequestParam Long notificationId) {
try {
notificationService.markAsRead(notificationId);
return ResultVO.success(true, "标记通知为已读成功");
} catch (Exception e) {
return ResultVO.error(500, "标记通知为已读失败:" + e.getMessage());
}
}
}

@ -0,0 +1,56 @@
package com.campus.water.controller;
import com.campus.water.entity.dto.request.StudentDrinkQueryDTO;
import com.campus.water.entity.vo.StudentDrinkStatsVO;
import com.campus.water.service.StudentDrinkStatsService;
import com.campus.water.util.ResultVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
*
*/
@RestController
@RequestMapping("/api/student/drink-stats")
@RequiredArgsConstructor
@Tag(name = "学生端-饮水量统计", description = "学生查看本日/本周/本月饮水量")
public class StudentDrinkStatsController {
private final StudentDrinkStatsService drinkStatsService;
@PostMapping("/today")
@Operation(summary = "查询本日饮水量", description = "获取学生当日的饮水量、次数及明细")
public ResultVO<StudentDrinkStatsVO> getTodayStats(@RequestBody StudentDrinkQueryDTO request) {
// 手动校验学生ID非空
if (request.getStudentId() == null || request.getStudentId().trim().isEmpty()) {
return ResultVO.badRequest("学生ID不能为空");
}
StudentDrinkStatsVO stats = drinkStatsService.getTodayDrinkStats(request.getStudentId());
return ResultVO.success(stats, "查询本日饮水量成功");
}
@PostMapping("/this-week")
@Operation(summary = "查询本周饮水量", description = "获取学生本周的饮水量、日均量及每日明细")
public ResultVO<StudentDrinkStatsVO> getThisWeekStats(@RequestBody StudentDrinkQueryDTO request) {
if (request.getStudentId() == null || request.getStudentId().trim().isEmpty()) {
return ResultVO.badRequest("学生ID不能为空");
}
StudentDrinkStatsVO stats = drinkStatsService.getThisWeekDrinkStats(request.getStudentId());
return ResultVO.success(stats, "查询本周饮水量成功");
}
@PostMapping("/this-month")
@Operation(summary = "查询本月饮水量", description = "获取学生本月的饮水量、日均量及每日明细")
public ResultVO<StudentDrinkStatsVO> getThisMonthStats(@RequestBody StudentDrinkQueryDTO request) {
if (request.getStudentId() == null || request.getStudentId().trim().isEmpty()) {
return ResultVO.badRequest("学生ID不能为空");
}
StudentDrinkStatsVO stats = drinkStatsService.getThisMonthDrinkStats(request.getStudentId());
return ResultVO.success(stats, "查询本月饮水量成功");
}
}

@ -0,0 +1,51 @@
package com.campus.water.entity;
import lombok.Data;
import jakarta.persistence.*;
import java.time.LocalDateTime;
/**
*
*
*/
@Data
@Entity
@Table(name = "notification")
public class Notification {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 维修人员ID */
@Column(name = "repairman_id", nullable = false, length = 50)
private String repairmanId;
/** 关联工单ID */
@Column(name = "order_id", length = 50)
private String orderId;
/** 通知内容 */
@Column(name = "content", nullable = false, length = 500)
private String content;
/** 是否已读(默认未读) */
@Column(name = "is_read")
private boolean isRead = false;
/** 创建时间 */
@Column(name = "created_time", nullable = false)
private LocalDateTime createdTime = LocalDateTime.now();
/** 通知类型 */
@Enumerated(EnumType.STRING)
@Column(name = "type", nullable = false, length = 20)
private NotificationType type;
/** 通知类型枚举 */
public enum NotificationType {
ORDER_ASSIGNED, // 派单通知
ORDER_GRABBED, // 抢单通知
ORDER_REJECTED, // 拒单通知
SYSTEM // 系统通知
}
}

@ -0,0 +1,12 @@
package com.campus.water.entity.dto.request;
import lombok.Data;
/**
* DTO
*/
@Data
public class StudentDrinkQueryDTO {
/** 学生ID */
private String studentId;
}

@ -0,0 +1,16 @@
package com.campus.water.entity.vo;
import lombok.Data;
/**
* VO
*/
@Data
public class DailyDrinkVO {
/** 日期yyyy-MM-dd */
private String date;
/** 当日饮水量(升) */
private Double consumption;
/** 当日饮水次数 */
private Integer count;
}

@ -0,0 +1,28 @@
package com.campus.water.entity.vo;
import lombok.Data;
import java.util.List;
import com.campus.water.entity.DrinkRecord;
import com.campus.water.entity.vo.DailyDrinkVO;
/**
* VO
*/
@Data
public class StudentDrinkStatsVO {
/** 学生ID */
private String studentId;
/** 统计维度(本日/本周/本月) */
private String timeDimension;
/** 统计时间范围(如"2025-12-25~2025-12-25" */
private String timeRange;
/** 总饮水量(升) */
private Double totalConsumption;
/** 日均饮水量(升) */
private Double avgDailyConsumption;
/** 饮水次数 */
private Integer drinkCount;
/** 按日期分组的每日饮水量明细 */
private List<DailyDrinkVO> dailyDetails;
/** 所有饮水记录明细 */
private List<DrinkRecord> drinkRecords;
}

@ -0,0 +1,22 @@
package com.campus.water.mapper;
import com.campus.water.entity.Notification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* 访
*/
@Repository
public interface NotificationRepository extends JpaRepository<Notification, Long> {
/**
*
*/
List<Notification> findByRepairmanIdAndIsReadFalseOrderByCreatedTimeDesc(String repairmanId);
/**
*
*/
List<Notification> findByRepairmanIdOrderByCreatedTimeDesc(String repairmanId);
}

@ -0,0 +1,37 @@
package com.campus.water.service;
import com.campus.water.entity.Notification;
import java.util.List;
/**
*
*/
public interface NotificationService {
/**
*
* @param repairmanId ID
* @param orderId ID
* @param content
*/
void sendOrderAssignedNotification(String repairmanId, String orderId, String content);
/**
*
* @param repairmanId ID
* @return
*/
List<Notification> getUnreadNotifications(String repairmanId);
/**
*
* @param repairmanId ID
* @return
*/
List<Notification> getAllNotifications(String repairmanId);
/**
*
* @param notificationId ID
*/
void markAsRead(Long notificationId);
}

@ -0,0 +1,59 @@
package com.campus.water.service.impl;
import com.campus.water.entity.Notification;
import com.campus.water.mapper.NotificationRepository;
import com.campus.water.service.NotificationService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
/**
*
*/
@Service
@RequiredArgsConstructor
public class NotificationServiceImpl implements NotificationService {
private final NotificationRepository notificationRepository;
/**
*
*/
@Override
public void sendOrderAssignedNotification(String repairmanId, String orderId, String content) {
Notification notification = new Notification();
notification.setRepairmanId(repairmanId);
notification.setOrderId(orderId);
notification.setContent(content);
notification.setType(Notification.NotificationType.ORDER_ASSIGNED);
notificationRepository.save(notification);
}
/**
*
*/
@Override
public List<Notification> getUnreadNotifications(String repairmanId) {
return notificationRepository.findByRepairmanIdAndIsReadFalseOrderByCreatedTimeDesc(repairmanId);
}
/**
*
*/
@Override
public List<Notification> getAllNotifications(String repairmanId) {
return notificationRepository.findByRepairmanIdOrderByCreatedTimeDesc(repairmanId);
}
/**
*
*/
@Override
public void markAsRead(Long notificationId) {
notificationRepository.findById(notificationId).ifPresent(notification -> {
notification.setRead(true);
notificationRepository.save(notification);
});
}
}

@ -0,0 +1,105 @@
package com.campus.water.service;
import com.campus.water.entity.DrinkRecord;
import com.campus.water.entity.vo.DailyDrinkVO;
import com.campus.water.entity.vo.StudentDrinkStatsVO;
import com.campus.water.mapper.DrinkRecordRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.*;
import java.time.temporal.TemporalAdjusters;
import java.util.*;
import java.util.stream.Collectors;
/**
*
*/
@Service
@RequiredArgsConstructor
public class StudentDrinkStatsService {
private final DrinkRecordRepository drinkRecordRepository;
/**
*
*/
public StudentDrinkStatsVO getTodayDrinkStats(String studentId) {
LocalDate today = LocalDate.now();
LocalDateTime start = today.atStartOfDay();
LocalDateTime end = LocalDateTime.now();
return calculateStats(studentId, start, end, "本日", "today");
}
/**
*
*/
public StudentDrinkStatsVO getThisWeekDrinkStats(String studentId) {
LocalDate today = LocalDate.now();
LocalDateTime start = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).atStartOfDay();
LocalDateTime end = LocalDateTime.now();
return calculateStats(studentId, start, end, "本周", "thisWeek");
}
/**
*
*/
public StudentDrinkStatsVO getThisMonthDrinkStats(String studentId) {
LocalDate today = LocalDate.now();
LocalDateTime start = today.with(TemporalAdjusters.firstDayOfMonth()).atStartOfDay();
LocalDateTime end = LocalDateTime.now();
return calculateStats(studentId, start, end, "本月", "thisMonth");
}
/**
*
*/
private StudentDrinkStatsVO calculateStats(String studentId, LocalDateTime start, LocalDateTime end,
String timeRangeDesc, String timeDimension) {
// 1. 查询时间范围内的饮水记录
List<DrinkRecord> records = drinkRecordRepository
.findByStudentIdAndDrinkTimeBetweenOrdered(studentId, start, end);
// 2. 按日期分组统计
Map<LocalDate, List<DrinkRecord>> dailyGroup = records.stream()
.collect(Collectors.groupingBy(record -> record.getDrinkTime().toLocalDate()));
// 3. 构建每日明细
List<DailyDrinkVO> dailyDetails = new ArrayList<>();
dailyGroup.forEach((date, dailyRecords) -> {
DailyDrinkVO dailyVO = new DailyDrinkVO();
dailyVO.setDate(date.toString());
// 当日总饮水量
double dailyTotal = dailyRecords.stream()
.map(DrinkRecord::getWaterConsumption)
.filter(Objects::nonNull)
.mapToDouble(BigDecimal::doubleValue)
.sum();
dailyVO.setConsumption(dailyTotal);
dailyVO.setCount(dailyRecords.size());
dailyDetails.add(dailyVO);
});
// 按日期排序
dailyDetails.sort(Comparator.comparing(DailyDrinkVO::getDate));
// 4. 计算总饮水量、总次数、日均饮水量
double totalConsumption = dailyDetails.stream()
.mapToDouble(DailyDrinkVO::getConsumption)
.sum();
int totalCount = records.size();
double avgDaily = dailyDetails.isEmpty() ? 0 : totalConsumption / dailyDetails.size();
// 5. 封装结果VO
StudentDrinkStatsVO statsVO = new StudentDrinkStatsVO();
statsVO.setStudentId(studentId);
statsVO.setTimeDimension(timeDimension);
statsVO.setTimeRange(timeRangeDesc + "(" + start.toLocalDate() + "~" + end.toLocalDate() + ")");
statsVO.setTotalConsumption(totalConsumption);
statsVO.setDrinkCount(totalCount);
statsVO.setAvgDailyConsumption(avgDaily);
statsVO.setDailyDetails(dailyDetails);
statsVO.setDrinkRecords(records);
return statsVO;
}
}

@ -9,7 +9,8 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.campus.water.service.NotificationService;
import com.campus.water.service.WorkOrderService;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
@ -28,6 +29,7 @@ public class WorkOrderServiceImpl implements WorkOrderService {
private final WorkOrderRepository workOrderRepository;
private final RepairmanRepository repairmanRepository;
private final NotificationService notificationService;
@Override
public WorkOrder getOrderDetail(String orderId) {
@ -288,6 +290,16 @@ public class WorkOrderServiceImpl implements WorkOrderService {
order.setGrabbedTime(LocalDateTime.now());
workOrderRepository.save(order);
// ===== 新增:派单通知 =====
try {
String notificationContent = String.format("您有新的维修工单待处理工单ID%s", orderId);
notificationService.sendOrderAssignedNotification(repairmanId, orderId, notificationContent);
} catch (Exception e) {
// 捕获异常,不影响原有派单逻辑
System.err.println("派单通知发送失败:" + e.getMessage());
}
// ==============================================
// 更新维修人员状态
Repairman repairman = repairmanOpt.get();
repairman.setStatus(Repairman.RepairmanStatus.busy);

@ -18,6 +18,11 @@ const router = createRouter({
name: 'WaterQuality',
component: () => import('../views/WaterQualityPage.vue')
},
{
path: '/realtime-data',
name: 'RealtimeData',
component: () => import('../views/RealtimeDataPage.vue')
},
{
path: '/scan',
name: 'ScanPage',

@ -62,6 +62,17 @@ export const deviceService = {
}
}
throw error.response?.data || error.message
}
},
// 新增:获取实时数据接口
async getRealtimeData(terminalId) {
try {
const response = await api.get(`/api/water/realtime/${terminalId}`)
console.log(`终端 ${terminalId} 实时数据:`, response.data)
return response.data
} catch (error) {
console.error(`获取终端 ${terminalId} 实时数据失败:`, error)
throw error.response?.data || error.message
}
}

@ -0,0 +1,49 @@
// src/services/studentDrinkStatsService.js
import apiClient from '@/services/api'
export const studentDrinkStatsService = {
/**
* 获取今日饮水统计
*/
async getTodayStats(studentId) {
try {
const response = await apiClient.post('/api/student/drink-stats/today', {
studentId: studentId
})
return response.data
} catch (error) {
console.error('获取今日饮水统计失败:', error)
throw error
}
},
/**
* 获取本周饮水统计
*/
async getThisWeekStats(studentId) {
try {
const response = await apiClient.post('/api/student/drink-stats/this-week', {
studentId: studentId
})
return response.data
} catch (error) {
console.error('获取本周饮水统计失败:', error)
throw error
}
},
/**
* 获取本月饮水统计
*/
async getThisMonthStats(studentId) {
try {
const response = await apiClient.post('/api/student/drink-stats/this-month', {
studentId: studentId
})
return response.data
} catch (error) {
console.error('获取本月饮水统计失败:', error)
throw error
}
}
}

@ -39,18 +39,36 @@
<div class="stats-section">
<div class="stats-cards">
<div class="stat-card">
<div class="stat-value">{{ userStats.days }}</div>
<div class="stat-label">累计用水天数</div>
<div class="stat-value">{{ userStats.totalConsumption }}ml</div>
<div class="stat-label">累计饮水量</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ userStats.todayWater }}</div>
<div class="stat-label">今日饮水量</div>
<div class="stat-value">{{ userStats.drinkCount }}</div>
<div class="stat-label">饮水次数</div>
</div>
</div>
<div class="stats-cards">
<div class="stat-card">
<div class="stat-value">{{ userStats.avgDaily }}ml</div>
<div class="stat-label">日均饮水量</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ userStats.days }}</div>
<div class="stat-label">饮水天数</div>
</div>
</div>
</div>
<!-- 时间切换 -->
<div class="time-switch">
<div
class="time-option"
:class="{ active: selectedPeriod === 'today' }"
@click="selectPeriod('today')"
>
今日
</div>
<div
class="time-option"
:class="{ active: selectedPeriod === 'week' }"
@ -81,10 +99,10 @@
<div class="chart-axis">
<!-- Y轴 -->
<div class="y-axis">
<div class="y-label">800ml</div>
<div class="y-label">600ml</div>
<div class="y-label">400ml</div>
<div class="y-label">200ml</div>
<div class="y-label">{{ maxChartValue }}ml</div>
<div class="y-label">{{ Math.round(maxChartValue * 0.75) }}ml</div>
<div class="y-label">{{ Math.round(maxChartValue * 0.5) }}ml</div>
<div class="y-label">{{ Math.round(maxChartValue * 0.25) }}ml</div>
<div class="y-label">0ml</div>
</div>
@ -97,7 +115,7 @@
>
<div
class="bar"
:style="{ height: item.value + 'px' }"
:style="{ height: calculateBarHeight(item.value) + 'px' }"
:class="{ active: item.active }"
>
<div class="bar-value">{{ item.value }}ml</div>
@ -110,6 +128,26 @@
</div>
</div>
<!-- 每日详情 -->
<div class="daily-details-section">
<div class="section-title">每日详情</div>
<div class="daily-details-list">
<div
v-for="detail in dailyDetails"
:key="detail.date"
class="daily-detail-item"
>
<div class="detail-date">{{ formatDate(detail.date) }}</div>
<div class="detail-consumption">{{ detail.consumption }}ml</div>
<div class="detail-count">{{ detail.count }}</div>
</div>
<div v-if="dailyDetails.length === 0" class="no-data">
暂无饮水记录
</div>
</div>
</div>
<!-- 功能按钮 -->
<div class="action-section">
<button class="action-btn history-btn" @click="goToHistory">
@ -152,10 +190,10 @@
</template>
<script setup>
// ProfilePage.vue script setup
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { studentDrinkStatsService } from '@/services/studentDrinkStatsService'
const router = useRouter()
const userStore = useUserStore()
@ -169,46 +207,129 @@ const userInfo = reactive({
class: '软件2301班'
})
//
//
const userStats = reactive({
days: '26天',
todayWater: '500ml'
totalConsumption: 0,
drinkCount: 0,
avgDaily: 0,
days: 0,
timeRange: '',
timeDimension: ''
})
//
const dailyDetails = ref([])
//
const selectedPeriod = ref('week')
const selectedPeriod = ref('today')
const showChart = ref(false)
const chartData = ref([])
const maxChartValue = ref(0)
//
const fetchDrinkStats = async (period) => {
try {
let response
const studentId = userStore.studentId
if (!studentId) {
console.error('未获取到学生ID')
return
}
//
if (period === 'today') {
response = await studentDrinkStatsService.getTodayStats(studentId)
} else if (period === 'week') {
response = await studentDrinkStatsService.getThisWeekStats(studentId)
} else if (period === 'month') {
response = await studentDrinkStatsService.getThisMonthStats(studentId)
}
if (response.code === 200) {
const data = response.data
//
userStats.totalConsumption = data.totalConsumption
userStats.drinkCount = data.drinkCount
userStats.avgDaily = data.avgDailyConsumption
userStats.days = data.dailyDetails.length
userStats.timeRange = data.timeRange
userStats.timeDimension = data.timeDimension
//
dailyDetails.value = data.dailyDetails || []
//
updateChartData(data.dailyDetails, period)
} else {
console.error('获取饮水统计失败:', response.message)
}
} catch (error) {
console.error('获取饮水统计异常:', error)
}
}
//
const initChartData = () => {
if (selectedPeriod.value === 'week') {
//
chartData.value = [
{ label: '周一', value: 450, active: false },
{ label: '周二', value: 620, active: false },
{ label: '周三', value: 380, active: false },
{ label: '周四', value: 540, active: true },
{ label: '周五', value: 280, active: false },
{ label: '周六', value: 720, active: false },
{ label: '周日', value: 400, active: false }
]
//
const updateChartData = (details, period) => {
if (!details || details.length === 0) {
chartData.value = []
maxChartValue.value = 0
return
}
// Y
const values = details.map(item => item.consumption)
maxChartValue.value = Math.max(...values, 100) // 100
//
if (period === 'today') {
// -
chartData.value = details.map((item, index) => ({
label: formatDate(item.date).split('-').slice(1).join('/'),
value: item.consumption,
active: index === details.length - 1 //
}))
} else {
// 4
chartData.value = [
{ label: '第1周', value: 1800, active: false },
{ label: '第2周', value: 2200, active: false },
{ label: '第3周', value: 2500, active: true },
{ label: '第4周', value: 800, active: false }
]
// / -
chartData.value = details.map((item, index) => ({
label: formatDate(item.date).split('-').slice(1).join('/'),
value: item.consumption,
active: index === details.length - 1 //
}))
}
}
//
const calculateBarHeight = (value) => {
if (maxChartValue.value === 0) return 0
// 120px
return (value / maxChartValue.value) * 120
}
//
const formatDate = (dateString) => {
if (!dateString) return ''
// YYYY-MM-DD
if (dateString.includes('-')) {
return dateString
}
// YYYY-MM-DD
return dateString
}
//
const selectPeriod = (period) => {
const selectPeriod = async (period) => {
selectedPeriod.value = period
initChartData()
showChart.value = false
//
await fetchDrinkStats(period)
//
setTimeout(() => {
showChart.value = true
}, 300)
}
//
@ -232,7 +353,6 @@ const handleLogout = () => {
}
}
//
const goToPage = (page) => {
switch(page) {
@ -248,7 +368,7 @@ const goToPage = (page) => {
}
}
onMounted(() => {
onMounted(async () => {
//
if (userStore.isLoggedIn) {
userInfo.studentId = userStore.studentId
@ -256,10 +376,10 @@ onMounted(() => {
userInfo.lastName = userStore.username.charAt(0)
}
//
initChartData()
//
await fetchDrinkStats('today')
//
//
setTimeout(() => {
showChart.value = true
}, 500)
@ -379,6 +499,7 @@ onMounted(() => {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-bottom: 12px;
}
.stat-card {
@ -546,12 +667,74 @@ onMounted(() => {
white-space: nowrap;
}
.bar-label {
font-size: 10px;
color: #666;
margin-top: 4px;
}
/* 每日详情区域 */
.daily-details-section {
background: white;
border-radius: 16px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
}
.daily-details-section .section-title {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
padding-left: 4px;
border-left: 4px solid #1890ff;
}
.daily-details-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.daily-detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e8e8e8;
}
.detail-date {
font-size: 14px;
color: #333;
font-weight: 500;
}
.detail-consumption {
font-size: 14px;
color: #1890ff;
font-weight: 600;
}
.detail-count {
font-size: 12px;
color: #666;
background: #e8f4fc;
padding: 4px 8px;
border-radius: 10px;
}
.no-data {
text-align: center;
padding: 20px;
color: #999;
font-size: 14px;
}
/* 功能按钮区域 */
.action-section {
display: flex;
@ -676,4 +859,4 @@ onMounted(() => {
width: 25px;
}
}
</style>
</style>

@ -0,0 +1,414 @@
<template>
<div class="realtime-data-page">
<!-- 顶部标题栏 -->
<div class="header">
<div class="header-title">实时数据详情</div>
<button class="back-btn" @click="goBack"></button>
</div>
<!-- 加载状态 -->
<div v-if="isLoading" class="loading-section">
<div class="loading-spinner"></div>
<div class="loading-text">加载实时数据中...</div>
</div>
<!-- 主要内容区域 -->
<div v-else class="main-content">
<!-- 终端信息 -->
<div class="section-card">
<h2 class="section-title">终端设备信息</h2>
<div class="data-grid">
<div class="data-item">
<span class="data-label">终端ID</span>
<span class="data-value">{{ realtimeData.terminalInfo.terminalId }}</span>
</div>
<div class="data-item">
<span class="data-label">终端名称</span>
<span class="data-value">{{ realtimeData.terminalInfo.terminalName }}</span>
</div>
<div class="data-item">
<span class="data-label">终端状态</span>
<span class="data-value status" :class="realtimeData.terminalInfo.terminalStatus.toLowerCase()">{{ realtimeData.terminalInfo.terminalStatus }}</span>
</div>
<div class="data-item">
<span class="data-label">安装日期</span>
<span class="data-value">{{ realtimeData.terminalInfo.installDate }}</span>
</div>
<div class="data-item">
<span class="data-label">数据更新时间</span>
<span class="data-value">{{ realtimeData.updateTime }}</span>
</div>
</div>
</div>
<!-- 制水机信息 -->
<div class="section-card" v-if="realtimeData.makerDevice">
<h2 class="section-title">制水机实时数据</h2>
<div class="data-grid">
<div class="data-item">
<span class="data-label">设备ID</span>
<span class="data-value">{{ realtimeData.makerDevice.deviceId }}</span>
</div>
<div class="data-item">
<span class="data-label">设备类型</span>
<span class="data-value">{{ realtimeData.makerDevice.realtimeData.deviceType }}</span>
</div>
<div class="data-item">
<span class="data-label">在线状态</span>
<span class="data-value status" :class="realtimeData.makerDevice.realtimeData.onlineStatus.toLowerCase()">{{ realtimeData.makerDevice.realtimeData.onlineStatus }}</span>
</div>
<div class="data-item">
<span class="data-label">TDS值</span>
<span class="data-value">{{ realtimeData.makerDevice.realtimeData.tdsValue }} mg/L</span>
</div>
<div class="data-item">
<span class="data-label">pH值</span>
<span class="data-value">{{ realtimeData.makerDevice.realtimeData.phValue }}</span>
</div>
<div class="data-item">
<span class="data-label">出水温度</span>
<span class="data-value">{{ realtimeData.makerDevice.realtimeData.temperature }}</span>
</div>
<div class="data-item">
<span class="data-label">滤芯寿命</span>
<span class="data-value">{{ realtimeData.makerDevice.realtimeData.filterLife }}%</span>
</div>
<div class="data-item">
<span class="data-label">流量</span>
<span class="data-value">{{ realtimeData.makerDevice.realtimeData.flowRate }} L/min</span>
</div>
<div class="data-item">
<span class="data-label">累计用水量</span>
<span class="data-value">{{ realtimeData.makerDevice.realtimeData.totalUsage }} L</span>
</div>
<div class="data-item">
<span class="data-label">故障码</span>
<span class="data-value">{{ realtimeData.makerDevice.realtimeData.faultCode }}</span>
</div>
</div>
</div>
<!-- 供水机信息 -->
<div class="section-card" v-if="realtimeData.supplyDevice">
<h2 class="section-title">供水机实时数据</h2>
<div class="data-grid">
<div class="data-item">
<span class="data-label">设备ID</span>
<span class="data-value">{{ realtimeData.supplyDevice.deviceId }}</span>
</div>
<div class="data-item">
<span class="data-label">设备类型</span>
<span class="data-value">{{ realtimeData.supplyDevice.realtimeData.deviceType }}</span>
</div>
<div class="data-item">
<span class="data-label">在线状态</span>
<span class="data-value status" :class="realtimeData.supplyDevice.realtimeData.onlineStatus.toLowerCase()">{{ realtimeData.supplyDevice.realtimeData.onlineStatus }}</span>
</div>
<div class="data-item">
<span class="data-label">水压</span>
<span class="data-value">{{ realtimeData.supplyDevice.realtimeData.waterPressure }} MPa</span>
</div>
<div class="data-item">
<span class="data-label">水箱水位</span>
<span class="data-value">{{ realtimeData.supplyDevice.realtimeData.waterLevel }}%</span>
</div>
<div class="data-item">
<span class="data-label">水泵状态</span>
<span class="data-value">{{ realtimeData.supplyDevice.realtimeData.pumpStatus }}</span>
</div>
<div class="data-item">
<span class="data-label">工作电压</span>
<span class="data-value">{{ realtimeData.supplyDevice.realtimeData.voltage }} V</span>
</div>
<div class="data-item">
<span class="data-label">累计运行时长</span>
<span class="data-value">{{ realtimeData.supplyDevice.realtimeData.runHours }} h</span>
</div>
<div class="data-item">
<span class="data-label">故障码</span>
<span class="data-value">{{ realtimeData.supplyDevice.realtimeData.faultCode }}</span>
</div>
</div>
</div>
</div>
<!-- 底部导航栏 -->
<div class="bottom-nav">
<div class="nav-button" @click="goToPage('home')">
<div class="nav-icon">🗺</div>
<div class="nav-text">地图</div>
</div>
<div class="nav-button" @click="goToPage('scan')">
<div class="nav-icon">📷</div>
<div class="nav-text">扫码</div>
</div>
<div class="nav-button" @click="goToPage('profile')">
<div class="nav-icon">👤</div>
<div class="nav-text">我的</div>
</div>
</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { deviceService } from '@/services/deviceService'
export default {
name: 'RealtimeDataPage',
setup() {
const router = useRouter()
const route = useRoute()
const isLoading = ref(true)
const realtimeData = ref({})
//
const fetchRealtimeData = async () => {
try {
const terminalId = route.query.terminalId
if (!terminalId) {
console.error('缺少终端ID参数')
return
}
const response = await deviceService.getRealtimeData(terminalId)
if (response.code === 200) {
realtimeData.value = response
} else {
console.error('获取实时数据失败:', response.msg)
// 使
realtimeData.value = {
terminalInfo: {},
makerDevice: {},
supplyDevice: {}
}
}
} catch (error) {
console.error('获取实时数据异常:', error)
//
} finally {
isLoading.value = false
}
}
//
const goBack = () => {
router.back()
}
//
const goToPage = (page) => {
switch(page) {
case 'home':
router.push('/home')
break
case 'scan':
router.push('/scan')
break
case 'profile':
router.push('/profile')
break
}
}
onMounted(() => {
fetchRealtimeData()
})
return {
isLoading,
realtimeData,
goBack,
goToPage
}
}
}
</script>
<style scoped>
.realtime-data-page {
width: 375px;
height: 667px;
background: #f5f5f5;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
/* 顶部标题栏 */
.header {
height: 40px;
background: linear-gradient(135deg, #1156b1 0%, #81d3f8 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
}
.header-title {
font-size: 16px;
font-weight: 600;
color: white;
letter-spacing: 1px;
}
.back-btn {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.back-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
}
/* 主要内容区域 */
.main-content {
flex: 1;
padding: 20px 16px;
overflow-y: auto;
}
/* 加载状态 */
.loading-section {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #1890ff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
font-size: 14px;
color: #666;
}
/* 数据卡片 */
.section-card {
background: white;
border-radius: 16px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0 0 16px 0;
padding-bottom: 12px;
border-bottom: 1px solid #eee;
}
.data-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.data-item {
display: flex;
flex-direction: column;
padding: 8px 0;
}
.data-label {
font-size: 12px;
color: #999;
margin-bottom: 4px;
}
.data-value {
font-size: 14px;
color: #333;
font-weight: 500;
}
.status {
font-weight: 600;
}
.status.online {
color: #52c41a;
}
.status.offline {
color: #f5222d;
}
/* 底部导航栏 */
.bottom-nav {
height: 60px;
background: white;
border-top: 1px solid #e8e8e8;
display: grid;
grid-template-columns: repeat(3, 1fr);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
}
.nav-button {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
color: #666;
background: none;
border: none;
padding: 0;
}
.nav-button:hover {
background: #f5f5f5;
}
.nav-button.active {
color: #1890ff;
}
.nav-icon {
font-size: 20px;
margin-bottom: 4px;
}
.nav-text {
font-size: 12px;
line-height: 1;
}
</style>

@ -142,10 +142,10 @@
</button>
<button
class="action-btn secondary"
@click="viewWaterQuality"
@click="viewRealtimeData"
:disabled="deviceInfo.status !== 'online'"
>
查看水质
查看实时数据
</button>
</div>
@ -430,7 +430,34 @@ const startWaterProcess = async () => {
progress.value = 0
remainingTime.value = Math.ceil(selectedAmount.value / 50)
//
// API
let apiResult = null
try {
apiResult = await callWaterUsageAPI()
} catch (error) {
console.error('取水API调用失败:', error)
//
isProcessing.value = false
showResult(
'error',
'取水失败',
'API调用失败请重试'
)
return
}
// API
if (!apiResult || apiResult.code !== 200) {
isProcessing.value = false
showResult(
'error',
'取水失败',
apiResult?.message || '取水操作失败'
)
return
}
// API
const interval = setInterval(() => {
progress.value += 5
if (remainingTime.value > 0) {
@ -442,13 +469,13 @@ const startWaterProcess = async () => {
completeWaterProcess()
}
}, 200)
// API
await callWaterUsageAPI()
}
// API
const callWaterUsageAPI = async () => {
//
console.log('调用取水API水量:', selectedAmount.value)
try {
const result = await deviceService.scanToDrink(
deviceInfo.value.id,
@ -457,6 +484,8 @@ const callWaterUsageAPI = async () => {
)
if (result.code === 200) {
// API
console.log('API调用成功记录历史')
recordWaterHistory(result.data)
return result
} else {
@ -473,32 +502,18 @@ const callWaterUsageAPI = async () => {
const completeWaterProcess = async () => {
isProcessing.value = false
const apiResult = await callWaterUsageAPI()
if (apiResult.code === 200) {
showResult(
'success',
'取水成功',
`您已成功取水 ${selectedAmount.value}ml`
)
showResult(
'success',
'取水成功',
`您已成功取水 ${selectedAmount.value}ml`
)
recordWaterHistory({
deviceName: deviceInfo.value.name,
deviceId: deviceInfo.value.id,
amount: selectedAmount.value
})
setTimeout(() => {
resetScan()
showResultDialog.value = false
}, 2000)
} else {
showResult(
'error',
'取水失败',
apiResult.message || '取水操作失败,请重试'
)
}
setTimeout(() => {
resetScan()
showResultDialog.value = false
}, 2000)
}
//
@ -516,6 +531,7 @@ const closeResultDialog = () => {
}
///
//
const recordWaterHistory = (data) => {
//
if (!deviceInfo.value || !deviceInfo.value.id) {
@ -527,27 +543,42 @@ const recordWaterHistory = (data) => {
id: Date.now(),
date: '今日',
deviceName: deviceInfo.value.name || '饮水机',
deviceId: deviceInfo.value.id, // 使terminalID
deviceId: deviceInfo.value.id,
time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }),
amount: `${selectedAmount.value}ml`,
timestamp: new Date().toISOString(),
location: deviceInfo.value.location || ''
}
console.log('保存取水历史:', history)
//
//
const existingHistory = JSON.parse(localStorage.getItem('waterHistory') || '[]')
// terminalID
const filteredHistory = existingHistory.filter(record => record.deviceId !== deviceInfo.value.id)
// 5
const now = new Date()
const fiveSecondsAgo = new Date(now.getTime() - 5000) // 5
//
filteredHistory.unshift(history)
const duplicateIndex = existingHistory.findIndex(record => {
const recordTime = new Date(record.timestamp)
return (
record.deviceId === deviceInfo.value.id &&
recordTime >= fiveSecondsAgo &&
recordTime <= now
)
})
//
if (duplicateIndex !== -1) {
// 5
console.log('发现重复记录,替换而不是新增')
existingHistory[duplicateIndex] = history
} else {
//
console.log('保存新记录')
existingHistory.unshift(history)
}
// terminalID
const deviceRecords = {}
const finalHistory = filteredHistory.filter(record => {
const finalHistory = existingHistory.filter(record => {
if (!deviceRecords[record.deviceId]) {
deviceRecords[record.deviceId] = true
return true
@ -556,17 +587,17 @@ const recordWaterHistory = (data) => {
})
//
const limitedHistory = finalHistory.slice(0, 20) // 20
const limitedHistory = finalHistory.slice(0, 20)
localStorage.setItem('waterHistory', JSON.stringify(limitedHistory))
console.log('更新后的历史记录:', limitedHistory)
}
//
const viewWaterQuality = () => {
const viewRealtimeData = () => {
if (deviceInfo.value) {
router.push({
path: '/water-quality',
path: '/realtime-data',
query: {
terminalId: deviceInfo.value.id,
deviceId: deviceInfo.value.deviceId

Loading…
Cancel
Save