1112 lines
26 KiB
Plaintext
1112 lines
26 KiB
Plaintext
<template>
|
|
<scroll-view direction="vertical" class="training-record-container">
|
|
<!-- Assignment Data -->
|
|
<supadb v-if="assignmentId" collection="ak_assignments" :filter="{ id: assignmentId }"
|
|
@process-data="handleAssignmentData" #default="{ data, loading: assignmentLoading }">
|
|
<view style="display: none;"></view>
|
|
</supadb>
|
|
|
|
<!-- Training Project Data -->
|
|
<supadb v-if="projectId" collection="ak_training_projects" :filter="{ id: projectId }"
|
|
@process-data="handleProjectData" #default="{ data, loading: projectLoading }">
|
|
<view style="display: none;"></view>
|
|
</supadb>
|
|
<!-- Content Wrapper -->
|
|
<view class="content-wrapper" :class="{ 'large-screen': isLargeScreen }">
|
|
|
|
<!-- Header -->
|
|
<view class="header">
|
|
<view class="header-info">
|
|
<text class="assignment-title">{{ getAssignmentTitle() }}</text>
|
|
<text class="project-name">{{ projectDisplayName }}</text>
|
|
</view>
|
|
<view v-if="!isCompleted" class="header-status">
|
|
<view class="status-indicator" :class="getStatusClass()">
|
|
<text class="status-text">{{ getStatusText() }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Training Timer -->
|
|
<view class="timer-section" v-if="!isCompleted">
|
|
<view class="timer-card">
|
|
<view class="timer-display">
|
|
<text class="timer-text">{{ formattedTime }}</text>
|
|
</view>
|
|
<view class="timer-controls">
|
|
<button class="timer-btn start" v-if="!isRecording" @click="startRecording">
|
|
<simple-icon type="play-filled" :size="16" color="#FFFFFF" />
|
|
开始记录
|
|
</button>
|
|
<button class="timer-btn pause" v-else-if="isRecording && !isPaused" @click="pauseRecording">
|
|
<simple-icon type="pause" :size="16" color="#FFFFFF" />
|
|
暂停
|
|
</button>
|
|
<button class="timer-btn resume" v-else-if="isRecording && isPaused" @click="resumeRecording">
|
|
<simple-icon type="play-filled" :size="16" color="#FFFFFF" />
|
|
继续
|
|
</button>
|
|
<button class="timer-btn stop" v-if="isRecording" @click="stopRecording">
|
|
<simple-icon type="stop" :size="16" color="#FFFFFF" />
|
|
停止
|
|
</button>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Training Form -->
|
|
<view class="record-form-section">
|
|
<view class="form-section">
|
|
<view class="section-title">训练记录</view> <!-- Basic Information -->
|
|
<view class="form-item">
|
|
<view class="form-label">
|
|
<text class="label-text">训练日期</text>
|
|
</view>
|
|
<picker-date :value="formData.trainingDate" placeholder="选择训练日期" class="date-picker" />
|
|
</view>
|
|
|
|
<view class="form-item">
|
|
<view class="form-label">
|
|
<text class="label-text">训练时长</text>
|
|
<text class="required-mark">*</text>
|
|
</view>
|
|
<view class="duration-input">
|
|
<input :value="formData.durationMinutes" type="number" placeholder="30" class="number-input"
|
|
@input="onDurationInput" />
|
|
<text class="unit-text">分钟</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Performance Metrics -->
|
|
<view class="form-item">
|
|
<view class="form-label">
|
|
<text class="label-text">完成度</text>
|
|
<text class="required-mark">*</text>
|
|
</view>
|
|
<view class="completion-slider">
|
|
<slider :value="formData.completionRate" min="0" max="100" step="5" class="slider-input"
|
|
@change="onCompletionChange" />
|
|
<text class="completion-text">{{ formData.completionRate }}%</text>
|
|
</view>
|
|
</view> <!-- Training Notes -->
|
|
<view class="form-item">
|
|
<view class="form-label">
|
|
<text class="label-text">训练感受</text>
|
|
</view>
|
|
<textarea :value="formData.notes" placeholder="记录训练过程中的感受、困难或收获..." maxlength="500"
|
|
class="textarea-input" @input="onNotesInput" />
|
|
<view class="word-count">{{ formData.notes.length }}/500</view>
|
|
</view>
|
|
|
|
<!-- Difficulty Rating -->
|
|
<view class="form-item">
|
|
<view class="form-label">
|
|
<text class="label-text">难度评价</text>
|
|
</view>
|
|
<view class="rating-section">
|
|
<view v-for="(star, index) in 5" :key="index" class="star-item"
|
|
@click="setDifficultyRating(index + 1)">
|
|
<simple-icon :type="index < formData.difficultyRating ? 'star-filled' : 'star'"
|
|
:size="32" :color="index < formData.difficultyRating ? '#F59E0B' : '#D1D5DB'" />
|
|
</view>
|
|
<text class="rating-text">{{ getDifficultyText() }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Effort Rating -->
|
|
<view class="form-item">
|
|
<view class="form-label">
|
|
<text class="label-text">努力程度</text>
|
|
</view>
|
|
<view class="rating-section">
|
|
<view v-for="(star, index) in 5" :key="index" class="star-item"
|
|
@click="setEffortRating(index + 1)">
|
|
<simple-icon :type="index < formData.effortRating ? 'star-filled' : 'star'" :size="32"
|
|
:color="index < formData.effortRating ? '#10B981' : '#D1D5DB'" />
|
|
</view>
|
|
<text class="rating-text">{{ getEffortText() }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Performance Data -->
|
|
<view class="form-section" v-if="showPerformanceData">
|
|
<view class="section-title">成绩数据</view>
|
|
|
|
<!-- Dynamic Performance Fields -->
|
|
<view v-for="(field, index) in performanceFields" :key="index" class="form-item">
|
|
<view class="form-label">
|
|
<text class="label-text">{{ field.getString('label') ?? '' }}</text>
|
|
<text v-if="field.getBoolean('required') ?? false" class="required-mark">*</text>
|
|
</view>
|
|
<view class="performance-input">
|
|
<input :value="getPerformanceValue(field.getString('key') ?? '')"
|
|
:type="getInputType(field)" :placeholder="field.getString('placeholder') ?? ''"
|
|
class="number-input"
|
|
@input="onPerformanceInput(field.getString('key') ?? '', $event as UniInputEvent)" />
|
|
<text class="unit-text">{{ field.getString('unit') ?? '' }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Action Buttons -->
|
|
<view class="action-section">
|
|
<button v-if="!isCompleted" @click="saveDraft" class="action-btn draft-btn" :disabled="saving">
|
|
<text>保存草稿</text>
|
|
</button>
|
|
<button @click="submitRecord" class="action-btn submit-btn" :disabled="saving || !isFormValid">
|
|
<text>{{ isCompleted ? '更新记录' : '提交记录' }}</text>
|
|
</button>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- Completion Modal -->
|
|
<modal :show="showCompletionModal" @close="showCompletionModal = false">
|
|
<view class="completion-modal">
|
|
<view class="modal-content">
|
|
<view class="success-icon">
|
|
<simple-icon type="checkmark-circle" :size="64" color="#10B981" />
|
|
</view>
|
|
<text class="success-title">训练记录已提交!</text>
|
|
<text class="success-message">
|
|
您的训练记录已成功提交,教师将对您的表现进行评价。
|
|
</text>
|
|
<view class="modal-actions">
|
|
<button @click="goToRecords" class="modal-btn primary">
|
|
<text>查看记录</text>
|
|
</button>
|
|
<button @click="goToDashboard" class="modal-btn secondary">
|
|
<text>返回首页</text>
|
|
</button>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</modal>
|
|
</scroll-view>
|
|
</template>
|
|
|
|
<script setup lang="uts">
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
import {formatTime} from '../types.uts'
|
|
import { onLoad, onResize } from '@dcloudio/uni-app'
|
|
import { state, getCurrentUserId } from '@/utils/store.uts'
|
|
import supaClient from '@/components/supadb/aksupainstance.uts'
|
|
type TrainingFormData = {
|
|
assignmentId : string
|
|
projectId : string
|
|
trainingDate : string
|
|
durationMinutes : string
|
|
completionRate : number
|
|
notes : string
|
|
difficultyRating : number
|
|
effortRating : number
|
|
performanceData : UTSJSONObject
|
|
}
|
|
|
|
// Props from route
|
|
const props = defineProps<{
|
|
assignmentId ?: string
|
|
projectId ?: string
|
|
recordId ?: string
|
|
}>()
|
|
|
|
// Reactive data - User ID handling
|
|
const userId = ref('')
|
|
const assignmentId = ref<string>('')
|
|
const projectId = ref<string>('')
|
|
|
|
// Data
|
|
const currentAssignment = ref<UTSJSONObject | null>(null)
|
|
const currentProject = ref<UTSJSONObject | null>(null)
|
|
// Timer state
|
|
const isRecording = ref<boolean>(false)
|
|
const isPaused = ref<boolean>(false)
|
|
const currentTime = ref<number>(0)
|
|
const startTime = ref<number>(0)
|
|
const pausedTime = ref<number>(0)
|
|
const timerInterval = ref<number | null>(null)
|
|
|
|
// Form state
|
|
const isCompleted = ref<boolean>(false)
|
|
const saving = ref<boolean>(false)
|
|
const showCompletionModal = ref<boolean>(false)
|
|
|
|
// Form data
|
|
const formData = ref<TrainingFormData>({
|
|
assignmentId: '',
|
|
projectId: '',
|
|
trainingDate: '',
|
|
durationMinutes: '',
|
|
completionRate: 0,
|
|
notes: '',
|
|
difficultyRating: 0,
|
|
effortRating: 0,
|
|
performanceData: {} as UTSJSONObject
|
|
})
|
|
// Performance fields configuration
|
|
const performanceFields = ref<UTSJSONObject[]>([])
|
|
const showPerformanceData = ref<boolean>(false)
|
|
|
|
// Responsive state - using onResize for dynamic updates
|
|
const screenWidth = ref<number>(uni.getSystemInfoSync().windowWidth)
|
|
|
|
// Computed
|
|
const isFormValid = computed(() : boolean => {
|
|
return formData.value.durationMinutes.length > 0 &&
|
|
formData.value.completionRate > 0
|
|
})
|
|
|
|
// Computed properties for template functions to avoid UTS Android compilation issues
|
|
const isLargeScreen = computed(() : boolean => {
|
|
return screenWidth.value >= 768
|
|
})
|
|
|
|
const projectDisplayName = computed(() : string => {
|
|
const project = currentProject.value
|
|
if (project != null) {
|
|
return project.getString('name')??''
|
|
}
|
|
return ''
|
|
})
|
|
|
|
const formattedTime = computed(() : string => {
|
|
// Convert milliseconds to seconds for formatTime function
|
|
const timeInSeconds = Math.floor(currentTime.value / 1000)
|
|
return formatTime(timeInSeconds)
|
|
})
|
|
|
|
// Get current student ID using unified function
|
|
const getCurrentStudentId = () : string => {
|
|
// Use unified user ID handling
|
|
if (userId.value.length > 0) {
|
|
return userId.value
|
|
}
|
|
return getCurrentUserId()
|
|
}
|
|
|
|
const updateTimer = () => {
|
|
timerInterval.value = setInterval(() => {
|
|
currentTime.value = Date.now() - startTime.value
|
|
}, 100)
|
|
}
|
|
const clearTimer = () => {
|
|
if (timerInterval.value != null) {
|
|
clearInterval(timerInterval.value!)
|
|
timerInterval.value = null
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const loadExistingRecord = (recordId : string) => {
|
|
// Load existing training record for editing
|
|
// This would typically fetch from API
|
|
console.log('Loading record:', recordId)
|
|
isCompleted.value = true
|
|
}
|
|
|
|
// Timer methods
|
|
const startRecording = () => {
|
|
isRecording.value = true
|
|
isPaused.value = false
|
|
startTime.value = Date.now() - pausedTime.value
|
|
updateTimer()
|
|
}
|
|
|
|
const pauseRecording = () => {
|
|
isPaused.value = true
|
|
clearTimer()
|
|
}
|
|
|
|
const resumeRecording = () => {
|
|
isPaused.value = false
|
|
startTime.value = Date.now() - currentTime.value
|
|
updateTimer()
|
|
}
|
|
|
|
const stopRecording = () => {
|
|
isRecording.value = false
|
|
isPaused.value = false
|
|
clearTimer()
|
|
|
|
// Auto-fill duration from timer
|
|
const minutes = Math.floor(currentTime.value / 60000)
|
|
formData.value.durationMinutes = minutes.toString()
|
|
}
|
|
|
|
// Load assignment data
|
|
|
|
|
|
// Data handlers (for compatibility with template)
|
|
const handleAssignmentData = (data : UTSJSONObject[]) => {
|
|
if (data.length > 0) {
|
|
const assignmentData = data[0]
|
|
currentAssignment.value = assignmentData
|
|
// Extract project ID if not provided
|
|
if (projectId.value.length == 0) {
|
|
projectId.value = assignmentData.getString('project_id') ?? ''
|
|
formData.value.projectId = projectId.value
|
|
}
|
|
}
|
|
}
|
|
const setupPerformanceFields = () => {
|
|
if (currentProject.value == null) return
|
|
|
|
// Configure performance fields based on project type
|
|
const projectData = currentProject.value
|
|
const projectType = projectData?.getString('category') ?? ''
|
|
|
|
switch (projectType) {
|
|
case 'running':
|
|
performanceFields.value = [
|
|
{ key: 'distance', label: '跑步距离', unit: '公里', type: 'number', required: true, placeholder: '5.0' },
|
|
{ key: 'pace', label: '配速', unit: '分钟/公里', type: 'text', required: false, placeholder: '5:30' },
|
|
{ key: 'heartRate', label: '平均心率', unit: 'bpm', type: 'number', required: false, placeholder: '150' }
|
|
]
|
|
showPerformanceData.value = true
|
|
break
|
|
case 'strength':
|
|
performanceFields.value = [
|
|
{ key: 'weight', label: '训练重量', unit: '公斤', type: 'number', required: true, placeholder: '50' },
|
|
{ key: 'reps', label: '重复次数', unit: '次', type: 'number', required: true, placeholder: '12' },
|
|
{ key: 'sets', label: '组数', unit: '组', type: 'number', required: true, placeholder: '3' }
|
|
]
|
|
showPerformanceData.value = true
|
|
break
|
|
case 'flexibility':
|
|
performanceFields.value = [
|
|
{ key: 'stretchTime', label: '拉伸时长', unit: '分钟', type: 'number', required: true, placeholder: '20' },
|
|
{ key: 'flexibility', label: '柔韧度评分', unit: '分', type: 'number', required: false, placeholder: '8' }
|
|
]
|
|
showPerformanceData.value = true
|
|
break
|
|
default:
|
|
showPerformanceData.value = false
|
|
} // Initialize performance data
|
|
performanceFields.value.forEach(field => {
|
|
const key = field.getString('key') ?? ''
|
|
if (key.length > 0) {
|
|
(formData.value.performanceData as UTSJSONObject)[key] = ''
|
|
}
|
|
})
|
|
}
|
|
|
|
const handleProjectData = (data : UTSJSONObject[]) => {
|
|
if (data.length > 0) {
|
|
currentProject.value = data[0]
|
|
setupPerformanceFields()
|
|
}
|
|
}
|
|
|
|
// Form methods
|
|
const onCompletionChange = (value : number) => {
|
|
formData.value.completionRate = value
|
|
}
|
|
|
|
const setDifficultyRating = (rating : number) => {
|
|
formData.value.difficultyRating = rating
|
|
}
|
|
|
|
const setEffortRating = (rating : number) => {
|
|
formData.value.effortRating = rating
|
|
}
|
|
const getDifficultyText = () : string => {
|
|
const ratings = ['', '很简单', '简单', '适中', '困难', '很困难']
|
|
const rating = ratings[formData.value.difficultyRating]
|
|
return rating != null ? rating : ''
|
|
}
|
|
const getEffortText = () : string => {
|
|
const ratings = ['', '很轻松', '轻松', '适中', '努力', '非常努力']
|
|
const rating = ratings[formData.value.effortRating]
|
|
return rating != null ? rating : ''
|
|
}
|
|
const getInputType = (field : UTSJSONObject) : string => {
|
|
const type = field.getString('type') ?? 'text'
|
|
return type === 'number' ? 'number' : 'text'
|
|
}
|
|
const getPerformanceValue = (key : string) : string => {
|
|
const perfData = formData.value.performanceData as UTSJSONObject
|
|
return perfData?.getString(key) ?? ''
|
|
}
|
|
|
|
// Input event handlers
|
|
const onDurationInput = (event : UniInputEvent) => {
|
|
formData.value.durationMinutes = event.detail.value
|
|
}
|
|
|
|
const onNotesInput = (event : UniInputEvent) => {
|
|
formData.value.notes = event.detail.value
|
|
}
|
|
|
|
const onPerformanceInput = (key : string, event : UniInputEvent) => {
|
|
const perfData = formData.value.performanceData as UTSJSONObject
|
|
perfData[key] = event.detail.value
|
|
}
|
|
|
|
const saveDraft = async () => {
|
|
saving.value = true
|
|
|
|
try {
|
|
const draftData = {
|
|
...formData.value,
|
|
student_id: getCurrentStudentId(),
|
|
status: 'draft',
|
|
updated_at: new Date().toISOString()
|
|
}
|
|
|
|
const result = await supaClient
|
|
.from('ak_training_records')
|
|
.insert(draftData)
|
|
.execute()
|
|
|
|
if (result.error == null) {
|
|
uni.showToast({
|
|
title: '草稿已保存',
|
|
icon: 'success'
|
|
})
|
|
} else {
|
|
throw new Error('保存失败')
|
|
}
|
|
} catch (error) {
|
|
console.error('保存草稿失败:', error)
|
|
uni.showToast({
|
|
title: '保存失败',
|
|
icon: 'none'
|
|
})
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
// Load project data
|
|
const loadProjectData = async () => {
|
|
try {
|
|
const result = await supaClient
|
|
.from('ak_training_projects')
|
|
.select('*', {})
|
|
.eq('id', projectId.value)
|
|
.single()
|
|
.execute()
|
|
if (result.error == null && result.data != null) {
|
|
const projectData = result.data as UTSJSONObject
|
|
currentProject.value = projectData
|
|
setupPerformanceFields()
|
|
}
|
|
} catch (error) {
|
|
console.error('加载项目失败:', error)
|
|
}
|
|
}
|
|
const loadAssignmentData = async () => {
|
|
try {
|
|
const result = await supaClient
|
|
.from('ak_assignments')
|
|
.select('*, ak_training_projects(*)', {})
|
|
.eq('id', assignmentId.value)
|
|
.execute()
|
|
|
|
if (result.error == null && result.data != null) {
|
|
const assignmentData = result.data as UTSJSONObject
|
|
currentAssignment.value = assignmentData
|
|
// Extract project ID if not provided
|
|
if (projectId.value.length == 0) {
|
|
projectId.value = assignmentData.getString('project_id') ?? ''
|
|
formData.value.projectId = projectId.value
|
|
if (projectId.value.length > 0) {
|
|
loadProjectData()
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('加载作业失败:', error)
|
|
}
|
|
}
|
|
const submitRecord = async () => {
|
|
if (!isFormValid.value) {
|
|
uni.showToast({
|
|
title: '请完善必填信息',
|
|
icon: 'error'
|
|
})
|
|
return
|
|
}
|
|
|
|
saving.value = true
|
|
|
|
try {
|
|
const recordData = {
|
|
...formData.value,
|
|
student_id: getCurrentStudentId(),
|
|
status: 'submitted',
|
|
submitted_at: new Date().toISOString()
|
|
}
|
|
|
|
const result = await supaClient
|
|
.from('ak_training_records')
|
|
.insert(recordData)
|
|
.execute()
|
|
|
|
if (result.error == null) {
|
|
isCompleted.value = true
|
|
showCompletionModal.value = true
|
|
} else {
|
|
throw new Error('提交失败')
|
|
}
|
|
} catch (error) {
|
|
console.error('提交记录失败:', error)
|
|
uni.showToast({
|
|
title: '提交失败',
|
|
icon: 'none'
|
|
})
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
const goToRecords = () => {
|
|
uni.navigateTo({
|
|
url: '/pages/sport/student/records'
|
|
})
|
|
}
|
|
|
|
const goToDashboard = () => {
|
|
uni.switchTab({
|
|
url: '/pages/sport/student/dashboard'
|
|
})
|
|
|
|
}
|
|
// Helper methods
|
|
const getAssignmentTitle = () : string => {
|
|
if (currentAssignment.value != null) {
|
|
return currentAssignment.value?.getString('title') ?? '训练记录'
|
|
}
|
|
return '训练记录'
|
|
}
|
|
|
|
const getStatusClass = () : string => {
|
|
if (isRecording.value && !isPaused.value) return 'status-recording'
|
|
if (isPaused.value) return 'status-paused'
|
|
return 'status-ready'
|
|
}
|
|
|
|
const getStatusText = () : string => {
|
|
if (isRecording.value && !isPaused.value) return '记录中'
|
|
if (isPaused.value) return '已暂停'
|
|
return '准备开始'
|
|
}
|
|
// Lifecycle
|
|
onLoad((options : OnLoadOptions) => {
|
|
userId.value = options['id'] ?? getCurrentUserId()
|
|
|
|
// Get page parameters from options
|
|
assignmentId.value = options['assignmentId'] ?? props.assignmentId ?? ''
|
|
projectId.value = options['projectId'] ?? props.projectId ?? ''
|
|
})
|
|
onMounted(() => {
|
|
// Get route params from props (passed via navigation) or use defaults
|
|
if (assignmentId.value.length == 0) {
|
|
assignmentId.value = props.assignmentId ?? ''
|
|
}
|
|
if (projectId.value.length == 0) {
|
|
projectId.value = props.projectId ?? ''
|
|
}
|
|
|
|
formData.value.assignmentId = assignmentId.value
|
|
formData.value.projectId = projectId.value
|
|
|
|
// Set default training date to today
|
|
const today = new Date()
|
|
formData.value.trainingDate = today.toISOString().split('T')[0]
|
|
|
|
// Check if this is editing an existing record
|
|
const recordId = props.recordId ?? ''
|
|
if (recordId.length > 0) {
|
|
loadExistingRecord(recordId)
|
|
}
|
|
|
|
// Load data
|
|
if (assignmentId.value.length > 0) {
|
|
loadAssignmentData()
|
|
}
|
|
if (projectId.value.length > 0) {
|
|
loadProjectData()
|
|
}
|
|
|
|
// Initialize screen width
|
|
screenWidth.value = uni.getSystemInfoSync().windowWidth
|
|
})
|
|
onUnmounted(() => {
|
|
clearTimer()
|
|
})
|
|
|
|
// Handle resize events for responsive design
|
|
onResize((size) => {
|
|
screenWidth.value = size.size.windowWidth
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.training-record-container {
|
|
flex: 1;
|
|
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
|
|
|
|
}
|
|
|
|
.content-wrapper {
|
|
flex: 1;
|
|
padding: 30rpx;
|
|
background: #F8FAFC;
|
|
border-radius: 30rpx 30rpx 0 0;
|
|
margin-top: 20rpx;
|
|
}
|
|
|
|
.content-wrapper.large-screen {
|
|
padding: 40rpx;
|
|
max-width: 800rpx;
|
|
margin: 20rpx auto 0;
|
|
border-radius: 30rpx;
|
|
}
|
|
|
|
/* Header */
|
|
.header {
|
|
background-image: linear-gradient(to bottom right, #6366F1, #8B5CF6);
|
|
padding: 30rpx;
|
|
border-radius: 20rpx;
|
|
margin-bottom: 30rpx;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.header-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.assignment-title {
|
|
font-size: 36rpx;
|
|
font-weight: bold;
|
|
color: #FFFFFF;
|
|
margin-bottom: 10rpx;
|
|
}
|
|
|
|
.project-name {
|
|
font-size: 26rpx;
|
|
color: rgba(255, 255, 255, 0.8);
|
|
}
|
|
|
|
.header-status {
|
|
align-items: flex-end;
|
|
}
|
|
|
|
.status-indicator {
|
|
padding: 8rpx 16rpx;
|
|
border-radius: 20rpx;
|
|
font-size: 22rpx;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.status-ready {
|
|
background: rgba(255, 255, 255, 0.2);
|
|
color: #FFFFFF;
|
|
}
|
|
|
|
.status-recording {
|
|
background: #EF4444;
|
|
color: #FFFFFF;
|
|
}
|
|
|
|
.status-paused {
|
|
background: #F59E0B;
|
|
color: #FFFFFF;
|
|
}
|
|
|
|
.status-text {
|
|
font-size: 22rpx;
|
|
}
|
|
|
|
/* Timer Section */
|
|
.timer-section {
|
|
margin-bottom: 30rpx;
|
|
}
|
|
|
|
.timer-card {
|
|
background: #FFFFFF;
|
|
padding: 40rpx;
|
|
border-radius: 20rpx;
|
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
|
align-items: center;
|
|
}
|
|
|
|
.timer-display {
|
|
margin-bottom: 30rpx;
|
|
}
|
|
|
|
.timer-text {
|
|
font-size: 72rpx;
|
|
font-weight: bold;
|
|
color: #1E293B;
|
|
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
|
|
}
|
|
.timer-controls {
|
|
flex-direction: row;
|
|
justify-content: center;
|
|
}
|
|
|
|
.timer-btn {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
padding: 20rpx 30rpx;
|
|
border: none;
|
|
border-radius: 15rpx;
|
|
font-size: 28rpx;
|
|
font-weight: 400;
|
|
margin: 0 10rpx;
|
|
}
|
|
|
|
.timer-btn:first-child {
|
|
margin-left: 0;
|
|
}
|
|
|
|
.timer-btn:last-child {
|
|
margin-right: 0;
|
|
}
|
|
|
|
/* Icon spacing inside timer buttons */
|
|
.timer-btn simple-icon {
|
|
margin-right: 8rpx;
|
|
}
|
|
|
|
.timer-btn.start {
|
|
background: #10B981;
|
|
color: #FFFFFF;
|
|
}
|
|
|
|
.timer-btn.pause {
|
|
background: #F59E0B;
|
|
color: #FFFFFF;
|
|
}
|
|
|
|
.timer-btn.resume {
|
|
background: #3B82F6;
|
|
color: #FFFFFF;
|
|
}
|
|
|
|
.timer-btn.stop {
|
|
background: #EF4444;
|
|
color: #FFFFFF;
|
|
}
|
|
|
|
.timer-btn:active {
|
|
opacity: 0.8;
|
|
}
|
|
/* Form Sections */
|
|
.record-form-section {
|
|
/* 使用 margin 替代 gap */
|
|
}
|
|
|
|
.record-form-section .form-section {
|
|
margin-bottom: 30rpx;
|
|
}
|
|
|
|
.record-form-section .form-section:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.form-section {
|
|
background: #FFFFFF;
|
|
padding: 30rpx;
|
|
border-radius: 20rpx;
|
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 36rpx;
|
|
font-weight: bold;
|
|
color: #1E293B;
|
|
margin-bottom: 30rpx;
|
|
padding-bottom: 15rpx;
|
|
border-bottom: 2rpx solid #E2E8F0;
|
|
}
|
|
|
|
.form-item {
|
|
margin-bottom: 30rpx;
|
|
}
|
|
|
|
.form-item:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.form-label {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
margin-bottom: 15rpx;
|
|
}
|
|
|
|
.label-text {
|
|
font-size: 28rpx;
|
|
color: #374151;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.required-mark {
|
|
color: #EF4444;
|
|
font-size: 28rpx;
|
|
margin-left: 5rpx;
|
|
}
|
|
|
|
.date-picker {
|
|
width: 100%;
|
|
height: 80rpx;
|
|
padding: 0 20rpx;
|
|
border: 2rpx solid #E5E7EB;
|
|
border-radius: 15rpx;
|
|
background: #FFFFFF;
|
|
font-size: 28rpx;
|
|
color: #374151;
|
|
}
|
|
.duration-input,
|
|
.performance-input {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
}
|
|
|
|
.duration-input .number-input,
|
|
.performance-input .number-input {
|
|
margin-right: 15rpx;
|
|
}
|
|
|
|
.number-input {
|
|
flex: 1;
|
|
padding: 20rpx;
|
|
border: 2rpx solid #E5E7EB;
|
|
border-radius: 15rpx;
|
|
font-size: 28rpx;
|
|
color: #374151;
|
|
background: #FFFFFF;
|
|
text-align: center;
|
|
}
|
|
|
|
.unit-text {
|
|
font-size: 28rpx;
|
|
color: #6B7280;
|
|
}
|
|
.completion-slider {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
}
|
|
|
|
.completion-slider .slider-input {
|
|
margin-right: 20rpx;
|
|
}
|
|
|
|
.slider-input {
|
|
flex: 1;
|
|
height: 40rpx;
|
|
}
|
|
|
|
.completion-text {
|
|
font-size: 28rpx;
|
|
color: #1E293B;
|
|
font-weight: 400;
|
|
min-width: 80rpx;
|
|
text-align: center;
|
|
}
|
|
|
|
.textarea-input {
|
|
width: 100%;
|
|
height: 200rpx;
|
|
padding: 20rpx;
|
|
border: 2rpx solid #E5E7EB;
|
|
border-radius: 15rpx;
|
|
font-size: 28rpx;
|
|
color: #374151;
|
|
background: #FFFFFF;
|
|
text-align: top;
|
|
}
|
|
|
|
.word-count {
|
|
text-align: right;
|
|
font-size: 24rpx;
|
|
color: #9CA3AF;
|
|
margin-top: 10rpx;
|
|
}
|
|
.rating-section {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
}
|
|
|
|
.star-item {
|
|
padding: 10rpx;
|
|
margin-right: 15rpx;
|
|
}
|
|
|
|
.star-item:last-child {
|
|
margin-right: 0;
|
|
}
|
|
|
|
.star-item:active {
|
|
transform: scale(0.9);
|
|
}
|
|
|
|
.rating-text {
|
|
font-size: 28rpx;
|
|
color: #6B7280;
|
|
margin-left: 15rpx;
|
|
}
|
|
/* Action Section */
|
|
.action-section {
|
|
flex-direction: row;
|
|
padding: 30rpx;
|
|
}
|
|
|
|
.action-btn {
|
|
flex: 1;
|
|
height: 90rpx;
|
|
border-radius: 20rpx;
|
|
font-size: 32rpx;
|
|
font-weight: 400;
|
|
border: none;
|
|
margin-right: 20rpx;
|
|
}
|
|
|
|
.action-btn:last-child {
|
|
margin-right: 0;
|
|
}
|
|
|
|
.draft-btn {
|
|
background: #F3F4F6;
|
|
color: #6B7280;
|
|
}
|
|
|
|
.draft-btn:active {
|
|
background: #E5E7EB;
|
|
}
|
|
|
|
.submit-btn {
|
|
background-image: linear-gradient(to bottom right, #6366F1, #8B5CF6);
|
|
color: #FFFFFF;
|
|
}
|
|
|
|
.submit-btn:active {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.action-btn:disabled {
|
|
opacity: 0.5;
|
|
}
|
|
|
|
/* Completion Modal */
|
|
.completion-modal {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 999;
|
|
}
|
|
|
|
.modal-content {
|
|
background: #FFFFFF;
|
|
padding: 60rpx;
|
|
border-radius: 30rpx;
|
|
margin: 60rpx;
|
|
align-items: center;
|
|
max-width: 600rpx;
|
|
}
|
|
|
|
.success-icon {
|
|
margin-bottom: 30rpx;
|
|
}
|
|
|
|
.success-title {
|
|
font-size: 42rpx;
|
|
font-weight: bold;
|
|
color: #1E293B;
|
|
margin-bottom: 20rpx;
|
|
text-align: center;
|
|
}
|
|
|
|
.success-message {
|
|
font-size: 28rpx;
|
|
color: #64748B;
|
|
text-align: center;
|
|
line-height: 1.6;
|
|
margin-bottom: 40rpx;
|
|
}
|
|
.modal-actions {
|
|
flex-direction: row;
|
|
width: 100%;
|
|
}
|
|
|
|
.modal-btn {
|
|
flex: 1;
|
|
height: 80rpx;
|
|
border-radius: 15rpx;
|
|
font-size: 28rpx;
|
|
font-weight: 400;
|
|
border: none;
|
|
margin-right: 20rpx;
|
|
}
|
|
|
|
.modal-btn:last-child {
|
|
margin-right: 0;
|
|
}
|
|
|
|
.modal-btn.primary {
|
|
background-image: linear-gradient(to bottom right, #6366F1, #8B5CF6);
|
|
color: #FFFFFF;
|
|
}
|
|
|
|
.modal-btn.secondary {
|
|
background: #F3F4F6;
|
|
color: #6B7280;
|
|
}
|
|
|
|
.modal-btn:active {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
/* Responsive Design - similar to user-management page */
|
|
@media screen and (max-width: 768px) {
|
|
.content-wrapper.large-screen {
|
|
padding: 30rpx;
|
|
margin: 20rpx 10rpx 0;
|
|
border-radius: 30rpx 30rpx 0 0;
|
|
}
|
|
.timer-controls {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.timer-btn {
|
|
width: 100%;
|
|
justify-content: center;
|
|
margin: 0 0 15rpx 0;
|
|
}
|
|
|
|
.timer-btn:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.action-section {
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
|
|
@media screen and (min-width: 769px) {
|
|
.content-wrapper {
|
|
max-width: 900rpx;
|
|
margin: 20rpx auto 0;
|
|
}
|
|
|
|
.timer-controls {
|
|
flex-direction: row;
|
|
justify-content: center;
|
|
}
|
|
|
|
.action-section {
|
|
flex-direction: row;
|
|
}
|
|
}
|
|
</style> |