Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

View File

@@ -0,0 +1,886 @@
<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>