155 lines
3.4 KiB
Plaintext
155 lines
3.4 KiB
Plaintext
<template>
|
|
<view class="sports-stats-page">
|
|
<view class="stats-controls">
|
|
<picker mode="date" :value="selectedDate" @change="onDateChange">
|
|
<view class="picker">
|
|
当前选择: {{ selectedDate }}
|
|
</view>
|
|
</picker>
|
|
<button @click="fetchStats">查询</button>
|
|
</view>
|
|
|
|
<view v-if="loading" class="loading-container">加载中...</view>
|
|
<view v-else-if="error" class="error-container">{{ error }}</view>
|
|
<view v-else class="stats-container">
|
|
<view class="stat-card">
|
|
<text class="stat-title">总步数</text>
|
|
<text class="stat-value">{{ summary.total_steps ?? 0 }}</text>
|
|
</view>
|
|
<view class="stat-card">
|
|
<text class="stat-title">总距离 (米)</text>
|
|
<text class="stat-value">{{ summary.total_distance?.toFixed(2) ?? 0 }}</text>
|
|
</view>
|
|
<view class="stat-card">
|
|
<text class="stat-title">平均心率</text>
|
|
<text class="stat-value">{{ summary.avg_heart_rate?.toFixed(1) ?? 0 }}</text>
|
|
</view>
|
|
<view class="stat-card">
|
|
<text class="stat-title">活跃用户</text>
|
|
<text class="stat-value">{{ summary.active_users ?? 0 }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Add charts here later -->
|
|
<view class="chart-container">
|
|
<text>图表区域</text>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script lang="uts">
|
|
import { ref, reactive, onMounted } from 'vue'
|
|
import supa from '@/components/supadb/aksupainstance.uts'
|
|
|
|
type SportsSummary = {
|
|
total_steps: number
|
|
total_distance: number
|
|
avg_heart_rate: number
|
|
active_users: number
|
|
}
|
|
|
|
export default {
|
|
setup() {
|
|
const selectedDate = ref(new Date().toISOString().slice(0, 10))
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
const summary = reactive < SportsSummary > ({
|
|
total_steps: 0,
|
|
total_distance: 0,
|
|
avg_heart_rate: 0,
|
|
active_users: 0
|
|
})
|
|
|
|
const onDateChange = (e: any) => {
|
|
selectedDate.value = e.detail.value
|
|
}
|
|
|
|
const fetchStats = async () => {
|
|
loading.value = true
|
|
error.value = null
|
|
|
|
const date = selectedDate.value
|
|
const result = await supa.rpc('get_daily_sports_stats', { query_date: date })
|
|
|
|
if (result.error) {
|
|
error.value = `获取统计数据失败: ${result.error.message}`
|
|
console.error(result.error)
|
|
} else if (result.data) {
|
|
const stats = result.data[0]
|
|
summary.total_steps = stats.total_steps
|
|
summary.total_distance = stats.total_distance
|
|
summary.avg_heart_rate = stats.avg_heart_rate
|
|
summary.active_users = stats.active_users
|
|
}
|
|
|
|
loading.value = false
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchStats()
|
|
})
|
|
|
|
return {
|
|
selectedDate,
|
|
loading,
|
|
error,
|
|
summary,
|
|
onDateChange,
|
|
fetchStats
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.sports-stats-page {
|
|
padding: 15px;
|
|
}
|
|
|
|
.stats-controls {
|
|
display: flex;
|
|
gap: 15px;
|
|
margin-bottom: 20px;
|
|
align-items: center;
|
|
}
|
|
|
|
.stats-controls .picker {
|
|
border: 1px solid #ccc;
|
|
padding: 8px 12px;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.stats-container {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
gap: 15px;
|
|
}
|
|
|
|
.stat-card {
|
|
background-color: #fff;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-title {
|
|
font-size: 14px;
|
|
color: #666;
|
|
display: block;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
color: #333;
|
|
}
|
|
|
|
.chart-container {
|
|
margin-top: 30px;
|
|
padding: 20px;
|
|
background-color: #fff;
|
|
border-radius: 8px;
|
|
}
|
|
</style> |