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,980 @@
<template>
<scroll-view direction="vertical" class="goal-settings-page">
<!-- Header -->
<view class="header">
<view class="header-left">
<button @click="goBack" class="back-btn">
<simple-icon type="arrow-left" :size="16" />
<text>返回</text>
</button>
<text class="title">训练目标</text>
</view>
<view class="header-actions">
<button @click="addGoal" class="add-btn">
<simple-icon type="plus" :size="16" />
<text>添加</text>
</button>
</view>
</view>
<!-- Loading State -->
<view v-if="loading" class="loading-container">
<text class="loading-text">加载中...</text>
</view>
<!-- Content -->
<scroll-view v-else class="content" scroll-y="true" :style="{ height: contentHeight + 'px' }">
<!-- Current Goals -->
<view class="goals-section">
<view class="section-title">当前目标</view>
<view v-if="goals.length === 0" class="empty-state">
<simple-icon type="target" :size="48" color="#BDC3C7" />
<text class="empty-text">还没有设置训练目标</text>
<text class="empty-desc">设置目标让训练更有动力</text>
<button @click="addGoal" class="add-goal-btn">设置第一个目标</button>
</view>
<view v-else class="goals-list">
<view v-for="(goal,index) in goals" :key="goal.id" class="goal-item" @click="editGoal(goal)">
<view class="goal-header">
<view class="goal-icon">
<text class="goal-emoji">{{ goal["goal_type"] }}</text>
</view>
<view class="goal-info">
<text class="goal-name">{{ goal["goal_type"] }}</text>
<text class="goal-desc">{{ goal.getString("description") }}</text>
</view>
<view class="goal-status" :class="getGoalStatusClass(goal.getString('status')??'')">
<text class="status-text">{{ getGoalStatusText(goal.getString('status')??"") }}</text>
</view>
</view>
<view class="goal-progress">
<view class="progress-info">
<text class="progress-text">
{{ goal.current_value ?? 0 }} / {{ goal.target_value }} {{ goal.unit ?? '' }}
</text>
<text class="progress-percent">{{ getProgressPercent(goal) }}%</text>
</view>
<view class="progress-bar">
<view class="progress-fill" :style="{ width: getProgressPercent(goal) + '%' }"></view>
</view>
</view>
<view class="goal-meta">
<text class="goal-date">目标日期: {{ getGoalTargetDate(goal) }}</text>
<text class="goal-priority">优先级: {{ getGoalPriorityText(goal) }}</text>
</view>
</view>
</view>
</view>
<!-- Goal Templates -->
<view class="templates-section">
<view class="section-title">目标模板</view>
<view class="templates-grid">
<view v-for="template in goalTemplates" :key="template.type"
class="template-item" @click="createFromTemplate(template)">
<view class="template-icon">
<text class="template-emoji">{{ template.icon }}</text>
</view>
<text class="template-name">{{ template.name }}</text>
<text class="template-desc">{{ template.description }}</text>
</view>
</view>
</view>
<!-- Statistics -->
<view class="stats-section">
<view class="section-title">目标统计</view>
<view class="stats-grid">
<view class="stat-item">
<text class="stat-number">{{ completedGoals }}</text>
<text class="stat-label">已完成</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ activeGoals }}</text>
<text class="stat-label">进行中</text>
</view>
<view class="stat-item">
<text class="stat-number">{{ getAverageProgress() }}%</text>
<text class="stat-label">平均进度</text>
</view>
</view>
</view>
</scroll-view>
<!-- Add/Edit Goal Modal -->
<view v-if="showGoalModal" class="modal-overlay" @click="closeGoalModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">{{ editingGoal != null ? '编辑目标' : '添加目标' }}</text>
<button @click="closeGoalModal" class="modal-close" type="button">
<simple-icon type="x" :size="20" color="#666" />
</button>
</view>
<view class="modal-body">
<view class="form-group">
<text class="form-label">目标类型</text>
<view @click="showGoalTypePicker = true" class="picker-input">
<text>{{ goalTypeOptions[goalTypeIndex] ?? '请选择目标类型' }}</text>
<simple-icon type="chevron-down" :size="16" />
</view>
<view v-if="showGoalTypePicker" class="picker-view-modal">
<picker-view :value="[goalTypeIndex]" :indicator-style="'height: 40px;'" @change="onGoalTypePickerChange">
<picker-view-column>
<view v-for="(item, idx) in goalTypeOptions" :key="idx" class="picker-view-item">{{ item }}</view>
</picker-view-column>
</picker-view>
<view class="picker-view-actions">
<button @click="showGoalTypePicker = false">取消</button>
<button @click="confirmGoalTypePicker">确定</button>
</view>
</view>
</view>
<view class="form-group">
<text class="form-label">目标数值</text>
<input :value="goalForm.target_value" type="number"
class="form-input" placeholder="请输入目标数值" />
</view>
<view class="form-group">
<text class="form-label">单位</text>
<input :value="goalForm.unit" type="text"
class="form-input" placeholder="如: kg, 次, 分钟" />
</view>
<view class="form-group">
<text class="form-label">目标日期</text>
<input :value="goalForm.target_date" type="date" class="form-input" placeholder="请选择目标日期" />
</view>
<view class="form-group">
<text class="form-label">优先级</text>
<view @click="showPriorityPicker = true" class="picker-input">
<text>{{ priorityOptions[priorityIndex] ?? '请选择优先级' }}</text>
<simple-icon type="chevron-down" :size="16" color="#999" />
</view>
<view v-if="showPriorityPicker" class="picker-view-modal">
<picker-view :value="[priorityIndex]" :indicator-style="'height: 40px;'" @change="onPriorityPickerChange">
<picker-view-column>
<view v-for="(item, idx) in priorityOptions" :key="idx" class="picker-view-item">{{ item }}</view>
</picker-view-column>
</picker-view>
<view class="picker-view-actions">
<button @click="showPriorityPicker = false">取消</button>
<button @click="confirmPriorityPicker">确定</button>
</view>
</view>
</view>
<view class="form-group">
<text class="form-label">描述</text>
<textarea :value="goalForm.description" class="form-textarea"
placeholder="描述你的目标..." maxlength="200"></textarea>
</view>
</view>
<view class="modal-footer">
<button @click="closeGoalModal" class="cancel-btn">取消</button>
<button @click="saveGoal" class="save-btn" :disabled="!isFormValid">保存</button>
</view>
</view>
</view>
</scroll-view>
</template>
<script setup lang="uts">
import { ref, onMounted, computed } from 'vue'
import { onLoad,onResize } from '@dcloudio/uni-app'
import { formatDate } from '../types.uts'
import { getCurrentUserId } from '@/utils/store.uts'
import supaClient from '@/components/supadb/aksupainstance.uts'
const userId = ref('')
// Responsive state - using onResize for dynamic updates
const screenWidth = ref<number>(uni.getSystemInfoSync().windowWidth)
// Computed properties for responsive design
const isLargeScreen = computed(() : boolean => {
return screenWidth.value >= 768
})
// 响应式数据
const loading = ref(true)
const goals = ref<Array<UTSJSONObject>>([])
const showGoalModal = ref(false)
const editingGoal = ref<UTSJSONObject | null>(null)
const contentHeight = ref(0)
// 表单数据
const goalForm = ref<UTSJSONObject>({
goal_type: '',
target_value: '',
unit: '',
target_date: '',
priority: 1,
description: ''
})
// 选择器数据
const goalTypeIndex = ref(0)
const priorityIndex = ref(0)
// Add missing picker-view state refs for Android compatibility
const tempGoalTypeIndex = ref(0)
const tempPriorityIndex = ref(0)
const showGoalTypePicker = ref(false)
const showPriorityPicker = ref(false)
const goalTypeOptions = ['减肥', '增肌', '耐力提升', '柔韧性', '力量增强', '技能提升']
const goalTypes = ['weight_loss', 'muscle_gain', 'endurance', 'flexibility', 'strength', 'skill']
const priorityOptions = ['低', '一般', '中等', '较高', '最高']
// 目标模板
const goalTemplates = ref<Array<UTSJSONObject>>([
{
type: 'weight_loss',
name: '减重目标',
description: '设定理想体重目标',
icon: '⚖️',
defaultValue: 5,
unit: 'kg'
},
{
type: 'muscle_gain',
name: '增肌目标',
description: '增加肌肉量',
icon: '',
defaultValue: 3,
unit: 'kg'
},
{
type: 'endurance',
name: '耐力提升',
description: '提高有氧耐力',
icon: '',
defaultValue: 30,
unit: '分钟'
},
{
type: 'strength',
name: '力量增强',
description: '提升最大力量',
icon: '',
defaultValue: 20,
unit: 'kg'
}
])
// 计算属性
const completedGoals = computed(() => {
return goals.value.filter(goal => goal.get('status') === 'completed').length
})
const activeGoals = computed(() => {
return goals.value.filter(goal => goal.get('status') === 'active').length
})
const isFormValid = computed(() => {
return (goalForm.value['goal_type'] as string) !== '' &&
(goalForm.value['target_value'] as string) !== '' &&
(goalForm.value['unit'] as string) !== '' &&
(goalForm.value['target_date'] as string) !== ''
})
// 计算内容高度
const calculateContentHeight = () => {
const systemInfo = uni.getSystemInfoSync()
const windowHeight = systemInfo.windowHeight
const headerHeight = 60
contentHeight.value = windowHeight - headerHeight
}
// 加载目标数据
const loadGoals = async () => {
try {
loading.value = true
if ((userId.value as string) === '') {
uni.showToast({
title: '请先登录',
icon: 'none'
})
return
}
const result = await supaClient
.from('ak_user_training_goals')
.select('*', {})
.eq('user_id', userId.value)
.order('created_at', { ascending: false })
.execute()
if (result.error == null && result.data != null) {
goals.value = result.data as UTSJSONObject[]
}
} catch (error) {
console.error('加载目标失败:', error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
loading.value = false
}
}
// 生命周期
onMounted(() => {
calculateContentHeight()
loadGoals()
})
onLoad((options: OnLoadOptions) => {
userId.value = options['id'] ?? getCurrentUserId()
loadGoals()
})
// 返回上一页
const goBack = () => {
uni.navigateBack()
}
// 重置表单
function resetForm() {
goalForm.value = {
goal_type: '',
target_value: '',
unit: '',
target_date: '',
priority: 1,
description: ''
}
goalTypeIndex.value = 0
priorityIndex.value = 0
}
// 填充表单
function populateForm(goal: UTSJSONObject) {
goalForm.value = {
goal_type: goal.get('goal_type') as string,
target_value: goal.get('target_value') as string,
unit: goal.get('unit') as string,
target_date: goal.get('target_date') as string,
priority: goal.get('priority') != null ? goal.get('priority') as number : 1,
description: goal.get('description') as string
}
const form = goalForm.value
const typeIndex = goalTypes.indexOf(form['goal_type'])
goalTypeIndex.value = typeIndex >= 0 ? typeIndex : 0
const priorityNum = parseInt(form['priority'] != null ? form['priority'].toString() : '1')
priorityIndex.value = isNaN(priorityNum) ? 0 : priorityNum - 1
}
// 添加目标
const addGoal = () => {
editingGoal.value = null
resetForm()
showGoalModal.value = true
}
// 编辑目标
const editGoal = (goal: UTSJSONObject) => {
editingGoal.value = goal
populateForm(goal)
showGoalModal.value = true
}
// 关闭模态框
const closeGoalModal = () => {
showGoalModal.value = false
editingGoal.value = null
}
// 保存目标
const saveGoal = async () => {
try {
const goalData = {
user_id: userId.value,
goal_type: goalTypes[goalTypeIndex.value],
target_value: parseFloat(goalForm.value['target_value'] != null ? goalForm.value['target_value'].toString() : '0'),
unit: goalForm.value['unit'],
target_date: goalForm.value['target_date'],
priority: priorityIndex.value + 1,
description: goalForm.value['description'],
status: 'active'
}
let result: any
const currentEditingGoal = editingGoal.value
if (currentEditingGoal != null) {
// 更新
result = await supaClient
.from('ak_user_training_goals')
.update(goalData)
.eq('id', (currentEditingGoal.get('id') ?? '').toString())
.execute()
} else {
// 创建
result = await supaClient
.from('ak_user_training_goals')
.insert(goalData)
.execute()
}
if (result.error != null) {
throw new Error(result.error?.message ?? '未知错误')
}
uni.showToast({
title: editingGoal.value != null ? '更新成功' : '创建成功',
icon: 'success'
})
closeGoalModal()
loadGoals()
} catch (error) {
console.error('保存目标失败:', error)
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
}
// 从模板创建目标
const createFromTemplate = (template: UTSJSONObject) => {
editingGoal.value = null
resetForm()
// Use bracket notation for UTS compatibility
goalForm.value['goal_type'] = template.get('type') as string
goalForm.value['target_value'] = template.get('defaultValue') as string
goalForm.value['unit'] = template.get('unit') as string
const typeIndex = goalTypes.indexOf(goalForm.value['goal_type'])
goalTypeIndex.value = typeIndex >= 0 ? typeIndex : 0
// 设置默认目标日期为3个月后
const targetDate = new Date()
targetDate.setMonth(targetDate.getMonth() + 3)
goalForm.value['target_date'] = targetDate.toISOString().split('T')[0]
showGoalModal.value = true
}
// 事件处理
const onGoalTypePickerChange = (e: UniPickerViewChangeEvent) => {
tempGoalTypeIndex.value = e.detail.value[0]
}
const confirmGoalTypePicker = () => {
goalTypeIndex.value = tempGoalTypeIndex.value
goalForm.value['goal_type'] = goalTypes[goalTypeIndex.value]
showGoalTypePicker.value = false
}
const onPriorityPickerChange = (e: UniPickerViewChangeEvent) => {
tempPriorityIndex.value = e.detail.value[0]
}
const confirmPriorityPicker = () => {
priorityIndex.value = tempPriorityIndex.value
goalForm.value['priority'] = priorityIndex.value + 1
showPriorityPicker.value = false
}
// 工具函数
const getGoalIcon = (goalType: string): string => {
const icons = {
'weight_loss': '⚖️',
'muscle_gain': '',
'endurance': '',
'flexibility': '',
'strength': '',
'skill': ''
}
return icons[goalType] ?? ''
}
const getGoalTypeName = (goalType: string): string => {
const names = {
'weight_loss': '减肥目标',
'muscle_gain': '增肌目标',
'endurance': '耐力提升',
'flexibility': '柔韧性',
'strength': '力量增强',
'skill': '技能提升'
}
return names[goalType] ?? '未知目标'
}
const getGoalStatusText = (status: string): string => {
const statusTexts = {
'active': '进行中',
'paused': '已暂停',
'completed': '已完成',
'cancelled': '已取消'
}
const result =statusTexts[status]
return result!=null ? result.toString() :'未知'
}
const getGoalStatusClass = (status: string): string => {
return `status-${status}`
}
const getPriorityText = (priority: number): string => {
const priorities = ['低', '一般', '中等', '较高', '最高']
return priorities[priority - 1] ?? '一般'
}
const getProgressPercent = (goal: UTSJSONObject): number => {
const current = goal.getNumber('current_value') ?? 0
const target = goal.getNumber('target_value') ?? 1
return Math.min(Math.round((current / target) * 100), 100)
}
const getAverageProgress = (): number => {
if (goals.value.length === 0) return 0
const totalProgress = goals.value.reduce((sum, goal) => sum + getProgressPercent(goal), 0)
return Math.round(totalProgress / goals.value.length)
}
// Add a helper to safely get description as string
function getGoalDescription(desc: string): string {
if (desc == null) return '暂无描述'
if (typeof desc === 'string') {
const trimmed = desc.trim()
return trimmed !== '' ? trimmed : '暂无描述'
}
return '暂无描述'
}
// Helper to safely get priority text from goal object
function getGoalPriorityText(goal: UTSJSONObject): string {
const raw = goal.get('priority')
let num = 1
if (typeof raw === 'number') {
num = raw
} else if (typeof raw === 'string') {
const parsed = parseInt(raw)
num = isNaN(parsed) ? 1 : parsed
}
return getPriorityText(num)
}
// Helper to safely get formatted date from goal object
function getGoalTargetDate(goal: UTSJSONObject): string {
const raw = goal.get('target_date')
const dateStr = (raw != null) ? raw.toString() : ''
// UTS: formatDate is always a function, call directly
return formatDate(dateStr, 'YYYY-MM-DD')
}
// Lifecycle hooks
onMounted(() => {
screenWidth.value = uni.getSystemInfoSync().windowWidth
userId.value = getCurrentUserId()
loadGoals()
})
onResize((size) => {
screenWidth.value = size.size.windowWidth
})
</script>
<style scoped>
.goal-settings-page {
flex: 1;
background-color: #f5f5f5;
}
.header { height: 60px;
background-image: linear-gradient(to top right, #4CAF50, #45a049);
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 0 16px;
}
.header-left {
flex-direction: row;
align-items: center;
flex: 1;
}
.back-btn, .add-btn {
background-color: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 20px;
padding: 8px 12px;
flex-direction: row;
align-items: center;
gap: 4px;
}
.back-btn {
margin-right: 12px;
}
.back-btn text, .add-btn text {
color: #FFFFFF;
font-size: 14px;
}
.title {
font-size: 18px;
font-weight: bold;
color: #FFFFFF;
}
.content {
flex: 1;
padding: 16px;
}
.section-title {
font-size: 18px;
font-weight: bold;
color: #333;
margin-bottom: 12px;
}
.goals-section {
margin-bottom: 24px;
}
.empty-state {
background-color: #FFFFFF;
border-radius: 12px;
padding: 40px 20px;
align-items: center;
text-align: center;
}
.empty-text {
font-size: 16px;
color: #666;
margin: 12px 0 4px;
}
.empty-desc {
font-size: 14px;
color: #999;
margin-bottom: 20px;
}
.add-goal-btn {
background-color: #4CAF50;
color: #FFFFFF;
border: none;
border-radius: 20px;
padding: 12px 24px;
font-size: 14px;
}
.goals-list {
gap: 12px;
}
.goal-item {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
}
.goal-header {
flex-direction: row;
align-items: center;
margin-bottom: 12px;
}
.goal-icon {
width: 40px;
height: 40px;
background-color: #f0f0f0;
border-radius: 20px;
justify-content: center;
align-items: center;
margin-right: 12px;
}
.goal-emoji {
font-size: 20px;
}
.goal-info {
flex: 1;
}
.goal-name {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.goal-desc {
font-size: 12px;
color: #666;
}
.goal-status {
padding: 4px 8px;
border-radius: 12px;
align-items: center;
}
.status-active {
background-color: #E8F5E8;
}
.status-completed {
background-color: #E3F2FD;
}
.status-paused {
background-color: #FFF3E0;
}
.status-cancelled {
background-color: #FFEBEE;
}
.status-text {
font-size: 12px;
color: #333;
}
.goal-progress {
margin-bottom: 12px;
}
.progress-info {
flex-direction: row;
justify-content: space-between;
margin-bottom: 8px;
}
.progress-text {
font-size: 14px;
color: #666;
}
.progress-percent {
font-size: 14px;
font-weight: bold;
color: #4CAF50;
}
.progress-bar {
height: 8px;
background-color: #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: #4CAF50;
border-radius: 4px;
}
.goal-meta {
flex-direction: row;
justify-content: space-between;
}
.goal-date, .goal-priority {
font-size: 12px;
color: #999;
}
.templates-section {
margin-bottom: 24px;
}
.templates-grid {
flex-direction: row;
flex-wrap: wrap;
gap: 12px;
}
.template-item {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
width: calc(50% - 6px);
align-items: center;
text-align: center;
}
.template-icon {
width: 40px;
height: 40px;
justify-content: center;
align-items: center;
margin-bottom: 8px;
}
.template-emoji {
font-size: 24px;
}
.template-name {
font-size: 14px;
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.template-desc {
font-size: 12px;
color: #666;
}
.stats-section {
margin-bottom: 24px;
}
.stats-grid {
flex-direction: row;
gap: 12px;
}
.stat-item {
background-color: #FFFFFF;
border-radius: 12px;
padding: 16px;
flex: 1;
align-items: center;
text-align: center;
}
.stat-number {
font-size: 24px;
font-weight: bold;
color: #4CAF50;
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: #666;
}
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: #FFFFFF;
border-radius: 12px;
width: 80%;
max-height: 80%;
}
.modal-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #f0f0f0;
}
.modal-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.modal-close {
background: transparent;
border: none;
padding: 4px;
}
.modal-body {
padding: 16px;
max-height: 400px;
overflow-y: auto;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
font-size: 14px;
color: #333;
margin-bottom: 8px;
display: block;
}
.form-input, .form-textarea {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
}
.form-textarea {
height: 80px;
text-align: start;
}
.picker-input {
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
background-color: #FFFFFF;
}
.modal-footer {
flex-direction: row;
justify-content: flex-end;
gap: 12px;
padding: 16px;
border-top: 1px solid #f0f0f0;
}
.cancel-btn, .save-btn {
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
border: none;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666;
}
.save-btn:disabled {
background-color: #cccccc;
color: #999999;
}
.loading-container {
flex: 1;
justify-content: center;
align-items: center;
}
.loading-text {
font-size: 16px;
color: #666;
}
</style>