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

1448 lines
37 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-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>