1448 lines
37 KiB
Plaintext
1448 lines
37 KiB
Plaintext
<template>
|
||
<scroll-view direction="vertical" class="project-edit" :class="{ 'small-screen': !isLargeScreen }">
|
||
<!-- Header -->
|
||
<view class="page-header">
|
||
<text class="page-title">编辑训练项目</text>
|
||
</view>
|
||
|
||
<!-- Loading State -->
|
||
<view class="loading-container" v-if="loading">
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
|
||
<!-- Form Container -->
|
||
<scroll-view class="form-container" v-else scroll-y="true" enhanced="true">
|
||
<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" :value="getFormTitle()" @input="handleTitleInput"
|
||
placeholder="请输入项目名称" maxlength="50" />
|
||
</view>
|
||
|
||
<view class="form-group">
|
||
<text class="form-label">项目描述</text>
|
||
<textarea class="form-textarea" :value="getFormDescription()" @input="handleDescriptionInput"
|
||
placeholder="请输入项目描述" maxlength="500"></textarea>
|
||
</view>
|
||
|
||
<view class="form-group">
|
||
<text class="form-label">训练类别 *</text> <button class="form-selector"
|
||
@click="showCategoryPicker">
|
||
<text class="selector-text">{{ getFormCategory() ?? '选择训练类别' }}</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: getFormDifficulty() === 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 class="form-group">
|
||
<text class="form-label">项目状态</text>
|
||
<view class="status-options"> <button class="status-btn"
|
||
v-for="(status, index) in statusOptions" :key="index"
|
||
:class="{ active: getFormStatus() === status.value }"
|
||
@click="setStatus(status.getString('value')??'')">
|
||
<text class="status-text">{{ status.label }}</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Media Files -->
|
||
<view class="form-section">
|
||
<text class="section-title">媒体资源</text>
|
||
|
||
<view class="form-group">
|
||
<text class="form-label">项目图片</text>
|
||
<view class="media-upload-container">
|
||
<view class="image-preview" v-if="getImageUrl()">
|
||
<image class="preview-image" :src="getImageUrl()" mode="aspectFill"></image>
|
||
<button class="remove-media-btn" @click="removeImage">
|
||
<text class="remove-media-icon">×</text>
|
||
</button>
|
||
</view>
|
||
<button class="upload-btn" @click="uploadImage" v-if="hasNoImage()">>
|
||
<text class="upload-icon"></text>
|
||
<text class="upload-text">上传图片</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="form-group">
|
||
<text class="form-label">演示视频</text>
|
||
<view class="media-upload-container">
|
||
<view class="video-preview" v-if="getVideoUrl()">
|
||
<video class="preview-video" :src="getVideoUrl()" controls></video>
|
||
<button class="remove-media-btn" @click="removeVideo">
|
||
<text class="remove-media-icon">×</text>
|
||
</button>
|
||
</view>
|
||
<button class="upload-btn" @click="uploadVideo" v-if="hasNoVideo()">>
|
||
<text class="upload-icon"></text>
|
||
<text class="upload-text">上传视频</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 getRequirements()" :key="index">
|
||
<input class="requirement-input" :value="requirement.getString('text') ?? ''"
|
||
@input="(e: InputEvent) => updateRequirement(index, e.detail.value)"
|
||
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 getScoringCriteria()" :key="index">
|
||
<view class="score-range-group"> <input class="score-input"
|
||
:value="criteria.getString('min_score') ?? ''"
|
||
@input="(e: InputEvent) => updateCriteriaMinScore(index, e.detail.value)"
|
||
placeholder="最低分" type="number" />
|
||
<text class="score-separator">-</text>
|
||
<input class="score-input" :value="criteria.getString('max_score') ?? ''"
|
||
@input="(e: InputEvent) => updateCriteriaMaxScore(index, e.detail.value)"
|
||
placeholder="最高分" type="number" />
|
||
</view> <input class="criteria-input" :value="criteria.getString('description') ?? ''"
|
||
@input="(e: InputEvent) => updateCriteriaDescription(index, e.detail.value)"
|
||
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 getPerformanceMetrics()" :key="index">
|
||
<input class="metric-name" :value="metric.getString('name') ?? ''"
|
||
@input="(e: InputEvent) => updateMetricName(index, e.detail.value)"
|
||
placeholder="指标名称" />
|
||
<input class="metric-unit" :value="metric.getString('unit') ?? ''"
|
||
@input="(e: InputEvent) => updateMetricUnit(index, e.detail.value)" 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 danger-btn" @click="deleteProject">
|
||
删除项目
|
||
</button>
|
||
<button class="action-btn secondary-btn" @click="saveDraft">
|
||
保存草稿
|
||
</button>
|
||
<button class="action-btn primary-btn" @click="updateProject">
|
||
保存更改
|
||
</button>
|
||
</view>
|
||
</form>
|
||
</scroll-view>
|
||
|
||
<!-- Category Picker Modal -->
|
||
<view class="modal-overlay" v-if="showCategoryModal" @click="hideCategoryPicker">
|
||
<view class="category-modal">
|
||
<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="(category, index) in categories" :key="index"
|
||
@click="selectCategory(category)">
|
||
<text class="category-icon">{{ category.icon }}</text>
|
||
<text class="category-name">{{ category.name }}</text>
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import supaClient from '../../../components/supadb/aksupainstance.uts'
|
||
|
||
function createEmptyUTSJSONObject() : UTSJSONObject {
|
||
return {} as UTSJSONObject
|
||
}
|
||
|
||
// Reactive data
|
||
const formData = ref<UTSJSONObject>(createEmptyUTSJSONObject())
|
||
const projectId = ref<string>('')
|
||
const showCategoryModal = ref(false)
|
||
const loading = ref(true)
|
||
const updating = ref(false)
|
||
|
||
// Responsive state - using standard pattern
|
||
const screenWidth = ref<number>(uni.getSystemInfoSync().windowWidth)
|
||
|
||
// Computed properties for responsive design
|
||
const isLargeScreen = computed(() : boolean => {
|
||
return screenWidth.value >= 768
|
||
})
|
||
|
||
// Screen size detection for responsive layout (deprecated - kept for compatibility)
|
||
function updateScreenInfo() {
|
||
screenWidth.value = uni.getSystemInfoSync().windowWidth
|
||
}
|
||
|
||
const categories = [
|
||
{ 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: '' }
|
||
]
|
||
|
||
const statusOptions = [
|
||
{ label: '草稿', value: 'draft' },
|
||
{ label: '激活', value: 'active' },
|
||
{ label: '暂停', value: 'inactive' },
|
||
{ label: '归档', value: 'archived' }
|
||
] // Lifecycle
|
||
// Methods
|
||
async function loadProjectData() {
|
||
loading.value = true
|
||
try {
|
||
const { data, error } = await supaClient
|
||
.from('ak_training_projects')
|
||
.select('*', {})
|
||
.eq('id', projectId.value)
|
||
.execute()
|
||
|
||
if (error != null) {
|
||
console.error('Error loading project:', error)
|
||
uni.showToast({
|
||
title: '加载项目失败',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
if (data != null && Array.isArray(data) && data.length > 0) {
|
||
const projectData = data[0] as UTSJSONObject
|
||
// Map database fields to form fields
|
||
const objectives = projectData.get('objectives') as Array<any> ?? []
|
||
const instructions = projectData.getString('instructions') ?? ''
|
||
const equipmentRequired = projectData.get('equipment_required') as Array<any> ?? []
|
||
|
||
// Map objectives to requirements array
|
||
let requirements : Array<UTSJSONObject> = []
|
||
if (Array.isArray(objectives) && objectives.length > 0) {
|
||
requirements = objectives.map((obj : any) => ({ text: obj.toString() } as UTSJSONObject))
|
||
} else {
|
||
requirements = [{ text: '' } as UTSJSONObject]
|
||
} // Map scoring_criteria to form data (new JSON structure)
|
||
let scoringCriteria : Array<UTSJSONObject> = []
|
||
const criteriaData = projectData.get('scoring_criteria')
|
||
if (criteriaData != null && typeof criteriaData === 'object') {
|
||
// New JSON format: {criteria: [{min_score, max_score, description}], ...}
|
||
const criteriaObj = criteriaData as UTSJSONObject
|
||
const criteria = criteriaObj.get('criteria') as Array<any> ?? []
|
||
if (criteria instanceof Array) {
|
||
scoringCriteria = criteria.map((item : any) => {
|
||
const itemObj = item as UTSJSONObject
|
||
return {
|
||
min_score: itemObj.getString('min_score') ?? '',
|
||
max_score: itemObj.getString('max_score') ?? '',
|
||
description: itemObj.getString('description') ?? ''
|
||
} as UTSJSONObject
|
||
})
|
||
}
|
||
} else if (instructions !== null && instructions.length > 0) {
|
||
// Fallback: Legacy format from instructions field (for backward compatibility)
|
||
const instructionSteps = instructions.split('\n').filter((step : string) => step.trim().length > 0)
|
||
scoringCriteria = instructionSteps.map((step : string, index : number) => ({
|
||
min_score: (index * 20).toString(),
|
||
max_score: ((index + 1) * 20).toString(),
|
||
description: step.trim()
|
||
} as UTSJSONObject))
|
||
}
|
||
|
||
if (scoringCriteria.length === 0) {
|
||
scoringCriteria = [{ min_score: '', max_score: '', description: '' } as UTSJSONObject]
|
||
}
|
||
|
||
// Map equipment to performance metrics
|
||
let performanceMetrics : Array<UTSJSONObject> = []
|
||
if (equipmentRequired instanceof Array && equipmentRequired.length > 0) {
|
||
performanceMetrics = equipmentRequired.map((equipment : any) => ({
|
||
name: equipment.toString(),
|
||
unit: '个'
|
||
} as UTSJSONObject))
|
||
} else {
|
||
performanceMetrics = [{ name: '', unit: '' } as UTSJSONObject]
|
||
} // Initialize form data with mapped information
|
||
formData.value = {
|
||
title: projectData.getString('title') ?? '',
|
||
description: projectData.getString('description') ?? '',
|
||
category: projectData.getString('sport_type') ?? '',
|
||
difficulty: projectData.getString('difficulty_level') ?? '',
|
||
status: (projectData.getBoolean('is_active') ?? true) ? 'active' : 'inactive',
|
||
image_url: projectData.getString('image_url') ?? '',
|
||
video_url: projectData.getString('video_url') ?? '',
|
||
requirements: requirements,
|
||
scoring_criteria: scoringCriteria,
|
||
performance_metrics: performanceMetrics
|
||
} as UTSJSONObject
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading project:', error)
|
||
uni.showToast({
|
||
title: '加载项目失败',
|
||
icon: 'none'
|
||
})
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
} function getRequirements() : Array<UTSJSONObject> {
|
||
const requirements = formData.value.get('requirements') as Array<UTSJSONObject> ?? []
|
||
if (Array.isArray(requirements)) {
|
||
return requirements as Array<UTSJSONObject>
|
||
}
|
||
return [{ text: '' } as UTSJSONObject]
|
||
}
|
||
|
||
function getScoringCriteria() : Array<UTSJSONObject> {
|
||
const criteria = formData.value.get('scoring_criteria') as Array<UTSJSONObject> ?? []
|
||
if (Array.isArray(criteria)) {
|
||
return criteria as Array<UTSJSONObject>
|
||
}
|
||
return [{ min_score: '', max_score: '', description: '' } as UTSJSONObject]
|
||
}
|
||
|
||
function getPerformanceMetrics() : Array<UTSJSONObject> {
|
||
const metrics = formData.value.get('performance_metrics') as Array<UTSJSONObject> ?? []
|
||
if (Array.isArray(metrics)) {
|
||
return metrics as Array<UTSJSONObject>
|
||
}
|
||
return [{ name: '', unit: '' } as UTSJSONObject]
|
||
}
|
||
|
||
function showCategoryPicker() {
|
||
showCategoryModal.value = true
|
||
}
|
||
|
||
function hideCategoryPicker() {
|
||
showCategoryModal.value = false
|
||
}
|
||
function selectCategory(category : UTSJSONObject) {
|
||
const categoryName = category.getString('name') ?? ''
|
||
formData.value.category = categoryName
|
||
hideCategoryPicker()
|
||
}
|
||
|
||
function setDifficulty(difficulty : string) {
|
||
formData.value.difficulty = difficulty
|
||
}
|
||
|
||
function setStatus(status : string) {
|
||
formData.value.status = status
|
||
}
|
||
function addRequirement() {
|
||
const requirements = getRequirements()
|
||
requirements.push({ text: '' } as UTSJSONObject)
|
||
formData.value.requirements = requirements
|
||
}
|
||
|
||
function removeRequirement(index : number) {
|
||
const requirements = getRequirements()
|
||
if (requirements.length > 1) {
|
||
requirements.splice(index, 1)
|
||
formData.value.requirements = requirements
|
||
}
|
||
}
|
||
|
||
function addCriteria() {
|
||
const criteria = getScoringCriteria()
|
||
criteria.push({ min_score: '', max_score: '', description: '' } as UTSJSONObject)
|
||
formData.value.scoring_criteria = criteria
|
||
}
|
||
|
||
function removeCriteria(index : number) {
|
||
const criteria = getScoringCriteria()
|
||
if (criteria.length > 1) {
|
||
criteria.splice(index, 1)
|
||
formData.value.scoring_criteria = criteria
|
||
}
|
||
}
|
||
|
||
function addMetric() {
|
||
const metrics = getPerformanceMetrics()
|
||
metrics.push({ name: '', unit: '' } as UTSJSONObject)
|
||
formData.value.performance_metrics = metrics
|
||
}
|
||
|
||
function removeMetric(index : number) {
|
||
const metrics = getPerformanceMetrics()
|
||
if (metrics.length > 1) {
|
||
metrics.splice(index, 1)
|
||
formData.value.performance_metrics = metrics
|
||
}
|
||
} function validateForm() : boolean {
|
||
const title = formData.value.getString('title') ?? ''
|
||
const category = formData.value.getString('category') ?? ''
|
||
const difficulty = formData.value.getString('difficulty') ?? ''
|
||
|
||
if (title === '' || title.length === 0) {
|
||
uni.showToast({
|
||
title: '请输入项目名称',
|
||
icon: 'none'
|
||
})
|
||
return false
|
||
}
|
||
|
||
if (category === '' || category.length === 0) {
|
||
uni.showToast({
|
||
title: '请选择训练类别',
|
||
icon: 'none'
|
||
})
|
||
return false
|
||
}
|
||
|
||
if (difficulty === '' || difficulty.length === 0) {
|
||
uni.showToast({
|
||
title: '请选择难度等级',
|
||
icon: 'none'
|
||
})
|
||
return false
|
||
}
|
||
|
||
return true
|
||
} async function saveDraft() {
|
||
const title = formData.value.getString('title') ?? ''
|
||
if (title === '' || title.length === 0) {
|
||
uni.showToast({
|
||
title: '请至少输入项目名称',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
updating.value = true
|
||
formData.value.status = 'draft'
|
||
try {
|
||
// Convert form data back to database format
|
||
const requirements = getRequirements()
|
||
const objectives = requirements.map((req : UTSJSONObject) => req.getString('text') ?? '')
|
||
.filter((text : string) => text.trim().length > 0)
|
||
|
||
const scoringCriteria = getScoringCriteria()
|
||
|
||
// Create new JSON structure for scoring criteria
|
||
const scoringCriteriaJson = {
|
||
criteria: scoringCriteria.map((criteria : UTSJSONObject) => ({
|
||
min_score: parseInt(criteria.getString('min_score') ?? '0') ?? 0, max_score: parseInt(criteria.getString('max_score') ?? '100') ?? 100,
|
||
description: criteria.getString('description') ?? ''
|
||
})).filter((item : UTSJSONObject) => {
|
||
const desc = item.getString('description') ?? ''
|
||
return desc.trim().length > 0
|
||
}),
|
||
scoring_method: "comprehensive",
|
||
weight_distribution: {
|
||
technique: 0.4, // 技术动作权重 40%
|
||
effort: 0.3, // 努力程度权重 30%
|
||
improvement: 0.3 // 进步幅度权重 30%
|
||
}
|
||
}
|
||
const performanceMetrics = getPerformanceMetrics()
|
||
const equipmentRequired = performanceMetrics.map((metric : UTSJSONObject) =>
|
||
metric.getString('name') ?? '').filter((name : string) => name.trim().length > 0)
|
||
|
||
// Create instructions from scoring criteria descriptions
|
||
const instructions = scoringCriteria.map((criteria : UTSJSONObject) =>
|
||
criteria.getString('description') ?? '').filter((desc : string) => desc.trim().length > 0).join('\n')
|
||
const { data, error } = await supaClient
|
||
.from('ak_training_projects')
|
||
.update({
|
||
title: formData.value.getString('title') ?? '',
|
||
description: formData.value.getString('description') ?? '',
|
||
sport_type: formData.value.getString('category') ?? '',
|
||
difficulty_level: formData.value.getString('difficulty') ?? '',
|
||
is_active: false, // draft is inactive
|
||
image_url: formData.value.getString('image_url') ?? '',
|
||
video_url: formData.value.getString('video_url') ?? '',
|
||
objectives: objectives,
|
||
instructions: instructions,
|
||
equipment_required: equipmentRequired,
|
||
updated_at: new Date().toISOString()
|
||
})
|
||
.eq('id', projectId.value)
|
||
.execute()
|
||
|
||
if (error !== null) {
|
||
console.error('Error saving draft:', error)
|
||
uni.showToast({
|
||
title: '保存草稿失败',
|
||
icon: 'none'
|
||
})
|
||
} else {
|
||
uni.showToast({
|
||
title: '草稿保存成功',
|
||
icon: 'success'
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.error('Error saving draft:', error)
|
||
uni.showToast({
|
||
title: '保存草稿失败',
|
||
icon: 'none'
|
||
})
|
||
} finally {
|
||
updating.value = false
|
||
}
|
||
}
|
||
async function updateProject() {
|
||
if (!validateForm()) return
|
||
|
||
updating.value = true
|
||
try { // Convert form data back to database format
|
||
const requirements = getRequirements()
|
||
const objectives = requirements.map((req : UTSJSONObject) => req.getString('text') ?? '')
|
||
.filter((text : string) => text.trim().length > 0)
|
||
|
||
const scoringCriteria = getScoringCriteria()
|
||
|
||
// Create new JSON structure for scoring criteria
|
||
const scoringCriteriaJson = {
|
||
criteria: scoringCriteria.map((criteria : UTSJSONObject) => ({
|
||
min_score: parseInt(criteria.getString('min_score') ?? '0') ?? 0,
|
||
max_score: parseInt(criteria.getString('max_score') ?? '100') ?? 100,
|
||
description: criteria.getString('description') ?? ''
|
||
})).filter((item : UTSJSONObject) => {
|
||
const desc = item.getString('description') ?? ''
|
||
return desc.trim().length > 0
|
||
}),
|
||
scoring_method: "comprehensive",
|
||
weight_distribution: {
|
||
technique: 0.4, // 技术动作权重 40%
|
||
effort: 0.3, // 努力程度权重 30%
|
||
improvement: 0.3 // 进步幅度权重 30%
|
||
}
|
||
}
|
||
|
||
const performanceMetrics = getPerformanceMetrics()
|
||
|
||
const equipmentRequired = performanceMetrics.map((metric : UTSJSONObject) =>
|
||
metric.getString('name') ?? '').filter((name : string) => name.trim().length > 0)
|
||
const { data, error } = await supaClient
|
||
.from('ak_training_projects')
|
||
.update({
|
||
title: formData.value.getString('title') ?? '',
|
||
description: formData.value.getString('description') ?? '',
|
||
sport_type: formData.value.getString('category') ?? '',
|
||
difficulty_level: formData.value.getString('difficulty') ?? '',
|
||
is_active: (formData.value.getString('status') ?? 'active') === 'active',
|
||
image_url: formData.value.getString('image_url') ?? '',
|
||
video_url: formData.value.getString('video_url') ?? '',
|
||
objectives: objectives,
|
||
scoring_criteria: scoringCriteriaJson,
|
||
equipment_required: equipmentRequired,
|
||
updated_at: new Date().toISOString()
|
||
}).eq('id', projectId.value)
|
||
.execute()
|
||
|
||
if (error !== null) {
|
||
console.error('Error updating project:', error)
|
||
uni.showToast({
|
||
title: '项目更新失败',
|
||
icon: 'none'
|
||
})
|
||
} else {
|
||
uni.showToast({
|
||
title: '项目更新成功',
|
||
icon: 'success'
|
||
})
|
||
setTimeout(() => {
|
||
uni.navigateBack()
|
||
}, 1500)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating project:', error)
|
||
uni.showToast({
|
||
title: '项目更新失败',
|
||
icon: 'none'
|
||
})
|
||
} finally {
|
||
updating.value = false
|
||
}
|
||
}
|
||
|
||
|
||
async function deleteProjectConfirmed() {
|
||
try {
|
||
const { data, error } = await supaClient
|
||
.from('ak_training_projects')
|
||
.delete()
|
||
.eq('id', projectId.value)
|
||
.execute()
|
||
|
||
if (error !== null) {
|
||
console.error('Error deleting project:', error)
|
||
uni.showToast({
|
||
title: '项目删除失败',
|
||
icon: 'none'
|
||
})
|
||
} else {
|
||
|
||
uni.showToast({
|
||
title: '项目删除成功',
|
||
icon: 'success'
|
||
})
|
||
setTimeout(() => {
|
||
uni.navigateBack()
|
||
}, 1500)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting project:', error)
|
||
uni.showToast({
|
||
title: '项目删除失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
}
|
||
function deleteProject() {
|
||
uni.showModal({
|
||
title: '确认删除',
|
||
content: '删除后将无法恢复,确定要删除这个项目吗?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
deleteProjectConfirmed()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
function handleSubmit(event : Event) {
|
||
event.preventDefault()
|
||
updateProject()
|
||
}
|
||
|
||
// Media upload functions
|
||
function uploadImage() {
|
||
uni.chooseImage({
|
||
count: 1,
|
||
sizeType: ['compressed'],
|
||
sourceType: ['album', 'camera'],
|
||
success: (res) => {
|
||
// 这里需要实现实际的上传逻辑
|
||
// 目前暂时使用本地路径,实际项目中需要上传到服务器
|
||
const tempFilePath = res.tempFilePaths[0]
|
||
formData.value.image_url = tempFilePath
|
||
|
||
uni.showToast({
|
||
title: '图片上传成功',
|
||
icon: 'success'
|
||
})
|
||
|
||
// TODO: 实现实际的文件上传到服务器逻辑
|
||
// uploadFileToServer(tempFilePath, 'image')
|
||
},
|
||
fail: (error) => {
|
||
console.error('选择图片失败:', error)
|
||
uni.showToast({
|
||
title: '选择图片失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
})
|
||
}
|
||
|
||
function uploadVideo() {
|
||
uni.chooseVideo({
|
||
sourceType: ['album', 'camera'],
|
||
maxDuration: 60, // 最长60秒
|
||
success: (res) => {
|
||
// 这里需要实现实际的上传逻辑
|
||
const tempFilePath = res.tempFilePath
|
||
formData.value.video_url = tempFilePath
|
||
|
||
uni.showToast({
|
||
title: '视频上传成功',
|
||
icon: 'success'
|
||
})
|
||
|
||
// TODO: 实现实际的文件上传到服务器逻辑
|
||
// uploadFileToServer(tempFilePath, 'video')
|
||
},
|
||
fail: (error) => {
|
||
console.error('选择视频失败:', error)
|
||
uni.showToast({
|
||
title: '选择视频失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
})
|
||
}
|
||
|
||
function removeImage() {
|
||
formData.value.image_url = ''
|
||
uni.showToast({
|
||
title: '图片已移除',
|
||
icon: 'success'
|
||
})
|
||
}
|
||
|
||
function removeVideo() {
|
||
formData.value.video_url = ''
|
||
uni.showToast({
|
||
title: '视频已移除',
|
||
icon: 'success'
|
||
})
|
||
}
|
||
// Template accessor functions for safe data access
|
||
function getFormTitle() : string {
|
||
return formData.value.getString('title') ?? ''
|
||
}
|
||
|
||
function getFormDescription() : string {
|
||
return formData.value.getString('description') ?? ''
|
||
}
|
||
|
||
function getFormCategory() : string {
|
||
return formData.value.getString('category') ?? ''
|
||
}
|
||
|
||
function getFormDifficulty() : string {
|
||
return formData.value.getString('difficulty') ?? ''
|
||
}
|
||
function getFormStatus() : string {
|
||
return formData.value.getString('status') ?? 'draft'
|
||
}
|
||
|
||
function getImageUrl() : string {
|
||
return formData.value.getString('image_url') ?? ''
|
||
}
|
||
|
||
function getVideoUrl() : string {
|
||
return formData.value.getString('video_url') ?? ''
|
||
}
|
||
|
||
function hasNoImage() : boolean {
|
||
const imageUrl = getImageUrl()
|
||
return imageUrl === '' || imageUrl.length === 0
|
||
}
|
||
|
||
function hasNoVideo() : boolean {
|
||
const videoUrl = getVideoUrl()
|
||
return videoUrl === '' || videoUrl.length === 0
|
||
}
|
||
|
||
function isScreenSmall() : boolean {
|
||
return !isLargeScreen.value
|
||
}
|
||
|
||
// Input event handlers
|
||
function handleTitleInput(e : InputEvent) {
|
||
formData.value.set('title', e.detail.value)
|
||
}
|
||
|
||
function handleDescriptionInput(e : InputEvent) {
|
||
formData.value.set('description', e.detail.value)
|
||
}
|
||
|
||
// Update functions for form data
|
||
function updateRequirement(index : number, value : string) {
|
||
const requirements = getRequirements()
|
||
if (index >= 0 && index < requirements.length && requirements[index] !== null) {
|
||
requirements[index].text = value
|
||
formData.value.requirements = requirements
|
||
}
|
||
}
|
||
|
||
function updateCriteriaMinScore(index : number, value : string) {
|
||
const criteria = getScoringCriteria()
|
||
if (index >= 0 && index < criteria.length && criteria[index] !== null) {
|
||
criteria[index].min_score = value
|
||
formData.value.scoring_criteria = criteria
|
||
}
|
||
}
|
||
|
||
function updateCriteriaMaxScore(index : number, value : string) {
|
||
const criteria = getScoringCriteria()
|
||
if (index >= 0 && index < criteria.length && criteria[index] !== null) {
|
||
criteria[index].max_score = value
|
||
formData.value.scoring_criteria = criteria
|
||
}
|
||
}
|
||
|
||
function updateCriteriaDescription(index : number, value : string) {
|
||
const criteria = getScoringCriteria()
|
||
if (index >= 0 && index < criteria.length && criteria[index] !== null) {
|
||
criteria[index].description = value
|
||
formData.value.scoring_criteria = criteria
|
||
}
|
||
}
|
||
|
||
function updateMetricName(index : number, value : string) {
|
||
const metrics = getPerformanceMetrics()
|
||
if (index >= 0 && index < metrics.length && metrics[index] !== null) {
|
||
metrics[index].name = value
|
||
formData.value.performance_metrics = metrics
|
||
}
|
||
}
|
||
|
||
function updateMetricUnit(index : number, value : string) {
|
||
const metrics = getPerformanceMetrics()
|
||
if (index >= 0 && index < metrics.length && metrics[index] !== null) {
|
||
metrics[index].unit = value
|
||
formData.value.performance_metrics = metrics
|
||
}
|
||
}
|
||
onLoad((options : OnLoadOptions) => {
|
||
const idParam = options['id']
|
||
if (typeof idParam === 'string' && idParam != null) {
|
||
projectId.value = idParam as string
|
||
} else {
|
||
projectId.value = ''
|
||
}
|
||
screenWidth.value = uni.getSystemInfoSync().windowWidth
|
||
if (projectId.value.length > 0) {
|
||
loadProjectData()
|
||
}
|
||
})
|
||
|
||
onMounted(() => {
|
||
screenWidth.value = uni.getSystemInfoSync().windowWidth
|
||
})
|
||
|
||
onResize((size) => {
|
||
screenWidth.value = size.size.windowWidth
|
||
})
|
||
</script>
|
||
|
||
<style>
|
||
.project-edit {
|
||
background-color: #f5f5f5;
|
||
min-height: 100vh;
|
||
padding: 20rpx;
|
||
}
|
||
|
||
/* Loading */
|
||
.loading-container {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 400rpx;
|
||
}
|
||
|
||
.loading-text {
|
||
font-size: 32rpx;
|
||
color: #666;
|
||
}
|
||
|
||
/* 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);
|
||
max-height: calc(100vh - 120rpx);
|
||
}
|
||
|
||
.small-screen .form-container {
|
||
padding: 20rpx;
|
||
border-radius: 15rpx;
|
||
}
|
||
|
||
.form-section {
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.form-section:nth-last-child(2) {
|
||
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;
|
||
}
|
||
|
||
/* Options */
|
||
.difficulty-options,
|
||
.status-options {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
margin-right: -15rpx;
|
||
margin-bottom: -15rpx;
|
||
}
|
||
|
||
.difficulty-btn,
|
||
.status-btn {
|
||
width: calc(50% - 15rpx);
|
||
margin-right: 15rpx;
|
||
margin-bottom: 15rpx;
|
||
height: 100rpx;
|
||
border: 2rpx solid #e0e0e0;
|
||
border-radius: 12rpx;
|
||
background-color: #fff;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.difficulty-btn:nth-child(2n) {
|
||
margin-right: 0;
|
||
}
|
||
|
||
.status-btn:nth-child(2n) {
|
||
margin-right: 0;
|
||
}
|
||
|
||
.difficulty-btn.active,
|
||
.status-btn.active {
|
||
border-color: #667eea;
|
||
background-color: #f8f9ff;
|
||
}
|
||
|
||
.difficulty-icon {
|
||
font-size: 32rpx;
|
||
}
|
||
|
||
.difficulty-text,
|
||
.status-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-right: -15rpx;
|
||
margin-bottom: 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;
|
||
margin-right: 15rpx;
|
||
}
|
||
|
||
.score-range-group {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-right: 15rpx;
|
||
min-width: 200rpx;
|
||
}
|
||
|
||
.score-input {
|
||
width: 80rpx;
|
||
height: 70rpx;
|
||
border: 2rpx solid #e0e0e0;
|
||
border-radius: 8rpx;
|
||
padding: 0 10rpx;
|
||
font-size: 26rpx;
|
||
text-align: center;
|
||
margin-right: 10rpx;
|
||
}
|
||
|
||
.score-separator {
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
margin-right: 10rpx;
|
||
}
|
||
|
||
.metric-unit {
|
||
width: 120rpx;
|
||
height: 70rpx;
|
||
border: 2rpx solid #e0e0e0;
|
||
border-radius: 8rpx;
|
||
padding: 0 15rpx;
|
||
font-size: 26rpx;
|
||
text-align: center;
|
||
margin-right: 15rpx;
|
||
}
|
||
|
||
.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;
|
||
margin-right: -10rpx;
|
||
}
|
||
|
||
.add-icon {
|
||
font-size: 28rpx;
|
||
color: #667eea;
|
||
font-weight: bold;
|
||
margin-right: 10rpx;
|
||
}
|
||
|
||
.add-text {
|
||
font-size: 26rpx;
|
||
color: #667eea;
|
||
font-weight: 400;
|
||
}
|
||
|
||
/* Action Buttons */
|
||
.action-buttons {
|
||
display: flex;
|
||
margin-right: -15rpx;
|
||
margin-top: 40rpx;
|
||
}
|
||
|
||
.action-btn {
|
||
flex: 1;
|
||
height: 88rpx;
|
||
border-radius: 44rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 28rpx;
|
||
font-weight: bold;
|
||
border: none;
|
||
margin-right: 15rpx;
|
||
}
|
||
|
||
.primary-btn {
|
||
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
|
||
color: white;
|
||
}
|
||
|
||
.secondary-btn {
|
||
background-color: #f5f5f5;
|
||
color: #666;
|
||
border: 2rpx solid #e0e0e0;
|
||
}
|
||
|
||
.danger-btn {
|
||
background-color: #ff4757;
|
||
color: white;
|
||
}
|
||
|
||
/* 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-wrap: wrap;
|
||
margin-right: -15rpx;
|
||
margin-bottom: -15rpx;
|
||
}
|
||
|
||
.category-option {
|
||
width: calc(50% - 15rpx);
|
||
height: 120rpx;
|
||
border: 2rpx solid #e0e0e0;
|
||
border-radius: 12rpx;
|
||
background-color: white;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 15rpx;
|
||
margin-bottom: 15rpx;
|
||
}
|
||
|
||
.category-option:nth-child(2n) {
|
||
margin-right: 0;
|
||
}
|
||
|
||
.category-option:active {
|
||
background-color: #f8f9ff;
|
||
border-color: #667eea;
|
||
}
|
||
|
||
.category-icon {
|
||
font-size: 32rpx;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.category-name {
|
||
font-size: 24rpx;
|
||
color: #333;
|
||
font-weight: 400;
|
||
}
|
||
|
||
/* Media Upload Styles */
|
||
.media-upload-container {
|
||
border: 2rpx dashed #e0e0e0;
|
||
border-radius: 12rpx;
|
||
padding: 20rpx;
|
||
text-align: center;
|
||
background-color: #fafafa;
|
||
position: relative;
|
||
}
|
||
|
||
.upload-btn {
|
||
width: 100%;
|
||
height: 160rpx;
|
||
background-color: transparent;
|
||
border: none;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.upload-icon {
|
||
font-size: 48rpx;
|
||
margin-bottom: 10rpx;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.upload-text {
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.image-preview,
|
||
.video-preview {
|
||
position: relative;
|
||
border-radius: 8rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.preview-image {
|
||
width: 100%;
|
||
height: 200rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.preview-video {
|
||
width: 100%;
|
||
height: 200rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.remove-media-btn {
|
||
position: absolute;
|
||
top: 10rpx;
|
||
right: 10rpx;
|
||
width: 50rpx;
|
||
height: 50rpx;
|
||
border-radius: 25rpx;
|
||
background-color: rgba(255, 71, 87, 0.9);
|
||
border: none;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.remove-media-icon {
|
||
font-size: 28rpx;
|
||
color: white;
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* Responsive Layout */
|
||
@media screen and (max-width: 400px) {
|
||
|
||
.difficulty-options,
|
||
.status-options,
|
||
.category-list {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.difficulty-btn,
|
||
.status-btn,
|
||
.category-option {
|
||
width: 100%;
|
||
margin-right: 0;
|
||
}
|
||
|
||
.action-buttons {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.action-btn {
|
||
margin-right: 0;
|
||
margin-bottom: 15rpx;
|
||
}
|
||
|
||
.action-btn:last-of-type {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.requirement-item,
|
||
.scoring-item,
|
||
.metric-item {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.score-range-group {
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
.requirement-input,
|
||
.criteria-input,
|
||
.metric-name,
|
||
.metric-unit {
|
||
margin-right: 0;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
.remove-btn {
|
||
align-self: center;
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.small-screen {
|
||
padding: 10rpx;
|
||
}
|
||
|
||
.small-screen .difficulty-options,
|
||
.small-screen .status-options,
|
||
.small-screen .category-list {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.small-screen .difficulty-btn,
|
||
.small-screen .status-btn,
|
||
.small-screen .category-option {
|
||
width: 100%;
|
||
margin-right: 0;
|
||
}
|
||
|
||
.small-screen .action-buttons {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.small-screen .action-btn {
|
||
margin-right: 0;
|
||
margin-bottom: 15rpx;
|
||
}
|
||
|
||
/* Android Performance Optimizations */
|
||
.project-edit {
|
||
display: flex;
|
||
flex:1;
|
||
transform: translateZ(0);
|
||
-webkit-transform: translateZ(0);
|
||
}
|
||
|
||
.form-container {
|
||
will-change: transform;
|
||
}
|
||
|
||
.modal-overlay {
|
||
-webkit-overflow-scrolling: touch;
|
||
overflow-scrolling: touch;
|
||
}
|
||
</style> |