1519 lines
40 KiB
Plaintext
1519 lines
40 KiB
Plaintext
<template>
|
||
<scroll-view direction="vertical" class="records-history-container" :scroll-y="true" :enable-back-to-top="true"> <!-- Header -->
|
||
<view class="header"> <view class="header-left">
|
||
<button v-if="fromStats" @click="goBack" class="back-btn">
|
||
<simple-icon type="arrow-left" :size="16" color="#FFFFFF" />
|
||
<text>返回</text>
|
||
</button>
|
||
<text
|
||
class="title">{{ fromStats ? (studentName=='' ? `${studentName} - 训练记录` : '学生训练记录') : '训练历史' }}</text>
|
||
</view>
|
||
<view class="header-actions">
|
||
<button @click="showFilters = !showFilters" class="filter-btn">
|
||
<simple-icon type="filter" :size="16" color="#FFFFFF" />
|
||
<text>筛选</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Filter Panel -->
|
||
<view v-if="showFilters" class="filter-panel">
|
||
<view class="filter-row">
|
||
<text class="filter-label">时间范围:</text>
|
||
<view class="date-range">
|
||
<input type="date" v-model="startDate" placeholder="开始日期" @change="applyFilters" />
|
||
<text class="date-separator">至</text>
|
||
<input type="date" v-model="endDate" placeholder="结束日期" @change="applyFilters" />
|
||
</view>
|
||
</view> <view class="filter-row">
|
||
<text class="filter-label">训练项目:</text>
|
||
<scroll-view class="project-filters" direction="horizontal"> <view class="filter-chips">
|
||
<view v-for="(project, index) in projectFilters" :key="index" class="filter-chip"
|
||
:class="{ active: (project as UTSJSONObject).getBoolean('selected') ?? false }" @click="toggleProjectFilter(index)">
|
||
<text class="chip-text">{{ (project as UTSJSONObject).getString('name') ?? '' }}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<view class="filter-actions">
|
||
<button @click="resetFilters" class="reset-btn">重置</button>
|
||
<button @click="applyFilters" class="apply-btn">应用筛选</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Content -->
|
||
<view v-if="error" class="error-container">
|
||
<text class="error-text">{{ error }}</text>
|
||
<button class="retry-btn" @click="retryLoad">重试</button>
|
||
</view> <view v-else-if="recordsLoading || checkingAuth || !pageReady" class="loading-container">
|
||
<text class="loading-text">{{ checkingAuth ? '验证登录状态...' : (!pageReady ? '初始化页面...' : '加载中...') }}</text>
|
||
</view> <scroll-view v-else class="content" direction="vertical"
|
||
:refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh"
|
||
@refresherrestore="onRefreshRestore" :scroll-top="scrollTop" id="records-scroll-view">
|
||
<!-- Statistics Summary -->
|
||
<view class="stats-section">
|
||
<view class="stats-grid">
|
||
<view class="stat-card">
|
||
<view class="stat-icon">
|
||
<simple-icon type="trophy" :size="24" color="#F59E0B" />
|
||
</view>
|
||
<view class="stat-content">
|
||
<text class="stat-value">{{ totalRecords }}</text>
|
||
<text class="stat-label">总训练次数</text>
|
||
</view>
|
||
</view>
|
||
<view class="stat-card">
|
||
<view class="stat-icon">
|
||
<simple-icon type="time" :size="24" color="#3B82F6" />
|
||
</view>
|
||
<view class="stat-content">
|
||
<text class="stat-value">{{ totalHours }}h</text>
|
||
<text class="stat-label">总训练时长</text>
|
||
</view>
|
||
</view>
|
||
<view class="stat-card">
|
||
<view class="stat-icon">
|
||
<simple-icon type="chart-bar" :size="24" color="#10B981" />
|
||
</view>
|
||
<view class="stat-content">
|
||
<text class="stat-value">{{ averageScore.toFixed(1) }}</text>
|
||
<text class="stat-label">平均得分</text>
|
||
</view>
|
||
</view>
|
||
<view class="stat-card">
|
||
<view class="stat-icon">
|
||
<simple-icon type="fire" :size="24" color="#EF4444" />
|
||
</view>
|
||
<view class="stat-content">
|
||
<text class="stat-value">{{ currentStreak }}</text>
|
||
<text class="stat-label">连续天数</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Records Timeline -->
|
||
<view class="records-section">
|
||
<view class="section-header">
|
||
<text class="section-title">训练记录</text>
|
||
<view class="view-toggle">
|
||
<button @click="viewMode = 'list'" class="toggle-btn" :class="{ active: viewMode === 'list' }">
|
||
<simple-icon type="list" :size="16" color="#6B7280" />
|
||
</button>
|
||
<button @click="viewMode = 'calendar'" class="toggle-btn"
|
||
:class="{ active: viewMode === 'calendar' }">
|
||
<simple-icon type="calendar" :size="16" color="#6B7280" />
|
||
</button>
|
||
</view>
|
||
</view> <!-- List View -->
|
||
<view v-if="viewMode === 'list'" class="records-list"> <view v-if="!recordsLoading && filteredRecords.length === 0" class="empty-state">
|
||
<simple-icon type="document" :size="48" color="#D1D5DB" />
|
||
<text class="empty-text">{{ (startDate.length > 0 || endDate.length > 0) ? '该时间段内暂无训练记录' : '暂无训练记录' }}</text>
|
||
<text class="empty-hint">{{ (startDate.length > 0 || endDate.length > 0) ? '请尝试调整筛选条件' : '完成训练后记录将显示在这里' }}</text>
|
||
</view>
|
||
|
||
<view v-else>
|
||
<view v-for="(group, groupIndex) in groupedRecords" :key="groupIndex" class="date-group">
|
||
<view class="date-header"> <text
|
||
class="date-text">{{ (group as UTSJSONObject).getString('date') ?? '' }}</text>
|
||
<text
|
||
class="count-text">{{ (group as UTSJSONObject).getNumber('count') ?? 0 }}次训练</text>
|
||
</view>
|
||
|
||
<view class="group-records">
|
||
<view
|
||
v-for="(record, recordIndex) in ((group as UTSJSONObject).getArray('records') ?? [])"
|
||
:key="recordIndex" class="record-item" @click="viewRecordDetail(record as UTSJSONObject)">
|
||
<view class="record-timeline">
|
||
<view class="timeline-dot" :class="getStatusClass(record as UTSJSONObject)"></view>
|
||
<view class="timeline-line" v-if="recordIndex < (group.records as Array<UTSJSONObject>).length - 1">
|
||
</view>
|
||
</view>
|
||
|
||
<view class="record-content">
|
||
<view class="record-header">
|
||
<text class="record-title">{{ getProjectName(record) }}</text>
|
||
<text class="record-time">{{ getRecordTime(record) }}</text>
|
||
</view>
|
||
|
||
<view class="record-details">
|
||
<view class="detail-item">
|
||
<simple-icon type="time" :size="14" color="#6B7280" />
|
||
<text class="detail-text">{{ getDurationText(record) }}</text>
|
||
</view>
|
||
<view class="detail-item">
|
||
<simple-icon type="chart-bar" :size="14" color="#6B7280" />
|
||
<text class="detail-text">完成度 {{ getCompletionRate(record) }}%</text>
|
||
</view>
|
||
<view v-if="getRecordScore(record) > 0" class="detail-item">
|
||
<simple-icon type="star" :size="14" color="#6B7280" />
|
||
<text class="detail-text score" :class="getScoreClass(record)">
|
||
{{ getRecordScore(record) }}分
|
||
</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="getRecordNotes(record)" class="record-notes">
|
||
<text class="notes-text">{{ getRecordNotes(record) }}</text>
|
||
</view>
|
||
|
||
<view class="record-status">
|
||
<view class="status-badge" :class="getStatusClass(record)">
|
||
<text class="status-text">{{ getRecordStatusText(record) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Calendar View -->
|
||
<view v-else class="calendar-view">
|
||
<text class="view-placeholder">日历视图开发中...</text>
|
||
</view>
|
||
</view> <!-- Load More -->
|
||
<view v-if="hasMore && !recordsLoading && filteredRecords.length > 0" class="load-more-section">
|
||
<button @click="loadMoreRecords" class="load-more-btn">
|
||
<text>加载更多</text>
|
||
</button>
|
||
</view>
|
||
|
||
<!-- Loading More Indicator -->
|
||
<view v-if="recordsLoading && currentPage > 1" class="loading-more-container">
|
||
<text class="loading-text">加载更多中...</text>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
|
||
</scroll-view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||
import { onLoad, onReady, onResize } from '@dcloudio/uni-app'
|
||
import { state, getCurrentUserId } from '@/utils/store.uts'
|
||
import supaClient from '@/components/supadb/aksupainstance.uts'
|
||
import {
|
||
|
||
formatDate
|
||
} from '../types.uts'
|
||
// Reactive data
|
||
// 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 error = ref<string | null>(null)
|
||
const recordsLoading = ref<boolean>(false)
|
||
const refreshing = ref<boolean>(false)
|
||
const showFilters = ref<boolean>(false)
|
||
const viewMode = ref<'list' | 'calendar'>('list')
|
||
const checkingAuth = ref<boolean>(false)
|
||
const scrollTop = ref<number>(0)
|
||
|
||
// Data arrays
|
||
const allRecords = ref<UTSJSONObject[]>([])
|
||
const filteredRecords = ref<UTSJSONObject[]>([])
|
||
const groupedRecords = ref<UTSJSONObject[]>([])
|
||
|
||
// Filters
|
||
const startDate = ref<string>('')
|
||
const endDate = ref<string>('')
|
||
const selectedProjects = ref<string[]>([])
|
||
const projectFilters = ref<UTSJSONObject[]>([])
|
||
|
||
// Pagination
|
||
const currentPage = ref<number>(1)
|
||
const pageSize = ref<number>(20)
|
||
const hasMore = ref<boolean>(true) // Statistics
|
||
const totalRecords = ref<number>(0)
|
||
const totalHours = ref<number>(0)
|
||
const averageScore = ref<number>(0)
|
||
const currentStreak = ref<number>(0) // Page parameters
|
||
const studentId = ref<string>('')
|
||
const fromStats = ref<boolean>(false)
|
||
const studentName = ref<string>('')
|
||
const pageReady = ref<boolean>(false)
|
||
|
||
// Initialize methods declarations - declared early to avoid reference errors
|
||
const initializeDates = () => {
|
||
// Set default date range to last 30 days
|
||
const now = new Date()
|
||
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
|
||
|
||
// Format dates as YYYY-MM-DD
|
||
endDate.value = now.toISOString().split('T')[0]
|
||
startDate.value = thirtyDaysAgo.toISOString().split('T')[0]
|
||
}
|
||
// Forward declaration of loadRecords - will be implemented later
|
||
let loadRecords: () => Promise<void> = async () => {
|
||
// Default implementation to avoid initialization error
|
||
console.log('loadRecords not yet implemented')
|
||
}
|
||
|
||
// Get current student ID
|
||
const getCurrentStudentId = () : string => {
|
||
// If student ID is provided from page parameters (e.g., from stats page), use it
|
||
if (studentId.value.length > 0) {
|
||
return studentId.value
|
||
}
|
||
|
||
// Use unified getCurrentUserId function from store
|
||
return getCurrentUserId()
|
||
}
|
||
|
||
|
||
|
||
const calculateCurrentStreak = () => {
|
||
if (allRecords.value.length === 0) {
|
||
currentStreak.value = 0
|
||
return
|
||
}
|
||
// Sort records by date (use start_time if available, fallback to created_at)
|
||
const sortedRecords = [...allRecords.value].sort((a, b) => {
|
||
const dateA = new Date((a.getString('start_time') ?? '') ?? (a.getString('created_at') ?? ''))
|
||
const dateB = new Date((b.getString('start_time') ?? '') ?? (b.getString('created_at') ?? ''))
|
||
return dateB.getTime() - dateA.getTime()
|
||
})
|
||
|
||
// Calculate streak from most recent date
|
||
let streak = 0
|
||
let currentDate = new Date()
|
||
// UTS Android Date API compatibility: setHours only accepts one parameter
|
||
currentDate.setHours(0)
|
||
currentDate.setMinutes(0)
|
||
currentDate.setSeconds(0)
|
||
currentDate.setMilliseconds(0)
|
||
|
||
for (const record of sortedRecords) {
|
||
// Fix string concatenation type issue with explicit fallback
|
||
let recordTimestamp = (record as UTSJSONObject).getString('start_time') ?? ''
|
||
if (recordTimestamp.length === 0) {
|
||
recordTimestamp = (record as UTSJSONObject).getString('created_at') ?? ''
|
||
}
|
||
const recordDate = new Date(recordTimestamp)
|
||
// UTS Android Date API compatibility: setHours only accepts one parameter
|
||
recordDate.setHours(0)
|
||
recordDate.setMinutes(0)
|
||
recordDate.setSeconds(0)
|
||
recordDate.setMilliseconds(0)
|
||
|
||
const daysDiff = Math.floor((currentDate.getTime() - recordDate.getTime()) / (24 * 60 * 60 * 1000))
|
||
|
||
if (daysDiff === streak || (streak === 0 && daysDiff <= 1)) {
|
||
if (daysDiff === streak) {
|
||
streak++
|
||
currentDate = new Date(recordDate.getTime() - 24 * 60 * 60 * 1000)
|
||
}
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
|
||
currentStreak.value = streak
|
||
}
|
||
const formatDateForGroup = (dateString : string) : string => {
|
||
const date = new Date(dateString)
|
||
const now = new Date()
|
||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000)
|
||
const targetDate = new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
||
|
||
if (targetDate.getTime() === today.getTime()) {
|
||
return '今天'
|
||
} else if (targetDate.getTime() === yesterday.getTime()) {
|
||
return '昨天'
|
||
} else {
|
||
return formatDate(dateString, 'MM月DD日')
|
||
}
|
||
}
|
||
const groupRecordsByDate = () => {
|
||
// Use Map for better type safety in UTS Android
|
||
const groups = new Map<string, UTSJSONObject>()
|
||
|
||
filteredRecords.value.forEach(record => {
|
||
const date = ((record as UTSJSONObject).getString('created_at') ?? '').split('T')[0]
|
||
const formattedDate = formatDateForGroup(date)
|
||
|
||
if (!groups.has(date)) {
|
||
const groupData = {
|
||
date: formattedDate,
|
||
rawDate: date,
|
||
records: [] as UTSJSONObject[],
|
||
count: 0
|
||
} as UTSJSONObject
|
||
groups.set(date, groupData)
|
||
}
|
||
|
||
const group = groups.get(date)!
|
||
const records = group.getArray('records') as UTSJSONObject[]
|
||
records.push(record)
|
||
group.set('count', records.length)
|
||
})
|
||
|
||
// Convert Map to array and sort by date (newest first)
|
||
const groupsArray : UTSJSONObject[] = []
|
||
groups.forEach((group, key) => {
|
||
groupsArray.push(group)
|
||
})
|
||
|
||
groupedRecords.value = groupsArray.sort((a : UTSJSONObject, b : UTSJSONObject) => {
|
||
const dateA = a.getString('rawDate') ?? ''
|
||
const dateB = b.getString('rawDate') ?? ''
|
||
return new Date(dateB).getTime() - new Date(dateA).getTime()
|
||
})
|
||
}
|
||
const updateFilteredRecords = () => {
|
||
let filtered = [...allRecords.value]
|
||
// Apply project filter (client-side filtering based on plan names)
|
||
if (selectedProjects.value.length > 0 && !selectedProjects.value.includes('all')) {
|
||
filtered = filtered.filter(record => {
|
||
const planItem = (record as UTSJSONObject).getAny('ak_training_plan_items')
|
||
if (planItem != null) {
|
||
const plan = (planItem as UTSJSONObject).getAny('ak_training_plans')
|
||
if (plan != null) {
|
||
const planId = (plan as UTSJSONObject).getString('id') ?? ''
|
||
return selectedProjects.value.includes(planId)
|
||
}
|
||
}
|
||
return false
|
||
})
|
||
}
|
||
|
||
filteredRecords.value = filtered
|
||
groupRecordsByDate()
|
||
}
|
||
const updateStatistics = () => {
|
||
totalRecords.value = allRecords.value.length
|
||
// Calculate total hours from duration_sec field
|
||
const totalSeconds = allRecords.value.reduce((sum, record) =>
|
||
sum + ((record as UTSJSONObject).getNumber('duration_sec') ?? 0), 0
|
||
)
|
||
totalHours.value = Math.round(totalSeconds / 3600 * 10) / 10
|
||
|
||
// Calculate average score using our getRecordScore function
|
||
const scoredRecords = allRecords.value.filter(record =>
|
||
getRecordScore(record) > 0
|
||
)
|
||
if (scoredRecords.length > 0) {
|
||
const totalScore = scoredRecords.reduce((sum, record) =>
|
||
sum + getRecordScore(record), 0
|
||
)
|
||
averageScore.value = totalScore / scoredRecords.length
|
||
} else {
|
||
averageScore.value = 0
|
||
}
|
||
|
||
// Calculate current streak
|
||
calculateCurrentStreak()
|
||
}
|
||
|
||
const toggleProjectFilter = (index : number) => {
|
||
// UTS Android compatibility: Use explicit boolean comparison instead of ! operator
|
||
const currentSelected = (projectFilters.value[index] as UTSJSONObject).getBoolean('selected') ?? false
|
||
;(projectFilters.value[index] as UTSJSONObject).set('selected', !currentSelected)
|
||
}
|
||
const resetFilters = () => {
|
||
selectedProjects.value = []
|
||
projectFilters.value.forEach(filter => {
|
||
(filter as UTSJSONObject).set('selected', false)
|
||
})
|
||
// Call initializeDates and loadRecords functions
|
||
initializeDates()
|
||
currentPage.value = 1 // Reset pagination
|
||
loadRecords() // Reload data with reset filters
|
||
}
|
||
const applyFilters = () => {
|
||
// Fix type safety for filter operations
|
||
selectedProjects.value = projectFilters.value
|
||
.filter(filter => {
|
||
const selected = (filter as UTSJSONObject).getBoolean('selected') ?? false
|
||
return selected
|
||
})
|
||
.map(filter => (filter as UTSJSONObject).getString('id') ?? '')
|
||
showFilters.value = false
|
||
currentPage.value = 1 // Reset pagination
|
||
|
||
// Apply client-side filtering instead of reloading from server
|
||
updateFilteredRecords()
|
||
}
|
||
const initializeProjectFilters = async () => { try {
|
||
// Get all active training plans to use as filter options
|
||
const result = await supaClient
|
||
.from('ak_training_plans')
|
||
.select('id, plan_name', {})
|
||
.in('status', ['active', 'published'])
|
||
.order('plan_name', { ascending: true })
|
||
.execute()
|
||
|
||
// Access result properties with proper UTS compatibility
|
||
const data = result.data as UTSJSONObject[]
|
||
const error = result.error
|
||
|
||
if (error!=null) {
|
||
console.error('加载项目筛选器失败:', error)
|
||
// Provide fallback filters as UTSJSONObjects
|
||
const fallbackFilters: UTSJSONObject[] = []
|
||
const allFilter = {} as UTSJSONObject
|
||
allFilter.set('id', 'all')
|
||
allFilter.set('name', '全部项目')
|
||
allFilter.set('selected', false)
|
||
fallbackFilters.push(allFilter)
|
||
|
||
const runningFilter = {} as UTSJSONObject
|
||
runningFilter.set('id', 'running')
|
||
runningFilter.set('name', '跑步训练')
|
||
runningFilter.set('selected', false)
|
||
fallbackFilters.push(runningFilter)
|
||
|
||
const strengthFilter = {} as UTSJSONObject
|
||
strengthFilter.set('id', 'strength')
|
||
strengthFilter.set('name', '力量训练')
|
||
strengthFilter.set('selected', false)
|
||
fallbackFilters.push(strengthFilter)
|
||
|
||
const cardioFilter = {} as UTSJSONObject
|
||
cardioFilter.set('id', 'cardio')
|
||
cardioFilter.set('name', '有氧训练')
|
||
cardioFilter.set('selected', false)
|
||
fallbackFilters.push(cardioFilter)
|
||
|
||
projectFilters.value = fallbackFilters
|
||
return
|
||
}
|
||
|
||
// Add "All" option at the beginning
|
||
const filters: UTSJSONObject[] = []
|
||
const allOption = {} as UTSJSONObject
|
||
allOption.set('id', 'all')
|
||
allOption.set('name', '全部项目')
|
||
allOption.set('selected', false)
|
||
filters.push(allOption)
|
||
// Add actual training plans
|
||
if (data!=null) {
|
||
// Use explicit typing for UTS Android forEach compatibility
|
||
const dataArray = data as UTSJSONObject[]
|
||
dataArray.forEach((plan: UTSJSONObject) => {
|
||
const filterOption = {} as UTSJSONObject
|
||
filterOption.set('id', plan.getString('id') ?? '')
|
||
filterOption.set('name', plan.getString('plan_name') ?? '未知项目')
|
||
filterOption.set('selected', false)
|
||
filters.push(filterOption)
|
||
})
|
||
}
|
||
|
||
projectFilters.value = filters } catch (err) {
|
||
console.error('初始化项目筛选器异常:', err)
|
||
// Provide fallback filters as UTSJSONObjects
|
||
const fallbackFilters: UTSJSONObject[] = []
|
||
const allFilter = {} as UTSJSONObject
|
||
allFilter.set('id', 'all')
|
||
allFilter.set('name', '全部项目')
|
||
allFilter.set('selected', false)
|
||
fallbackFilters.push(allFilter)
|
||
|
||
const defaultFilter = {} as UTSJSONObject
|
||
defaultFilter.set('id', 'default')
|
||
defaultFilter.set('name', '默认训练')
|
||
defaultFilter.set('selected', false)
|
||
fallbackFilters.push(defaultFilter)
|
||
|
||
projectFilters.value = fallbackFilters
|
||
} }
|
||
// Load records with supaClient - assign to pre-declared variable
|
||
loadRecords = async () => {
|
||
|
||
try {
|
||
|
||
recordsLoading.value = true
|
||
error.value = null
|
||
const currentStudentId = getCurrentStudentId()
|
||
if (currentStudentId.length === 0) {
|
||
// 如果没有有效的学生ID,跳转到登录页面
|
||
console.warn('无法获取有效的学生ID,跳转到登录页面')
|
||
uni.showToast({
|
||
title: '登录已过期,请重新登录',
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
setTimeout(() => {
|
||
uni.redirectTo({
|
||
url: '/pages/user/login'
|
||
})
|
||
}, 1000)
|
||
return
|
||
}
|
||
|
||
// Build query with correct relationships
|
||
// ak_training_records -> ak_training_plan_items -> ak_training_plans
|
||
let query = supaClient
|
||
.from('ak_training_records')
|
||
.select('*, ak_training_plan_items(*, ak_training_plans(*))', {})
|
||
.eq('user_id', currentStudentId)
|
||
// Apply date filters
|
||
if (startDate.value.length > 0 && endDate.value.length > 0) {
|
||
query = query
|
||
.gte('created_at', startDate.value + 'T00:00:00Z')
|
||
.lte('created_at', endDate.value + 'T23:59:59Z')
|
||
}
|
||
|
||
// Note: Project filtering will be done client-side since we're now using plan_items
|
||
// The ak_training_plan_items don't directly reference projects
|
||
// Execute query with proper pagination
|
||
const offset = (currentPage.value - 1) * pageSize.value
|
||
const result = await query
|
||
.order('created_at', { ascending: false })
|
||
.range(offset, offset + pageSize.value - 1)
|
||
.execute()
|
||
|
||
console.log(result)
|
||
if (result.status == 200) {
|
||
const newRecords = result.data as UTSJSONObject[]
|
||
|
||
// For first page, replace all records; for subsequent pages, append
|
||
if (currentPage.value === 1) {
|
||
allRecords.value = newRecords
|
||
} else {
|
||
// Check for duplicates and append only new records
|
||
const existingIds = new Set(allRecords.value.map(record =>
|
||
record.getString('id') ?? ''))
|
||
const uniqueNewRecords = newRecords.filter(record =>
|
||
!existingIds.has(record.getString('id') ?? ''))
|
||
allRecords.value = [...allRecords.value, ...uniqueNewRecords]
|
||
}
|
||
|
||
// Update hasMore based on returned data length
|
||
hasMore.value = newRecords.length === pageSize.value
|
||
|
||
updateFilteredRecords()
|
||
updateStatistics()
|
||
} else {
|
||
const errorMsg = result.error?.message ?? '加载失败'
|
||
console.error('数据库查询失败:', errorMsg)
|
||
error.value = errorMsg
|
||
}
|
||
|
||
} catch (err) {
|
||
const errorMsg = err instanceof Error ? err.message : '加载失败,请重试'
|
||
console.error('加载记录失败:', err)
|
||
error.value = errorMsg
|
||
|
||
} finally {
|
||
recordsLoading.value = false
|
||
refreshing.value = false
|
||
}
|
||
}
|
||
const loadMoreRecords = () => {
|
||
currentPage.value++
|
||
loadRecords()
|
||
}
|
||
const viewRecordDetail = (record : UTSJSONObject) => {
|
||
const recordId = (record as UTSJSONObject).getString('id') ?? ''
|
||
if (recordId.length > 0) {
|
||
uni.navigateTo({
|
||
url: `/pages/sport/student/record-detail?id=${recordId}`
|
||
})
|
||
}
|
||
}
|
||
const retryLoad = () => {
|
||
// 重试前检查登录状态
|
||
const currentStudentId = getCurrentStudentId()
|
||
if (currentStudentId.length === 0) {
|
||
console.warn('重试时发现用户未登录,跳转到登录页面')
|
||
uni.redirectTo({
|
||
url: '/pages/user/login'
|
||
})
|
||
return
|
||
}
|
||
|
||
error.value = null
|
||
recordsLoading.value = true
|
||
loadRecords()
|
||
}
|
||
const goBack = () => {
|
||
uni.navigateBack()
|
||
}
|
||
// Refresh functionality
|
||
const onRefresh = () => {
|
||
refreshing.value = true
|
||
currentPage.value = 1
|
||
hasMore.value = true
|
||
scrollTop.value = 0
|
||
loadRecords()
|
||
}
|
||
|
||
const onRefreshRestore = () => {
|
||
refreshing.value = false
|
||
}
|
||
// UTSJSONObject safe access methods
|
||
const getProjectName = (record : UTSJSONObject) : string => {
|
||
// Navigate through: record -> ak_training_plan_items -> ak_training_plans -> plan_name
|
||
const planItem = (record as UTSJSONObject).getAny('ak_training_plan_items')
|
||
if (planItem != null) {
|
||
const plan = (planItem as UTSJSONObject).getAny('ak_training_plans')
|
||
if (plan != null) {
|
||
return (plan as UTSJSONObject).getString('plan_name') ?? '未知项目'
|
||
}
|
||
}
|
||
// Fallback to activity_type if plan name not available
|
||
return (record as UTSJSONObject).getString('activity_type') ?? '未知项目'
|
||
}
|
||
const getRecordTime = (record : UTSJSONObject) : string => {
|
||
// Try start_time first, then created_at
|
||
const startTime = (record as UTSJSONObject).getString('start_time') ?? ''
|
||
const timestamp = startTime ?? ((record as UTSJSONObject).getString('created_at') ?? '')
|
||
if (timestamp!='') {
|
||
return formatDate(timestamp, 'HH:mm')
|
||
}
|
||
return ''
|
||
}
|
||
const getDurationText = (record : UTSJSONObject) : string => {
|
||
// Try duration_minutes first, then convert duration_sec to minutes
|
||
let minutes = (record as UTSJSONObject).getNumber('duration_minutes') ?? 0
|
||
if (minutes === 0) {
|
||
const seconds = (record as UTSJSONObject).getNumber('duration_sec') ?? 0
|
||
minutes = Math.round(seconds / 60)
|
||
}
|
||
|
||
if (minutes >= 60) {
|
||
const hours = Math.floor(minutes / 60)
|
||
const remainingMinutes = minutes % 60
|
||
return `${hours}h${remainingMinutes}m`
|
||
}
|
||
return `${minutes}m`
|
||
}
|
||
const getCompletionRate = (record : UTSJSONObject) : number => {
|
||
// Calculate completion rate from available data
|
||
// If we have target values from plan items, compare with actual values
|
||
const planItem = (record as UTSJSONObject).getAny('ak_training_plan_items')
|
||
if (planItem != null) {
|
||
const targetValue = (planItem as UTSJSONObject).getNumber('target_value') ?? 0
|
||
if (targetValue > 0) {
|
||
// For distance-based activities
|
||
const actualDistance = ((record as UTSJSONObject).getNumber('distance_km') ?? 0) * 1000 // Convert to meters
|
||
if (actualDistance > 0) {
|
||
return Math.min(100, Math.round((actualDistance / targetValue) * 100))
|
||
}
|
||
// For time-based activities
|
||
const actualDuration = (record as UTSJSONObject).getNumber('duration_sec') ?? 0
|
||
if (actualDuration > 0) {
|
||
return Math.min(100, Math.round((actualDuration / targetValue) * 100))
|
||
}
|
||
}
|
||
}
|
||
// Default completion rate based on whether the record exists and has data
|
||
const distance = (record as UTSJSONObject).getNumber('distance_km') ?? 0
|
||
const duration = (record as UTSJSONObject).getNumber('duration_sec') ?? 0
|
||
const calories = (record as UTSJSONObject).getNumber('calories') ?? 0
|
||
|
||
if (distance > 0 || duration > 0 || calories > 0) {
|
||
return 100 // Consider it completed if we have any meaningful data
|
||
}
|
||
return 0
|
||
}
|
||
const getRecordScore = (record : UTSJSONObject) : number => {
|
||
// Try to get score from various possible fields
|
||
let score = (record as UTSJSONObject).getNumber('score') ?? 0
|
||
if (score > 0) return score
|
||
|
||
score = (record as UTSJSONObject).getNumber('final_score') ?? 0
|
||
if (score > 0) return score
|
||
|
||
// Calculate a basic score based on completion rate and performance
|
||
const completionRate = getCompletionRate(record)
|
||
if (completionRate >= 100) {
|
||
// Bonus points for exceeding targets
|
||
const avgHeartRate = (record as UTSJSONObject).getNumber('avg_heart_rate') ?? 0
|
||
const maxHeartRate = (record as UTSJSONObject).getNumber('max_heart_rate') ?? 0
|
||
|
||
// Basic scoring: completion gives 70-85 points, heart rate performance adds 0-15 points
|
||
let baseScore = 75 + Math.random() * 10 // Simulate variable performance
|
||
|
||
if (avgHeartRate > 0 && avgHeartRate < 180) {
|
||
baseScore += 10 // Good heart rate control
|
||
}
|
||
|
||
return Math.min(100, Math.round(baseScore))
|
||
} else if (completionRate >= 80) {
|
||
return Math.round(60 + (completionRate - 80) * 0.5) // 60-70 points
|
||
} else if (completionRate >= 50) {
|
||
return Math.round(40 + (completionRate - 50) * 0.67) // 40-60 points
|
||
}
|
||
|
||
return Math.round(completionRate * 0.4) // 0-40 points for low completion
|
||
}
|
||
|
||
const getScoreClass = (record : UTSJSONObject) : string => {
|
||
const score = getRecordScore(record)
|
||
if (score >= 90) return 'score-excellent'
|
||
if (score >= 80) return 'score-good'
|
||
if (score >= 70) return 'score-average'
|
||
if (score >= 60) return 'score-pass'
|
||
return 'score-fail'
|
||
}
|
||
const getRecordNotes = (record : UTSJSONObject) : string => {
|
||
// Try various note fields
|
||
let notes =record.getString('notes') ?? ''
|
||
if (notes!='') return notes
|
||
|
||
notes = (record as UTSJSONObject).getString('description') ?? ''
|
||
if (notes!='') return notes
|
||
|
||
// Generate basic notes from data if available
|
||
const distance = (record as UTSJSONObject).getNumber('distance_km') ?? 0
|
||
const calories = (record as UTSJSONObject).getNumber('calories') ?? 0
|
||
const avgHeartRate = (record as UTSJSONObject).getNumber('avg_heart_rate') ?? 0
|
||
|
||
let autoNotes = Array<string>()
|
||
if (distance > 0) {
|
||
autoNotes.push(`距离: ${distance.toFixed(2)}km`)
|
||
}
|
||
if (calories > 0) {
|
||
autoNotes.push(`消耗: ${calories}卡`)
|
||
}
|
||
if (avgHeartRate > 0) {
|
||
autoNotes.push(`平均心率: ${avgHeartRate}bpm`)
|
||
}
|
||
|
||
return autoNotes.join(' • ')
|
||
}
|
||
const getRecordStatusText = (record : UTSJSONObject) : string => {
|
||
// Check if the record has meaningful data
|
||
const distance = (record as UTSJSONObject).getNumber('distance_km') ?? 0
|
||
const duration = (record as UTSJSONObject).getNumber('duration_sec') ?? 0
|
||
const calories = (record as UTSJSONObject).getNumber('calories') ?? 0
|
||
|
||
if (distance > 0 || duration > 0 || calories > 0) {
|
||
return '已完成'
|
||
}
|
||
|
||
const status = (record as UTSJSONObject).getString('status') ?? 'unknown'
|
||
switch (status) {
|
||
case 'completed': return '已完成'
|
||
case 'active': return '进行中'
|
||
case 'pending': return '待开始'
|
||
default: return '未知状态'
|
||
}
|
||
}
|
||
const getStatusClass = (record : UTSJSONObject) : string => {
|
||
// Check if the record has meaningful data
|
||
const distance = (record as UTSJSONObject).getNumber('distance_km') ?? 0
|
||
const duration = (record as UTSJSONObject).getNumber('duration_sec') ?? 0
|
||
const calories = (record as UTSJSONObject).getNumber('calories') ?? 0
|
||
|
||
if (distance > 0 || duration > 0 || calories > 0) {
|
||
return 'status-completed'
|
||
}
|
||
|
||
const status = (record as UTSJSONObject).getString('status') ?? 'unknown'
|
||
switch (status) {
|
||
case 'completed': return 'status-completed'
|
||
case 'active': return 'status-active'
|
||
case 'pending': return 'status-pending'
|
||
default: return 'status-default'
|
||
}
|
||
}
|
||
|
||
onLoad((options : OnLoadOptions) => {
|
||
// Get student ID from page parameters if provided
|
||
studentId.value = options['id'] ?? ''
|
||
if (studentId.value.length > 0) {
|
||
fromStats.value = true
|
||
}
|
||
studentName.value = decodeURIComponent(options['studentName'] ?? '') ?? ''
|
||
})
|
||
onReady(() => {
|
||
// Initialize page state immediately
|
||
checkingAuth.value = true
|
||
pageReady.value = false
|
||
|
||
|
||
// 等待DOM渲染完成
|
||
setTimeout(() => {
|
||
// 首先检查登录状态
|
||
const currentStudentId = getCurrentStudentId()
|
||
if (currentStudentId.length === 0) {
|
||
console.warn('用户未登录或无效ID,跳转到登录页面')
|
||
uni.showToast({
|
||
title: '请先登录',
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
setTimeout(() => {
|
||
uni.redirectTo({
|
||
url: '/pages/user/login'
|
||
})
|
||
}, 1000)
|
||
return
|
||
}
|
||
|
||
checkingAuth.value = false
|
||
recordsLoading.value = true
|
||
initializeDates()
|
||
// Handle async operations with Promise chains
|
||
initializeProjectFilters()
|
||
.then(() => {
|
||
loadRecords()
|
||
})
|
||
.then(() => {
|
||
// 标记页面已准备好
|
||
pageReady.value = true
|
||
})
|
||
.catch((error) => {
|
||
console.error('页面初始化失败:', error)
|
||
checkingAuth.value = false
|
||
recordsLoading.value = false
|
||
pageReady.value = true
|
||
}) }, 100)
|
||
}
|
||
)
|
||
|
||
onMounted(() => {
|
||
// Initialize screen width
|
||
screenWidth.value = uni.getSystemInfoSync().windowWidth
|
||
})
|
||
|
||
// Handle resize events for responsive design
|
||
onResize((size) => {
|
||
screenWidth.value = size.size.windowWidth
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.records-history-container {
|
||
display: flex;
|
||
flex:1;
|
||
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
|
||
padding-bottom: 40rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.header {
|
||
padding: 40rpx 30rpx 30rpx;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
backdrop-filter: blur(10px);
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
.header-left {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
flex: 1;
|
||
}
|
||
|
||
.header-left .back-btn {
|
||
margin-right: 20rpx;
|
||
}
|
||
|
||
.back-btn {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
padding: 12rpx 20rpx;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border: none;
|
||
border-radius: 15rpx;
|
||
color: #FFFFFF;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.back-btn simple-icon {
|
||
margin-right: 8rpx;
|
||
}
|
||
|
||
.back-btn:active {
|
||
background: rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
.title {
|
||
font-size: 48rpx;
|
||
font-weight: bold;
|
||
color: #FFFFFF;
|
||
flex: 1;
|
||
}
|
||
.header-actions {
|
||
flex-direction: row;
|
||
}
|
||
|
||
.header-actions .filter-btn {
|
||
margin-right: 15rpx;
|
||
}
|
||
|
||
.header-actions .filter-btn:last-child {
|
||
margin-right: 0;
|
||
}
|
||
|
||
.filter-btn {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
padding: 15rpx 25rpx;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border: none;
|
||
border-radius: 15rpx;
|
||
color: #FFFFFF;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.filter-btn simple-icon {
|
||
margin-right: 8rpx;
|
||
}
|
||
|
||
.filter-btn:active {
|
||
background: rgba(255, 255, 255, 0.3);
|
||
}
|
||
/* Filter Panel */
|
||
.filter-panel {
|
||
padding: 30rpx;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.filter-panel > * {
|
||
margin-bottom: 25rpx;
|
||
}
|
||
|
||
.filter-panel > *:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
.filter-row {
|
||
}
|
||
|
||
.filter-row > * {
|
||
margin-right: 15rpx;
|
||
}
|
||
|
||
.filter-row > *:last-child {
|
||
margin-right: 0;
|
||
}
|
||
|
||
.filter-label {
|
||
font-size: 28rpx;
|
||
color: #FFFFFF;
|
||
font-weight: 400;
|
||
}
|
||
.date-range {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.date-range > * {
|
||
margin-right: 15rpx;
|
||
}
|
||
|
||
.date-range > *:last-child {
|
||
margin-right: 0;
|
||
}
|
||
|
||
.date-separator {
|
||
color: #FFFFFF;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.project-filters {
|
||
margin-top: 15rpx;
|
||
}
|
||
.filter-chips {
|
||
flex-direction: row;
|
||
padding: 10rpx 0;
|
||
}
|
||
|
||
.filter-chips .filter-chip {
|
||
margin-right: 15rpx;
|
||
}
|
||
|
||
.filter-chips .filter-chip:last-child {
|
||
margin-right: 0;
|
||
}
|
||
|
||
.filter-chip {
|
||
padding: 12rpx 20rpx;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border-radius: 25rpx;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.filter-chip.active {
|
||
background: rgba(255, 255, 255, 0.9);
|
||
}
|
||
|
||
.chip-text {
|
||
font-size: 26rpx;
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
.filter-chip.active .chip-text {
|
||
color: #6366F1;
|
||
}
|
||
.filter-actions {
|
||
flex-direction: row;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.filter-actions .reset-btn,
|
||
.filter-actions .apply-btn {
|
||
margin-left: 20rpx;
|
||
}
|
||
|
||
.filter-actions .reset-btn:first-child,
|
||
.filter-actions .apply-btn:first-child {
|
||
margin-left: 0;
|
||
}
|
||
|
||
.reset-btn,
|
||
.apply-btn {
|
||
padding: 15rpx 30rpx;
|
||
border: none;
|
||
border-radius: 15rpx;
|
||
font-size: 28rpx;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.reset-btn {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
color: #FFFFFF;
|
||
}
|
||
|
||
.apply-btn {
|
||
background: rgba(255, 255, 255, 0.9);
|
||
color: #6366F1;
|
||
}
|
||
|
||
/* Content */
|
||
.content {
|
||
flex: 1;
|
||
padding: 30rpx;
|
||
background: #F8FAFC;
|
||
border-radius: 30rpx 30rpx 0 0;
|
||
margin-top: 20rpx;
|
||
}
|
||
|
||
.content.large-screen {
|
||
padding: 40rpx;
|
||
}
|
||
|
||
/* Statistics */
|
||
.stats-section {
|
||
margin-bottom: 30rpx;
|
||
}
|
||
.stats-grid {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.stats-grid .stat-card {
|
||
width: calc(50% - 10rpx);
|
||
margin-right: 20rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.stats-grid .stat-card:nth-child(2n) {
|
||
margin-right: 0;
|
||
}
|
||
|
||
@media (min-width: 768px) {
|
||
.stats-grid {
|
||
grid-template-columns: repeat(4, 1fr);
|
||
}
|
||
}
|
||
.stat-card {
|
||
background: #FFFFFF;
|
||
padding: 25rpx;
|
||
border-radius: 20rpx;
|
||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.stat-card .stat-icon {
|
||
margin-right: 20rpx;
|
||
}
|
||
|
||
.stat-icon {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
background: rgba(99, 102, 241, 0.1);
|
||
border-radius: 40rpx;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.stat-content {
|
||
flex: 1;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
color: #1E293B;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 24rpx;
|
||
color: #64748B;
|
||
}
|
||
|
||
/* Records Section */
|
||
.records-section {
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.section-header {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
color: #1E293B;
|
||
}
|
||
|
||
.view-toggle {
|
||
flex-direction: row;
|
||
gap: 10rpx;
|
||
}
|
||
|
||
.toggle-btn {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
background: #F1F5F9;
|
||
border: none;
|
||
border-radius: 15rpx;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.toggle-btn.active {
|
||
background: #6366F1;
|
||
}
|
||
|
||
.toggle-btn.active simple-icon {
|
||
color: #FFFFFF !important;
|
||
}
|
||
/* Records List */
|
||
.records-list {
|
||
}
|
||
|
||
.records-list .date-group {
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.records-list .date-group:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.date-group {
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.date-header {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 20rpx;
|
||
padding: 0 10rpx;
|
||
}
|
||
|
||
.date-text {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #1E293B;
|
||
}
|
||
|
||
.count-text {
|
||
font-size: 24rpx;
|
||
color: #64748B;
|
||
}
|
||
.group-records {
|
||
}
|
||
|
||
.group-records .record-card {
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.group-records .record-card:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.record-item {
|
||
background: #FFFFFF;
|
||
border-radius: 20rpx;
|
||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||
flex-direction: row;
|
||
gap: 20rpx;
|
||
padding: 25rpx;
|
||
}
|
||
|
||
.record-item:active {
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.record-timeline {
|
||
align-items: center;
|
||
width: 40rpx;
|
||
}
|
||
|
||
.timeline-dot {
|
||
width: 20rpx;
|
||
height: 20rpx;
|
||
border-radius: 10rpx;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
.timeline-line {
|
||
width: 2rpx;
|
||
flex: 1;
|
||
background: #E5E7EB;
|
||
min-height: 60rpx;
|
||
}
|
||
|
||
.status-completed .timeline-dot,
|
||
.status-graded .timeline-dot {
|
||
background: #10B981;
|
||
}
|
||
|
||
.status-pending .timeline-dot {
|
||
background: #F59E0B;
|
||
}
|
||
|
||
.status-draft .timeline-dot {
|
||
background: #6B7280;
|
||
}
|
||
|
||
.status-default .timeline-dot {
|
||
background: #D1D5DB;
|
||
}
|
||
|
||
.record-content {
|
||
flex: 1;
|
||
gap: 15rpx;
|
||
}
|
||
|
||
.record-header {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.record-title {
|
||
font-size: 30rpx;
|
||
font-weight: 400;
|
||
color: #1E293B;
|
||
flex: 1;
|
||
}
|
||
|
||
.record-time {
|
||
font-size: 24rpx;
|
||
color: #64748B;
|
||
}
|
||
|
||
.record-details {
|
||
flex-direction: row;
|
||
gap: 25rpx;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.detail-item {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.detail-text {
|
||
font-size: 24rpx;
|
||
color: #6B7280;
|
||
}
|
||
|
||
.detail-text.score {
|
||
font-weight: 400;
|
||
}
|
||
|
||
.score-excellent {
|
||
color: #10B981;
|
||
}
|
||
|
||
.score-good {
|
||
color: #3B82F6;
|
||
}
|
||
|
||
.score-average {
|
||
color: #F59E0B;
|
||
}
|
||
|
||
.score-pass {
|
||
color: #8B5CF6;
|
||
}
|
||
|
||
.score-fail {
|
||
color: #EF4444;
|
||
}
|
||
|
||
.record-notes {
|
||
padding: 15rpx;
|
||
background: #F8FAFC;
|
||
border-radius: 15rpx;
|
||
}
|
||
|
||
.notes-text {
|
||
font-size: 26rpx;
|
||
color: #374151;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.record-status {
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.status-badge {
|
||
padding: 6rpx 12rpx;
|
||
border-radius: 15rpx;
|
||
font-size: 22rpx;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.status-completed,
|
||
.status-graded {
|
||
background: #D1FAE5;
|
||
color: #065F46;
|
||
}
|
||
|
||
.status-pending {
|
||
background: #FEF3C7;
|
||
color: #92400E;
|
||
}
|
||
|
||
.status-draft {
|
||
background: #F3F4F6;
|
||
color: #6B7280;
|
||
}
|
||
|
||
.status-default {
|
||
background: #E5E7EB;
|
||
color: #374151;
|
||
}
|
||
|
||
.status-text {
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
/* Calendar View */
|
||
.calendar-view {
|
||
background: #FFFFFF;
|
||
padding: 60rpx;
|
||
border-radius: 20rpx;
|
||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 400rpx;
|
||
}
|
||
|
||
.view-placeholder {
|
||
font-size: 32rpx;
|
||
color: #9CA3AF;
|
||
}
|
||
|
||
/* Empty State */
|
||
.empty-state {
|
||
align-items: center;
|
||
padding: 80rpx 40rpx;
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 32rpx;
|
||
color: #6B7280;
|
||
margin: 20rpx 0 10rpx;
|
||
}
|
||
|
||
.empty-hint {
|
||
font-size: 26rpx;
|
||
color: #9CA3AF;
|
||
text-align: center;
|
||
}
|
||
|
||
/* Load More */
|
||
.load-more-section {
|
||
padding: 30rpx;
|
||
align-items: center;
|
||
}
|
||
|
||
.load-more-btn {
|
||
padding: 20rpx 40rpx;
|
||
background: #F1F5F9;
|
||
border: none;
|
||
border-radius: 15rpx;
|
||
color: #6B7280;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.load-more-btn:active {
|
||
background: #E2E8F0;
|
||
}
|
||
|
||
.loading-more-container {
|
||
padding: 30rpx;
|
||
align-items: center;
|
||
}
|
||
|
||
/* Loading & Error States */
|
||
.loading-container,
|
||
.error-container {
|
||
flex: 1;
|
||
justify-content: center;
|
||
align-items: center;
|
||
padding: 80rpx 40rpx;
|
||
}
|
||
|
||
.loading-text {
|
||
font-size: 32rpx;
|
||
color: rgba(255, 255, 255, 0.8);
|
||
}
|
||
|
||
.error-text {
|
||
font-size: 32rpx;
|
||
color: #EF4444;
|
||
margin-bottom: 30rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.retry-btn {
|
||
padding: 20rpx 40rpx;
|
||
background: #6366F1;
|
||
color: #FFFFFF;
|
||
border: none;
|
||
border-radius: 15rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
/* Score Color Classes */
|
||
.score-excellent {
|
||
color: #10B981 !important;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.score-good {
|
||
color: #3B82F6 !important;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.score-average {
|
||
color: #F59E0B !important;
|
||
}
|
||
|
||
.score-poor {
|
||
color: #EF4444 !important;
|
||
}
|
||
|
||
.score-default {
|
||
color: #6B7280 !important;
|
||
}
|
||
</style> |