app2学生饮水量统计功能 #106

Merged
hnu202326010125 merged 1 commits from luoyuehang_branch into develop 3 weeks ago

@ -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>

@ -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,7 +587,7 @@ 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)

Loading…
Cancel
Save