Files
akmon/pages/sport/teacher/project-create.uvue
2026-01-20 08:04:15 +08:00

886 lines
20 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<scroll-view direction="vertical" class="project-create" :scroll-y="true" :enable-back-to-top="true">
<!-- Header -->
<view class="page-header">
<text class="page-title">创建训练项目</text>
</view>
<!-- Form Container -->
<view class="form-container">
<form @submit="handleSubmit">
<!-- Basic Information -->
<view class="form-section">
<text class="section-title">基本信息</text>
<view class="form-group">
<text class="form-label">项目名称 *</text>
<input class="form-input" v-model="title" placeholder="请输入项目名称" maxlength="50" />
</view>
<view class="form-group">
<text class="form-label">项目描述</text>
<textarea class="form-textarea" v-model="description" placeholder="请输入项目描述"
maxlength="500"></textarea>
</view>
<view class="form-group">
<text class="form-label">训练类别 *</text>
<button class="form-selector" @click="showCategoryPicker">
<text class="selector-text">{{ category ?? '选择训练类别' }}</text>
<text class="selector-arrow">></text>
</button>
</view>
<view class="form-group">
<text class="form-label">难度等级 *</text>
<view class="difficulty-options">
<button class="difficulty-btn" v-for="(level, index) in difficultyLevels" :key="index"
:class="{ active: difficulty === level.value }"
@click="setDifficulty(level.getString('value')??'')">
<text class="difficulty-icon">{{ level.icon }}</text>
<text class="difficulty-text">{{ level.label }}</text>
</button>
</view>
</view>
</view>
<!-- Training Requirements -->
<view class="form-section">
<text class="section-title">训练要求</text>
<view class="requirements-list">
<view class="requirement-item" v-for="(requirement, index) in requirements" :key="index">
<input class="requirement-input" v-model="requirement.text" placeholder="输入训练要求" />
<button class="remove-btn" @click="removeRequirement(index)">
<text class="remove-icon">×</text>
</button>
</view>
</view>
<button class="add-requirement-btn" @click="addRequirement">
<text class="add-icon">+</text>
<text class="add-text">添加要求</text>
</button>
</view>
<!-- Scoring Criteria -->
<view class="form-section">
<text class="section-title">评分标准</text>
<view class="scoring-list">
<view class="scoring-item" v-for="(criteria, index) in scoringCriteria" :key="index">
<view class="score-range-group">
<input class="score-input" v-model="criteria.min_score" placeholder="最低分"
type="number" />
<text class="score-separator">-</text>
<input class="score-input" v-model="criteria.max_score" placeholder="最高分"
type="number" />
</view>
<input class="criteria-input" v-model="criteria.description" placeholder="评分标准描述" />
<button class="remove-btn" @click="removeCriteria(index)">
<text class="remove-icon">×</text>
</button>
</view>
</view>
<button class="add-criteria-btn" @click="addCriteria">
<text class="add-icon">+</text>
<text class="add-text">添加标准</text>
</button>
</view>
<!-- Performance Metrics -->
<view class="form-section">
<text class="section-title">绩效指标</text>
<view class="metrics-list">
<view class="metric-item" v-for="(metric, index) in performanceMetrics" :key="index">
<input class="metric-name" v-model="metric.name" placeholder="指标名称" />
<input class="metric-unit" v-model="metric.unit" placeholder="单位" />
<button class="remove-btn" @click="removeMetric(index)">
<text class="remove-icon">×</text>
</button>
</view>
</view>
<button class="add-metric-btn" @click="addMetric">
<text class="add-icon">+</text>
<text class="add-text">添加指标</text>
</button>
</view>
<!-- Action Buttons -->
<view class="action-buttons">
<button class="action-btn secondary-btn" @click="saveDraft">
保存草稿
</button>
<button class="action-btn primary-btn" @click="submitProject">
创建项目
</button>
</view>
</form>
</view>
<!-- Category Picker Modal -->
<view class="modal-overlay" v-if="showCategoryModal" @click="hideCategoryPicker">
<view class="category-modal" @click.stop>
<view class="modal-header">
<text class="modal-title">选择训练类别</text>
<button class="modal-close-btn" @click="hideCategoryPicker">×</button>
</view>
<view class="category-list"> <button class="category-option" v-for="(categoryItem, index) in categories"
:key="index" @click="selectCategory(categoryItem)">
<text class="category-icon">{{ categoryItem.icon }}</text>
<text class="category-name">{{ categoryItem.name }}</text>
</button>
</view>
</view>
</view>
</scroll-view>
</template>
<script setup lang="uts">
import supaClient from '../../../components/supadb/aksupainstance.uts'
// Type definitions
type CategoryItem = {
name : string
value : string
icon : string
}
type RequirementItem = {
text : string
}
type ScoringCriteriaItem = {
min_score : string
max_score : string
description : string
}
type PerformanceMetricItem = {
name : string
unit : string
}
// 1-dimensional reactive refs
const title = ref<string>('')
const description = ref<string>('')
const category = ref<string>('')
const difficulty = ref<string>('')
// Array refs for dynamic lists - using regular arrays
const requirements = ref<Array<RequirementItem>>([{ text: '' } as RequirementItem])
const scoringCriteria = ref<Array<ScoringCriteriaItem>>([
{ min_score: '', max_score: '', description: '' } as ScoringCriteriaItem
])
const performanceMetrics = ref<Array<PerformanceMetricItem>>([
{ name: '', unit: '' } as PerformanceMetricItem
])
// UI state
const showCategoryModal = ref(false)
const loading = ref(false)
// Computed formData for database operations
const formData = computed(() => {
return {
title: title.value,
description: description.value,
category: category.value,
difficulty: difficulty.value,
requirements: requirements.value,
scoring_criteria: scoringCriteria.value,
performance_metrics: performanceMetrics.value,
status: 'draft'
}
})
const categories : Array<CategoryItem> = [
{ name: '田径运动', icon: '', value: 'athletics' },
{ name: '球类运动', icon: '⚽', value: 'ball_sports' },
{ name: '游泳运动', icon: '', value: 'swimming' },
{ name: '体操运动', icon: '', value: 'gymnastics' },
{ name: '武术运动', icon: '', value: 'martial_arts' },
{ name: '健身运动', icon: '', value: 'fitness' }
]
const difficultyLevels = [
{ label: '初级', value: 'beginner', icon: '' },
{ label: '中级', value: 'intermediate', icon: '' },
{ label: '高级', value: 'advanced', icon: '' },
{ label: '专家', value: 'expert', icon: '' }
]
function initializeForm() {
title.value = ''
description.value = ''
category.value = ''
difficulty.value = ''
requirements.value = [{ text: '' } as RequirementItem]
scoringCriteria.value = [{ min_score: '', max_score: '', description: '' } as ScoringCriteriaItem]
performanceMetrics.value = [{ name: '', unit: '' } as PerformanceMetricItem]
}
function showCategoryPicker() {
showCategoryModal.value = true
}
function hideCategoryPicker() {
showCategoryModal.value = false
}
function selectCategory(categoryItem : any) {
const categoryObj = categoryItem as CategoryItem
category.value = categoryObj.name
hideCategoryPicker()
}
function setDifficulty(difficultyValue : string) {
difficulty.value = difficultyValue
}
function addRequirement() {
requirements.value.push({ text: '' } as RequirementItem)
}
function removeRequirement(index : number) {
if (requirements.value.length > 1) {
requirements.value.splice(index, 1)
}
}
function addCriteria() {
scoringCriteria.value.push({ min_score: '', max_score: '', description: '' } as ScoringCriteriaItem)
}
function removeCriteria(index : number) {
if (scoringCriteria.value.length > 1) {
scoringCriteria.value.splice(index, 1)
}
}
function addMetric() {
performanceMetrics.value.push({ name: '', unit: '' } as PerformanceMetricItem)
}
function removeMetric(index : number) {
if (performanceMetrics.value.length > 1) {
performanceMetrics.value.splice(index, 1)
}
}
function validateForm() : boolean {
if (title.value.trim() === '') {
uni.showToast({
title: '请输入项目名称',
icon: 'none'
})
return false
}
if (category.value.trim() === '') {
uni.showToast({
title: '请选择训练类别',
icon: 'none'
})
return false
}
if (difficulty.value.trim() === '') {
uni.showToast({
title: '请选择难度等级',
icon: 'none'
})
return false
}
return true
}
async function saveDraft() {
if (title.value.trim() === '') {
uni.showToast({
title: '请至少输入项目名称',
icon: 'none'
})
return
}
loading.value = true
try {
// Convert form data to database format
const objectives = requirements.value
.map(req => req.text)
.filter(text => text.trim().length > 0)
// Create scoring criteria JSON structure
type MappedCriteria = {
min_score : number
max_score : number
description : string
}
const mappedCriteria : Array<MappedCriteria> = []
for (let i = 0; i < scoringCriteria.value.length; i++) {
const criteria = scoringCriteria.value[i]
mappedCriteria.push({
min_score: parseInt(criteria.min_score) ?? 0,
max_score: parseInt(criteria.max_score) ?? 100,
description: criteria.description
} as MappedCriteria)
}
const filteredCriteria = mappedCriteria.filter((item : MappedCriteria) : boolean => {
return item.description.trim().length > 0
})
const scoringCriteriaJson = {
criteria: filteredCriteria,
scoring_method: "comprehensive",
weight_distribution: {
technique: 0.4, // 技术动作权重 40%
effort: 0.3, // 努力程度权重 30%
improvement: 0.3 // 进步幅度权重 30%
}
}
const equipmentRequired = performanceMetrics.value
.map(metric => metric.name)
.filter(name => name.trim().length > 0)
const insertResult = await supaClient
.from('ak_training_projects')
.insert({
title: title.value,
description: description.value,
sport_type: category.value,
difficulty_level: difficulty.value,
is_active: false, // draft is inactive image_url: '',
video_url: '',
objectives: objectives,
scoring_criteria: scoringCriteriaJson,
equipment_required: equipmentRequired,
created_at: new Date().toISOString(), updated_at: new Date().toISOString()
})
.execute()
if (insertResult.error != null) {
console.error('Error saving draft:', insertResult.error)
uni.showToast({
title: '保存草稿失败',
icon: 'none'
})
} else {
uni.showToast({
title: '草稿保存成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
} catch (error) {
console.error('Error saving draft:', error)
uni.showToast({
title: '保存草稿失败',
icon: 'none'
})
} finally {
loading.value = false
}
} async function submitProject() {
if (!validateForm()) return
loading.value = true
try {
// Convert form data to database format
const objectives = requirements.value
.map(req => req.text)
.filter(text => text.trim().length > 0)
// Create scoring criteria JSON structure
type MappedCriteria = {
min_score : number
max_score : number
description : string
}
const mappedCriteria : Array<MappedCriteria> = []
for (let i = 0; i < scoringCriteria.value.length; i++) {
const criteria = scoringCriteria.value[i]
mappedCriteria.push({
min_score: parseInt(criteria.min_score) ?? 0,
max_score: parseInt(criteria.max_score) ?? 100,
description: criteria.description
} as MappedCriteria)
}
const filteredCriteria = mappedCriteria.filter((item : MappedCriteria) : boolean => {
return item.description.trim().length > 0
})
const scoringCriteriaJson = {
criteria: filteredCriteria,
scoring_method: "comprehensive",
weight_distribution: {
technique: 0.4, // 技术动作权重 40%
effort: 0.3, // 努力程度权重 30%
improvement: 0.3 // 进步幅度权重 30%
}
}
const equipmentRequired = performanceMetrics.value
.map((metric : PerformanceMetricItem) => metric.name)
.filter((name : string) => name.trim().length > 0)
const insertResult = await supaClient
.from('ak_training_projects')
.insert({
title: title.value,
description: description.value,
sport_type: category.value,
difficulty_level: difficulty.value,
is_active: true, // active project
image_url: '',
video_url: '',
objectives: objectives,
scoring_criteria: scoringCriteriaJson,
equipment_required: equipmentRequired,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
})
.execute()
if (insertResult.error != null) {
console.error('Error creating project:', insertResult.error)
uni.showToast({
title: '项目创建失败',
icon: 'none'
})
} else {
uni.showToast({
title: '项目创建成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
} catch (error) {
console.error('Error creating project:', error)
uni.showToast({
title: '项目创建失败',
icon: 'none'
})
} finally {
loading.value = false
}
}
function handleSubmit(event : Event) {
event.preventDefault()
submitProject()
}
// Lifecycle
onLoad(() => {
initializeForm()
})
</script>
<style>
.project-create {
flex:1;
background-color: #f5f5f5;
padding: 20rpx;
padding-bottom: 40rpx;
box-sizing: border-box;
}
/* Header */
.page-header {
margin-bottom: 25rpx;
}
.page-title {
font-size: 40rpx;
font-weight: bold;
color: #333;
}
/* Form Container */
.form-container {
background-color: white;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 2rpx 20rpx rgba(0, 0, 0, 0.05);
}
.form-section {
margin-bottom: 40rpx;
}
.form-section:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 25rpx;
display: block;
}
/* Form Controls */
.form-group {
margin-bottom: 25rpx;
}
.form-label {
font-size: 28rpx;
color: #333;
margin-bottom: 10rpx;
display: block;
font-weight: 400;
}
.form-input {
width: 100%;
height: 88rpx;
border: 2rpx solid #e0e0e0;
border-radius: 12rpx;
padding: 0 20rpx;
font-size: 28rpx;
color: #333;
background-color: #fff;
}
.form-input:focus {
border-color: #667eea;
outline: none;
}
.form-textarea {
width: 100%;
min-height: 160rpx;
border: 2rpx solid #e0e0e0;
border-radius: 12rpx;
padding: 20rpx;
font-size: 28rpx;
color: #333;
background-color: #fff;
resize: vertical;
}
.form-textarea:focus {
border-color: #667eea;
outline: none;
}
.form-selector {
width: 100%;
height: 88rpx;
border: 2rpx solid #e0e0e0;
border-radius: 12rpx;
padding: 0 20rpx;
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
}
.selector-text {
font-size: 28rpx;
color: #333;
}
.selector-arrow {
font-size: 24rpx;
color: #666;
}
/* Difficulty Options */
.difficulty-options {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 -7.5rpx;
}
.difficulty-options .difficulty-btn {
width: 45%;
flex: 0 0 45%;
margin: 0 7.5rpx 15rpx;
}
.difficulty-btn {
height: 100rpx;
border: 2rpx solid #e0e0e0;
border-radius: 12rpx;
background-color: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.difficulty-btn .difficulty-text {
margin-top: 8rpx;
}
.difficulty-btn.active {
border-color: #667eea;
background-color: #f8f9ff;
}
.difficulty-icon {
font-size: 32rpx;
}
.difficulty-text {
font-size: 24rpx;
color: #333;
font-weight: 400;
}
/* Dynamic Lists */
.requirements-list,
.scoring-list,
.metrics-list {
margin-bottom: 20rpx;
}
.requirement-item,
.scoring-item,
.metric-item {
display: flex;
align-items: center;
margin-bottom: 15rpx;
}
.requirement-item .remove-btn,
.scoring-item .remove-btn,
.metric-item .remove-btn {
margin-left: 15rpx;
}
.requirement-input,
.criteria-input,
.metric-name {
flex: 1;
height: 70rpx;
border: 2rpx solid #e0e0e0;
border-radius: 8rpx;
padding: 0 15rpx;
font-size: 26rpx;
color: #333;
}
.score-range-group {
display: flex;
align-items: center;
min-width: 200rpx;
}
.score-range-group .score-input {
margin-right: 10rpx;
}
.score-range-group .score-input:last-child {
margin-right: 0;
margin-left: 10rpx;
}
.score-input {
width: 80rpx;
height: 70rpx;
border: 2rpx solid #e0e0e0;
border-radius: 8rpx;
padding: 0 10rpx;
font-size: 26rpx;
text-align: center;
}
.score-separator {
font-size: 24rpx;
color: #666;
}
.metric-unit {
width: 120rpx;
height: 70rpx;
border: 2rpx solid #e0e0e0;
border-radius: 8rpx;
padding: 0 15rpx;
font-size: 26rpx;
text-align: center;
}
.remove-btn {
width: 60rpx;
height: 60rpx;
border-radius: 30rpx;
background-color: #ff4757;
border: none;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.remove-icon {
font-size: 32rpx;
color: white;
font-weight: bold;
}
/* Add Buttons */
.add-requirement-btn,
.add-criteria-btn,
.add-metric-btn {
width: 100%;
height: 70rpx;
border: 2rpx dashed #667eea;
border-radius: 8rpx;
background-color: #f8f9ff;
display: flex;
align-items: center;
justify-content: center;
}
.add-requirement-btn .add-text,
.add-criteria-btn .add-text,
.add-metric-btn .add-text {
margin-left: 10rpx;
}
.add-icon {
font-size: 28rpx;
color: #667eea;
font-weight: bold;
}
.add-text {
font-size: 26rpx;
color: #667eea;
font-weight: 400;
}
/* Action Buttons */
.action-buttons {
display: flex;
margin-top: 40rpx;
}
.action-buttons .action-btn {
margin-right: 20rpx;
}
.action-buttons .action-btn:last-child {
margin-right: 0;
}
.action-btn {
flex: 1;
height: 88rpx;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: bold;
border: none;
}
.primary-btn {
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
color: white;
}
.secondary-btn {
background-color: #f5f5f5;
color: #666;
border: 2rpx solid #e0e0e0;
}
/* Category Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.category-modal {
width: 90%;
max-width: 600rpx;
background-color: white;
border-radius: 20rpx;
overflow: hidden;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.modal-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.modal-close-btn {
width: 60rpx;
height: 60rpx;
border-radius: 30rpx;
background-color: #f5f5f5;
border: none;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: #666;
}
.category-list {
padding: 20rpx;
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 -7.5rpx;
}
.category-list .category-option {
width: 45%;
flex: 0 0 45%;
margin: 0 7.5rpx 15rpx;
}
.category-option {
height: 120rpx;
border: 2rpx solid #e0e0e0;
border-radius: 12rpx;
background-color: white;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.category-option .category-name {
margin-top: 8rpx;
}
.category-option:active {
background-color: #f8f9ff;
border-color: #667eea;
}
.category-icon {
font-size: 32rpx;
}
.category-name {
font-size: 24rpx;
color: #333;
font-weight: 400;
}
</style>