Files
akmon/pages/sport/student/simple-records.uvue
2026-01-20 08:04:15 +08:00

470 lines
11 KiB
Plaintext

<!-- 训练记录管理 - 直接使用 supaClient 示例 -->
<template>
<scroll-view direction="vertical" class="records-page">
<view class="header">
<text class="title">我的监测记录</text>
<button class="add-btn" @click="showAddRecord">新增记录</button>
</view>
<!-- 记录列表 -->
<scroll-view class="records-list" scroll-y="true">
<view class="record-item" v-for="record in records" :key="getRecordId(record)">
<view class="record-header">
<text class="record-title">{{ getRecordTitle(record) }}</text>
<text class="record-date">{{ formatDate(getRecordDate(record)) }}</text>
</view>
<view class="record-stats">
<text class="stat">时长: {{ getRecordDuration(record) }}分钟</text>
<text class="stat">消耗: {{ getRecordCalories(record) }}卡路里</text>
</view>
<view class="record-actions">
<button class="edit-btn" @click="editRecord(record)">编辑</button>
<button class="delete-btn" @click="deleteRecord(record)">删除</button>
</view>
</view>
</scroll-view>
<!-- 新增/编辑记录弹窗 -->
<view class="modal" v-if="showModal" @click="hideModal">
<view class="modal-content" @click.stop>
<text class="modal-title">{{ isEditing ? '编辑' : '新增' }}训练记录</text>
<input class="input" v-model="formData.title" placeholder="训练项目" />
<input class="input" v-model="formData.duration" type="number" placeholder="训练时长(分钟)" />
<input class="input" v-model="formData.calories" type="number" placeholder="消耗卡路里" />
<textarea class="textarea" v-model="formData.notes" placeholder="训练备注"></textarea>
<view class="modal-actions">
<button class="cancel-btn" @click="hideModal">取消</button>
<button class="save-btn" @click="saveRecord">保存</button>
</view>
</view>
</view>
</scroll-view>
</template>
<script setup lang="uts"> import { ref, onMounted, onUnmounted, computed } from 'vue'
import { onLoad, onResize } from '@dcloudio/uni-app'
import supaClient from '@/components/supadb/aksupainstance.uts'
import { getCurrentUserId } from '@/utils/store.uts'
// Responsive state - using onResize for dynamic updates
const screenWidth = ref<number>(uni.getSystemInfoSync().windowWidth)
// Computed properties for responsive design
const isLargeScreen = computed(() : boolean => {
return screenWidth.value >= 768
})
// 响应式数据
const userId = ref<string>('')
const records = ref<UTSJSONObject[]>([])
const showModal = ref<boolean>(false)
const isEditing = ref<boolean>(false)
const editingRecordId = ref<string>('')
const formData = ref({
title: '',
duration: '',
calories: '',
notes: ''
}) const loading = ref<boolean>(false)
const subscription = ref<any>(null)
// 页面加载时获取用户ID
onLoad((options: OnLoadOptions) => {
userId.value = options['id'] ?? getCurrentUserId()
})
// 生命周期
onMounted(() => {
screenWidth.value = uni.getSystemInfoSync().windowWidth
loadRecords()
setupRealtimeSubscription()
})
onResize((size: any) => {
screenWidth.value = size.size.windowWidth
})
onUnmounted(() => {
// 清理实时订阅
if (subscription.value) {
subscription.value.unsubscribe()
}
})// 获取记录相关信息
const getRecordId = (record: UTSJSONObject): string => {
return record.getString('id') ?? ''
}
const getRecordTitle = (record: UTSJSONObject): string => {
return record.getString('title') ?? '未命名训练'
}
const getRecordDate = (record: UTSJSONObject): string => {
return record.getString('created_at') ?? ''
}
const getRecordDuration = (record: UTSJSONObject): number => {
return record.getNumber('duration') ?? 0
}
const getRecordCalories = (record: UTSJSONObject): number => {
return record.getNumber('calories') ?? 0
}
// 格式化日期
const formatDate = (dateString: string): string => {
if (!dateString) return ''
try {
const date = new Date(dateString)
return `${date.getMonth() + 1}/${date.getDate()}`
} catch {
return ''
}
}
// 获取当前用户ID // 加载训练记录 - 直接使用 supaClient
const loadRecords = async () => {
try {
loading.value = true
const result = await supaClient
.from('ak_training_records')
.select('*', {})
.eq('student_id', userId.value)
.order('created_at', { ascending: false })
.limit(50)
.execute()
if (result.success) {
records.value = result.data as UTSJSONObject[]
} else {
uni.showToast({
title: '加载失败',
icon: 'none'
})
}
} catch (error) {
console.error('加载记录失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
loading.value = false
}
}
// 设置实时订阅 - 直接使用 supaClient
const setupRealtimeSubscription = () => {
try { subscription.value = supaClient
.from('ak_training_records') .on('INSERT', (payload) => {
console.log('新记录:', payload)
if ((payload.new as UTSJSONObject).getString('student_id') === userId.value) {
records.value.unshift(payload.new as UTSJSONObject)
}
})
.on('UPDATE', (payload) => {
console.log('记录更新:', payload)
const recordId = (payload.new as UTSJSONObject).getString('id') ?? ''
const index = records.value.findIndex(r => getRecordId(r) === recordId)
if (index !== -1) {
records.value[index] = payload.new as UTSJSONObject
}
})
.on('DELETE', (payload) => {
console.log('记录删除:', payload)
const recordId = (payload.old as UTSJSONObject).getString('id') ?? ''
records.value = records.value.filter(r => getRecordId(r) !== recordId)
})
.subscribe()
} catch (error) {
console.error('设置实时订阅失败:', error)
}
}
// 保存记录 - 直接使用 supaClient
const saveRecord = async () => {
if (!formData.value.title.trim()) {
uni.showToast({
title: '请输入训练项目',
icon: 'none'
})
return
}
try { const recordData = {
title: formData.value.title,
duration: parseInt(formData.value.duration) || 0,
calories: parseInt(formData.value.calories) || 0,
notes: formData.value.notes,
student_id: userId.value
}
let result
if (isEditing.value) {
// 更新记录
result = await supaClient
.from('ak_training_records')
.update(recordData)
.eq('id', editingRecordId.value)
.single()
.execute()
} else {
// 创建新记录
result = await supaClient
.from('ak_training_records')
.insert(recordData)
.single()
.execute()
}
if (result.success) {
uni.showToast({
title: isEditing.value ? '更新成功' : '保存成功',
icon: 'success'
})
hideModal()
loadRecords() // 重新加载数据
} else {
throw new Error(result.message || '保存失败')
}
} catch (error) {
console.error('保存记录失败:', error)
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
}
// 删除记录 - 直接使用 supaClient
const deleteRecord = async (record: UTSJSONObject) => {
const recordId = getRecordId(record)
const title = getRecordTitle(record)
uni.showModal({
title: '确认删除',
content: `确定要删除训练记录"${title}"吗?`,
success: async (res) => {
if (res.confirm) {
try {
const result = await supaClient
.from('ak_training_records')
.delete()
.eq('id', recordId)
.execute()
if (result.success) {
uni.showToast({
title: '删除成功',
icon: 'success'
})
loadRecords()
} else {
throw new Error(result.message || '删除失败')
}
} catch (error) {
console.error('删除记录失败:', error)
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
}
}
})
}
// 显示新增记录弹窗
const showAddRecord = () => {
isEditing.value = false
editingRecordId.value = ''
formData.value = {
title: '',
duration: '',
calories: '',
notes: ''
}
showModal.value = true
}
// 编辑记录
const editRecord = (record: UTSJSONObject) => {
isEditing.value = true
editingRecordId.value = getRecordId(record) formData.value = {
title: getRecordTitle(record),
duration: getRecordDuration(record).toString(),
calories: getRecordCalories(record).toString(),
notes: record.getString('notes') ?? ''
}
showModal.value = true
}
// 隐藏弹窗
const hideModal = () => {
showModal.value = false
}
</script>
<style>
.records-page {
display: flex;
flex:1;
padding: 20rpx;
background: #f5f5f5;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
padding: 20rpx;
background: white;
border-radius: 15rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.add-btn {
background-image: linear-gradient(to top right, #667eea, #764ba2);
color: white;
border: none;
padding: 15rpx 30rpx;
border-radius: 25rpx;
font-size: 28rpx;
}
.records-list {
height: calc(100vh - 200rpx);
}
.record-item {
background: white;
margin-bottom: 20rpx;
padding: 30rpx;
border-radius: 15rpx;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.1);
}
.record-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.record-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.record-date {
font-size: 24rpx;
color: #999;
}
.record-stats {
display: flex;
gap: 30rpx;
margin-bottom: 20rpx;
}
.stat {
font-size: 26rpx;
color: #666;
}
.record-actions {
display: flex;
gap: 20rpx;
}
.edit-btn, .delete-btn {
flex: 1;
padding: 15rpx;
border: none;
border-radius: 10rpx;
font-size: 26rpx;
}
.edit-btn {
background: #3498db;
color: white;
}
.delete-btn {
background: #e74c3c;
color: white;
}
/* 弹窗样式 */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 20rpx;
padding: 40rpx;
width: 90%;
max-width: 600rpx;
}
.modal-title {
font-size: 32rpx;
font-weight: bold;
text-align: center;
margin-bottom: 30rpx;
color: #333;
}
.input, .textarea {
width: 100%;
padding: 20rpx;
border: 1px solid #ddd;
border-radius: 10rpx;
margin-bottom: 20rpx;
font-size: 28rpx;
}
.textarea {
height: 120rpx;
resize: none;
}
.modal-actions {
display: flex;
gap: 20rpx;
margin-top: 30rpx;
}
.cancel-btn, .save-btn {
flex: 1;
padding: 25rpx;
border: none;
border-radius: 15rpx;
font-size: 28rpx;
}
.cancel-btn {
background: #95a5a6;
color: white;
}
.save-btn {
background-image: linear-gradient(to top right, #667eea, #764ba2);
color: white;
}
</style>