1052 lines
23 KiB
Plaintext
1052 lines
23 KiB
Plaintext
<template>
|
|
<scroll-view direction="vertical" class="records-container" :enable-back-to-top="true">
|
|
<!-- Training Records Data -->
|
|
<supadb ref="recordsRef" collection="ak_training_records" :filter="recordsFilter" :orderby="recordsOrderBy"
|
|
@process-data="handleRecordsData" @error="handleError">
|
|
</supadb>
|
|
|
|
<!-- Header -->
|
|
<view class="header">
|
|
<text class="title">训练记录</text>
|
|
<view class="header-actions">
|
|
<button @click="exportRecords" class="export-btn" :disabled="recordsLoading">
|
|
<simple-icon type="download" :size="16" color="#FFFFFF" />
|
|
<text>导出</text>
|
|
</button>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Filter Bar -->
|
|
<view class="filter-bar">
|
|
<scroll-view class="filter-scroll" scroll-x="true">
|
|
<view class="filter-chips">
|
|
<view v-for="(filter, index) in statusFilters" :key="index" class="filter-chip"
|
|
:class="{ active: filter.value === activeStatusFilter }" @click="setStatusFilter(filter.value)">
|
|
<text class="chip-text">{{ filter.label }}</text>
|
|
<text v-if="filter.count > 0" class="chip-count">{{ filter.count }}</text>
|
|
</view>
|
|
</view>
|
|
</scroll-view>
|
|
|
|
<view class="filter-controls">
|
|
<view class="search-box">
|
|
<simple-icon type="search" :size="16" color="#9CA3AF" />
|
|
<input v-model="searchQuery" placeholder="搜索学员姓名" class="search-input" @input="onSearchInput" />
|
|
</view>
|
|
|
|
<view class="date-filters">
|
|
<input type="date" v-model="startDate" placeholder="开始日期" @change="onDateFilterChange" />
|
|
<input type="date" v-model="endDate" placeholder="结束日期" @change="onDateFilterChange" />
|
|
</view>
|
|
</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" class="loading-container">
|
|
<text class="loading-text">加载中...</text>
|
|
</view>
|
|
|
|
<scroll-view v-else class="content" :class="{ 'large-screen': isLargeScreen }" scroll-y="true">
|
|
<!-- Statistics Cards -->
|
|
<view class="stats-section">
|
|
<view class="stats-grid">
|
|
<view class="stat-card">
|
|
<text class="stat-value">{{ totalRecords }}</text>
|
|
<text class="stat-label">总记录数</text>
|
|
</view>
|
|
<view class="stat-card">
|
|
<text class="stat-value">{{ completedRecords }}</text>
|
|
<text class="stat-label">已完成</text>
|
|
</view>
|
|
<view class="stat-card">
|
|
<text class="stat-value">{{ averageScore.toFixed(1) }}</text>
|
|
<text class="stat-label">平均分</text>
|
|
</view>
|
|
<view class="stat-card">
|
|
<text class="stat-value">{{ activeStudents }}</text>
|
|
<text class="stat-label">活跃学员</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Records List -->
|
|
<view class="records-section">
|
|
<view class="section-header">
|
|
<text class="section-title">训练记录</text>
|
|
<view class="sort-controls">
|
|
<button @click="toggleSortOrder" class="sort-btn">
|
|
<text class="sort-text">{{ getSortText() }}</text>
|
|
<simple-icon :type="sortOrder === 'desc' ? 'arrow-down' : 'arrow-up'" :size="14"
|
|
color="#6B7280" />
|
|
</button>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="filteredRecords.length === 0" class="empty-state">
|
|
<simple-icon type="document" :size="48" color="#D1D5DB" />
|
|
<text class="empty-text">暂无训练记录</text>
|
|
<text class="empty-hint">学员完成训练后记录将显示在这里</text>
|
|
</view>
|
|
|
|
<view v-else class="records-list">
|
|
<view v-for="(record, index) in filteredRecords" :key="index" class="record-card"
|
|
@click="viewRecordDetail(record)">
|
|
<view class="record-header">
|
|
<view class="student-info">
|
|
<view class="student-avatar">
|
|
<text class="avatar-text">{{ getStudentInitial(record) }}</text>
|
|
</view>
|
|
<view class="student-details">
|
|
<text class="student-name">{{ getStudentName(record) }}</text>
|
|
<text class="record-time">{{ getRecordTime(record) }}</text>
|
|
</view>
|
|
</view>
|
|
<view class="record-status">
|
|
<view class="status-badge" :class="getStatusClass(record)">
|
|
<text class="status-text">{{ getRecordStatus(record) }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="record-content">
|
|
<view class="project-info">
|
|
<text class="project-name">{{ getProjectName(record) }}</text>
|
|
<text class="project-category">{{ getProjectCategory(record) }}</text>
|
|
</view>
|
|
|
|
<view class="record-metrics">
|
|
<view class="metric-item">
|
|
<text class="metric-label">训练时长</text>
|
|
<text class="metric-value">{{ getDurationText(record) }}</text>
|
|
</view>
|
|
<view class="metric-item">
|
|
<text class="metric-label">完成度</text>
|
|
<text class="metric-value">{{ getCompletionRate(record) }}%</text>
|
|
</view>
|
|
<view v-if="getRecordScore(record) > 0" class="metric-item">
|
|
<text class="metric-label">得分</text>
|
|
<text class="metric-value score" :class="getScoreClass(record)">
|
|
{{ getRecordScore(record) }}
|
|
</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="getRecordNotes(record)" class="record-notes">
|
|
<text class="notes-text">{{ getRecordNotes(record) }}</text>
|
|
</view>
|
|
|
|
<view class="record-actions">
|
|
<button @click.stop="reviewRecord(record)" class="action-btn review-btn">
|
|
<simple-icon type="eye" :size="14" color="#6366F1" />
|
|
<text>查看详情</text>
|
|
</button>
|
|
<button v-if="canGrade(record)" @click.stop="gradeRecord(record)"
|
|
class="action-btn grade-btn">
|
|
<simple-icon type="edit" :size="14" color="#10B981" />
|
|
<text>评分</text>
|
|
</button>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Load More -->
|
|
<view v-if="hasMore && !recordsLoading" class="load-more-section">
|
|
<button @click="loadMoreRecords" class="load-more-btn">
|
|
<text>加载更多</text>
|
|
</button>
|
|
</view>
|
|
</scroll-view>
|
|
</scroll-view>
|
|
</template>
|
|
|
|
<script setup lang="uts">
|
|
import { ref, computed, onMounted, nextTick } from 'vue'
|
|
import { onResize } from '@dcloudio/uni-app'
|
|
import { formatDate } from '../types'
|
|
|
|
// 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
|
|
})
|
|
|
|
// Refs
|
|
const recordsRef = ref<SupadbComponentPublicInstance | null>(null)
|
|
const error = ref<string | null>(null)
|
|
const recordsLoading = ref(false)
|
|
const searchTimeout = ref<number | null>(null)
|
|
|
|
// Data arrays
|
|
const allRecords = ref<UTSJSONObject[]>([])
|
|
const filteredRecords = ref<UTSJSONObject[]>([])
|
|
|
|
// Filters
|
|
const activeStatusFilter = ref('all')
|
|
const searchQuery = ref('')
|
|
const startDate = ref('')
|
|
const endDate = ref('')
|
|
|
|
// Sorting
|
|
const sortField = ref('created_at')
|
|
const sortOrder = ref<'asc' | 'desc'>('desc')
|
|
|
|
// Pagination
|
|
const currentPage = ref(1)
|
|
const pageSize = ref(20)
|
|
const hasMore = ref(true)
|
|
// Status filters with counts
|
|
type StatusFilter = {
|
|
label : string
|
|
value : string
|
|
count : number
|
|
}
|
|
|
|
const statusFilters = ref<StatusFilter[]>([
|
|
{ label: '全部', value: 'all', count: 0 },
|
|
{ label: '已完成', value: 'completed', count: 0 },
|
|
{ label: '进行中', value: 'in_progress', count: 0 },
|
|
{ label: '待评分', value: 'pending_review', count: 0 },
|
|
{ label: '已评分', value: 'graded', count: 0 }
|
|
])
|
|
// Computed data
|
|
const totalRecords = ref(0)
|
|
const completedRecords = ref(0)
|
|
const averageScore = ref(0)
|
|
const activeStudents = ref(0)
|
|
// Local helper functions using direct UTSJSONObject methods
|
|
const getStudentName = (record : UTSJSONObject) : string => {
|
|
return record.getString('student_name') ?? '未知学员'
|
|
}
|
|
|
|
const getStudentInitial = (record : UTSJSONObject) : string => {
|
|
const name = getStudentName(record)
|
|
return name.charAt(0).toUpperCase()
|
|
}
|
|
const getRecordTime = (record : UTSJSONObject) : string => {
|
|
const timestamp = record.getString('created_at') ?? ''
|
|
if (timestamp.length > 0) {
|
|
return formatDate(timestamp, 'MM-DD HH:mm')
|
|
}
|
|
return ''
|
|
}
|
|
|
|
const getRecordStatus = (record : UTSJSONObject) : string => {
|
|
return record.getString('status') ?? 'unknown'
|
|
}
|
|
|
|
const getStatusClass = (record : UTSJSONObject) : string => {
|
|
const status = getRecordStatus(record)
|
|
switch (status) {
|
|
case 'completed': return 'status-completed'
|
|
case 'in_progress': return 'status-progress'
|
|
case 'pending_review': return 'status-pending'
|
|
case 'graded': return 'status-graded'
|
|
default: return 'status-default'
|
|
}
|
|
}
|
|
|
|
const getProjectName = (record : UTSJSONObject) : string => {
|
|
return record.getString('project_name') ?? '未知项目'
|
|
}
|
|
|
|
const getProjectCategory = (record : UTSJSONObject) : string => {
|
|
return record.getString('project_category') ?? '其他'
|
|
}
|
|
|
|
const getDurationText = (record : UTSJSONObject) : string => {
|
|
const minutes = record.getNumber('duration_minutes') ?? 0
|
|
if (minutes >= 60) {
|
|
const hours = Math.floor(minutes / 60)
|
|
const remainingMinutes = minutes % 60
|
|
return `${hours}小时${remainingMinutes}分钟`
|
|
}
|
|
return `${minutes}分钟`
|
|
}
|
|
|
|
const getCompletionRate = (record : UTSJSONObject) : number => {
|
|
return record.getNumber('completion_rate') ?? 0
|
|
}
|
|
|
|
const getRecordScore = (record : UTSJSONObject) : number => {
|
|
return record.getNumber('score') ?? 0
|
|
}
|
|
|
|
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 => {
|
|
return record.getString('notes') ?? ''
|
|
}
|
|
|
|
const canGrade = (record : UTSJSONObject) : boolean => {
|
|
const status = getRecordStatus(record)
|
|
return status === 'completed' || status === 'pending_review'
|
|
} // Computed properties
|
|
const recordsFilter = computed(() => {
|
|
const filter = {} as UTSJSONObject
|
|
|
|
if (activeStatusFilter.value !== 'all') {
|
|
filter['status'] = activeStatusFilter.value
|
|
}
|
|
|
|
if (startDate.value.length > 0 && endDate.value.length > 0) {
|
|
filter['created_at'] = {
|
|
gte: startDate.value + 'T00:00:00Z',
|
|
lte: endDate.value + 'T23:59:59Z'
|
|
} as UTSJSONObject
|
|
}
|
|
console.log(filter)
|
|
|
|
|
|
return filter
|
|
})
|
|
const recordsOrderBy = computed(() => {
|
|
return sortField.value + '.' + sortOrder.value
|
|
})
|
|
|
|
// Methods
|
|
const initializeDates = () => {
|
|
const now = new Date()
|
|
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
|
|
|
|
endDate.value = now.toISOString().split('T')[0]
|
|
startDate.value = thirtyDaysAgo.toISOString().split('T')[0]
|
|
}
|
|
|
|
const updateFilteredRecords = () => {
|
|
let filtered = [...allRecords.value]
|
|
|
|
// Apply search filter
|
|
if (searchQuery.value.trim().length > 0) {
|
|
const query = searchQuery.value.toLowerCase()
|
|
filtered = filtered.filter(record => {
|
|
const studentName = getStudentName(record).toLowerCase()
|
|
return studentName.includes(query)
|
|
})
|
|
}
|
|
|
|
filteredRecords.value = filtered
|
|
}
|
|
|
|
const updateStatistics = () => {
|
|
totalRecords.value = allRecords.value.length
|
|
completedRecords.value = allRecords.value.filter(record =>
|
|
getRecordStatus(record) === 'completed'
|
|
).length
|
|
|
|
// Calculate average score
|
|
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
|
|
}
|
|
// Count active students
|
|
const uniqueStudents = new Set(allRecords.value.map(record =>
|
|
record.getString('student_id') ?? ''
|
|
))
|
|
activeStudents.value = uniqueStudents.size
|
|
}
|
|
const updateStatusFilterCounts = () => {
|
|
statusFilters.value.forEach((filter : StatusFilter) => {
|
|
if (filter.value === 'all') {
|
|
filter.count = allRecords.value.length as number
|
|
} else {
|
|
filter.count = allRecords.value.filter(record =>
|
|
getRecordStatus(record) === filter.value
|
|
).length as number
|
|
}
|
|
})
|
|
}
|
|
const setStatusFilter = (status : string) => {
|
|
activeStatusFilter.value = status
|
|
updateFilteredRecords()
|
|
nextTick(() => {
|
|
const ref = recordsRef.value
|
|
if (ref != null) {
|
|
const refresh = ref.refresh
|
|
if (refresh != null) {
|
|
refresh()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
const onSearchInput = (event : InputEvent) => {
|
|
// Debounce search
|
|
if (searchTimeout.value != null) {
|
|
clearTimeout(searchTimeout.value as number)
|
|
}
|
|
searchTimeout.value = setTimeout(() => {
|
|
updateFilteredRecords()
|
|
}, 300)
|
|
}
|
|
|
|
const onDateFilterChange = () => {
|
|
console.log('onDateFilterChange')
|
|
}
|
|
|
|
const toggleSortOrder = () => {
|
|
sortOrder.value = sortOrder.value === 'desc' ? 'asc' : 'desc'
|
|
}
|
|
|
|
const getSortText = () : string => {
|
|
return sortOrder.value === 'desc' ? '最新在前' : '最早在前'
|
|
}
|
|
|
|
const loadMoreRecords = () => {
|
|
currentPage.value++
|
|
// This would typically load more data from the API
|
|
}
|
|
|
|
const exportRecords = () => {
|
|
uni.showToast({
|
|
title: '导出功能开发中',
|
|
icon: 'none'
|
|
})
|
|
}
|
|
|
|
const viewRecordDetail = (record : UTSJSONObject) => {
|
|
const recordId = record.getString('id') ?? ''
|
|
if (recordId.length > 0) {
|
|
uni.navigateTo({
|
|
url: `/pages/sport/teacher/record-detail?id=${recordId}`
|
|
})
|
|
}
|
|
}
|
|
|
|
const reviewRecord = (record : UTSJSONObject) => {
|
|
viewRecordDetail(record)
|
|
}
|
|
const gradeRecord = (record : UTSJSONObject) => {
|
|
const recordId = record.getString('id') ?? ''
|
|
if (recordId.length > 0) {
|
|
uni.navigateTo({
|
|
url: `/pages/sport/teacher/grade-record?id=${recordId}`
|
|
})
|
|
}
|
|
}
|
|
const handleError = (errorData : any) => {
|
|
console.error('Records error:', errorData)
|
|
error.value = '数据加载失败,请重试'
|
|
}
|
|
|
|
const retryLoad = () => {
|
|
error.value = null
|
|
// Trigger data reload
|
|
} // UTSJSONObject safe access methods
|
|
const handleRecordsData = (data : UTSJSONObject) => {
|
|
recordsLoading.value = false
|
|
const recordsData = data.getArray('data')
|
|
if (recordsData != null) {
|
|
allRecords.value = recordsData as UTSJSONObject[]
|
|
} else {
|
|
allRecords.value = []
|
|
}
|
|
updateFilteredRecords()
|
|
updateStatistics()
|
|
updateStatusFilterCounts()
|
|
} // Lifecycle hooks
|
|
onMounted(() => {
|
|
initializeDates()
|
|
// 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-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;
|
|
}
|
|
|
|
.title {
|
|
font-size: 48rpx;
|
|
font-weight: bold;
|
|
color: #FFFFFF;
|
|
}
|
|
|
|
.header-actions {
|
|
flex-direction: row;
|
|
}
|
|
|
|
.header-actions .export-btn {
|
|
margin-right: 15rpx;
|
|
}
|
|
|
|
.header-actions .export-btn:last-child {
|
|
margin-right: 0;
|
|
}
|
|
|
|
.export-btn {
|
|
display: flex;
|
|
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;
|
|
}
|
|
|
|
.export-btn text {
|
|
margin-left: 8rpx;
|
|
}
|
|
|
|
.export-btn:active {
|
|
background: rgba(255, 255, 255, 0.3);
|
|
}
|
|
|
|
.export-btn:disabled {
|
|
opacity: 0.5;
|
|
}
|
|
|
|
/* Filter Bar */
|
|
.filter-bar {
|
|
padding: 20rpx 30rpx;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.filter-scroll {
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.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 {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
padding: 12rpx 20rpx;
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border-radius: 25rpx;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.filter-chip .chip-text {
|
|
margin-right: 8rpx;
|
|
}
|
|
|
|
.filter-chip.active {
|
|
background: rgba(255, 255, 255, 0.9);
|
|
}
|
|
|
|
.chip-text {
|
|
font-size: 26rpx;
|
|
color: #FFFFFF;
|
|
}
|
|
|
|
.filter-chip.active .chip-text {
|
|
color: #6366F1;
|
|
}
|
|
|
|
.chip-count {
|
|
font-size: 22rpx;
|
|
color: #FFFFFF;
|
|
background: rgba(255, 255, 255, 0.3);
|
|
padding: 4rpx 8rpx;
|
|
border-radius: 10rpx;
|
|
min-width: 30rpx;
|
|
text-align: center;
|
|
}
|
|
|
|
.filter-chip.active .chip-count {
|
|
color: #6366F1;
|
|
background: rgba(99, 102, 241, 0.2);
|
|
}
|
|
|
|
.filter-controls {}
|
|
|
|
.filter-controls>* {
|
|
margin-bottom: 15rpx;
|
|
}
|
|
|
|
.filter-controls>*:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.search-box {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
padding: 15rpx 20rpx;
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border-radius: 15rpx;
|
|
}
|
|
|
|
.search-box simple-icon {
|
|
margin-right: 15rpx;
|
|
}
|
|
|
|
.search-input {
|
|
flex: 1;
|
|
font-size: 28rpx;
|
|
color: #FFFFFF;
|
|
background: transparent;
|
|
border: none;
|
|
}
|
|
|
|
.search-input::placeholder {
|
|
color: rgba(255, 255, 255, 0.7);
|
|
}
|
|
|
|
.date-filters {
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
/* 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: 46%;
|
|
flex: 0 0 46%;
|
|
margin-right: 20rpx;
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.stats-grid .stat-card:nth-child(2n) {
|
|
margin-right: 20rpx;
|
|
}
|
|
|
|
.stats-grid .stat-card:nth-child(4n) {
|
|
margin-right: 0;
|
|
}
|
|
|
|
.stat-card {
|
|
background: #FFFFFF;
|
|
padding: 30rpx;
|
|
border-radius: 20rpx;
|
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
|
align-items: center;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 42rpx;
|
|
font-weight: bold;
|
|
color: #6366F1;
|
|
margin-bottom: 10rpx;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 26rpx;
|
|
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;
|
|
}
|
|
|
|
.sort-controls {
|
|
flex-direction: row;
|
|
}
|
|
|
|
.sort-controls .sort-btn {
|
|
margin-right: 15rpx;
|
|
}
|
|
|
|
.sort-controls .sort-btn:last-child {
|
|
margin-right: 0;
|
|
}
|
|
|
|
.sort-btn {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
padding: 12rpx 20rpx;
|
|
background: #F1F5F9;
|
|
border: none;
|
|
border-radius: 15rpx;
|
|
font-size: 26rpx;
|
|
}
|
|
|
|
.sort-btn simple-icon {
|
|
margin-left: 8rpx;
|
|
}
|
|
|
|
.sort-text {
|
|
color: #6B7280;
|
|
}
|
|
|
|
/* Records List */
|
|
.records-list {}
|
|
|
|
.records-list .record-card {
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.records-list .record-card:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.record-card {
|
|
background: #FFFFFF;
|
|
padding: 30rpx;
|
|
border-radius: 20rpx;
|
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.record-card:active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
.record-header {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.student-info {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
flex: 1;
|
|
}
|
|
|
|
.student-info .student-avatar {
|
|
margin-right: 15rpx;
|
|
}
|
|
|
|
.student-avatar {
|
|
width: 80rpx;
|
|
height: 80rpx;
|
|
background-image: linear-gradient(to bottom right, #6366F1, #8B5CF6);
|
|
border-radius: 40rpx;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.avatar-text {
|
|
font-size: 28rpx;
|
|
font-weight: bold;
|
|
color: #FFFFFF;
|
|
}
|
|
|
|
.student-details {
|
|
flex: 1;
|
|
}
|
|
|
|
.student-name {
|
|
font-size: 30rpx;
|
|
font-weight: 400;
|
|
color: #1E293B;
|
|
margin-bottom: 8rpx;
|
|
}
|
|
|
|
.record-time {
|
|
font-size: 24rpx;
|
|
color: #64748B;
|
|
}
|
|
|
|
.record-status {
|
|
align-items: flex-end;
|
|
}
|
|
|
|
.status-badge {
|
|
padding: 8rpx 16rpx;
|
|
border-radius: 20rpx;
|
|
font-size: 22rpx;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.status-completed {
|
|
background: #D1FAE5;
|
|
color: #065F46;
|
|
}
|
|
|
|
.status-progress {
|
|
background: #DBEAFE;
|
|
color: #1E40AF;
|
|
}
|
|
|
|
.status-pending {
|
|
background: #FEF3C7;
|
|
color: #92400E;
|
|
}
|
|
|
|
.status-graded {
|
|
background: #EDE9FE;
|
|
color: #6B21A8;
|
|
}
|
|
|
|
.status-default {
|
|
background: #F3F4F6;
|
|
color: #6B7280;
|
|
}
|
|
|
|
.status-text {
|
|
font-size: 22rpx;
|
|
}
|
|
|
|
.record-content {
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.project-info {
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.project-name {
|
|
font-size: 28rpx;
|
|
font-weight: 400;
|
|
color: #1E293B;
|
|
margin-bottom: 8rpx;
|
|
}
|
|
|
|
.project-category {
|
|
font-size: 24rpx;
|
|
color: #64748B;
|
|
}
|
|
|
|
.record-metrics {
|
|
flex-direction: row;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.record-metrics .metric-item {
|
|
margin-right: 30rpx;
|
|
}
|
|
|
|
.metric-item {
|
|
align-items: center;
|
|
}
|
|
|
|
.metric-label {
|
|
font-size: 22rpx;
|
|
color: #6B7280;
|
|
margin-bottom: 8rpx;
|
|
}
|
|
|
|
.metric-value {
|
|
font-size: 26rpx;
|
|
font-weight: 400;
|
|
color: #1E293B;
|
|
}
|
|
|
|
.metric-value.score {
|
|
font-weight: bold;
|
|
}
|
|
|
|
.score-excellent {
|
|
color: #10B981;
|
|
}
|
|
|
|
.score-good {
|
|
color: #3B82F6;
|
|
}
|
|
|
|
.score-average {
|
|
color: #F59E0B;
|
|
}
|
|
|
|
.score-pass {
|
|
color: #8B5CF6;
|
|
}
|
|
|
|
.score-fail {
|
|
color: #EF4444;
|
|
}
|
|
|
|
.record-notes {
|
|
margin-bottom: 20rpx;
|
|
padding: 20rpx;
|
|
background: #F8FAFC;
|
|
border-radius: 15rpx;
|
|
}
|
|
|
|
.notes-text {
|
|
font-size: 26rpx;
|
|
color: #374151;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.record-actions {
|
|
flex-direction: row;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.record-actions .action-btn {
|
|
margin-left: 15rpx;
|
|
}
|
|
|
|
.record-actions .action-btn:first-child {
|
|
margin-left: 0;
|
|
}
|
|
|
|
.action-btn {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
padding: 12rpx 20rpx;
|
|
border: none;
|
|
border-radius: 15rpx;
|
|
font-size: 24rpx;
|
|
}
|
|
|
|
.action-btn simple-icon {
|
|
margin-right: 8rpx;
|
|
}
|
|
|
|
.review-btn {
|
|
background: #EDE9FE;
|
|
color: #6366F1;
|
|
}
|
|
|
|
.grade-btn {
|
|
background: #D1FAE5;
|
|
color: #10B981;
|
|
}
|
|
|
|
.action-btn:active {
|
|
opacity: 0.7;
|
|
}
|
|
|
|
/* 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 & 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;
|
|
}
|
|
</style> |