470 lines
11 KiB
Plaintext
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>
|