981 lines
23 KiB
Plaintext
981 lines
23 KiB
Plaintext
<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>
|