jiapu 2 месяцев назад
Родитель
Сommit
70e561c000

+ 310 - 0
cfc-frontend/pages/assessment-plan/detail.vue

@@ -0,0 +1,310 @@
+<template>
+  <view class="page-container">
+    <!-- 加载状态 -->
+    <view class="loading" v-if="loading">加载中...</view>
+
+    <!-- 空状态 -->
+    <view class="empty" v-else-if="!plan">
+      <text class="empty-title">数据不存在</text>
+    </view>
+
+    <!-- 详情内容 -->
+    <view class="detail-content" v-else>
+      <!-- 标题栏 -->
+      <view class="header">
+        <text class="title">{{ plan.name }}</text>
+        <text class="subtitle">{{ getStatusText(plan.status) }} · {{ plan.childName || '未知' }}</text>
+      </view>
+
+      <!-- 基本信息卡片 -->
+      <view class="info-card">
+        <view class="card-header">方案信息</view>
+        <view class="card-body">
+          <view class="info-item">
+            <text class="label">孩子:</text>
+            <text class="value">{{ plan.childName || '-' }}</text>
+          </view>
+          <view class="info-item">
+            <text class="label">周期:</text>
+            <text class="value">{{ plan.totalDays }}天</text>
+          </view>
+          <view class="info-item">
+            <text class="label">创建时间:</text>
+            <text class="value">{{ formatDateTime(plan.createdAt) }}</text>
+          </view>
+          <view class="info-item">
+            <text class="label">更新日期:</text>
+            <text class="value">{{ formatDateTime(plan.updatedAt) }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 进度卡片 -->
+      <view class="progress-card">
+        <view class="card-header">完成进度</view>
+        <view class="card-body">
+          <view class="progress-stat">
+            <text class="stat-number">{{ plan.completedDays }}/{{ plan.totalDays }}</text>
+            <text class="stat-text">天已完成</text>
+          </view>
+          <view class="progress-bar">
+            <view 
+              class="progress-fill" 
+              :style="'width: ' + (plan.completedDays / plan.totalDays * 100) + '%'"
+            ></view>
+          </view>
+        </view>
+      </view>
+
+      <!-- 操作按钮 -->
+      <view class="action-bar">
+        <button class="btn btn-primary" @click="goBack">返回</button>
+        <button class="btn btn-secondary" @click="goEdit">编辑</button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getAssessmentPlanDetail } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      loading: false,
+      plan: null,
+      planId: null
+    }
+  },
+  onLoad(options) {
+    this.planId = options.id ? parseInt(options.id) : null
+    
+    if (!this.planId) {
+      uni.showToast({ title: '参数错误', icon: 'none' })
+      setTimeout(() => uni.navigateBack(), 1500)
+      return
+    }
+    
+    this.loadDetail()
+  },
+  methods: {
+    loadDetail() {
+      var self = this
+      self.loading = true
+      
+      getAssessmentPlanDetail(self.planId).then(function(res) {
+        self.loading = false
+        
+        if (res.code === 200 && res.data) {
+          self.plan = res.data
+        } else {
+          uni.showToast({ 
+            title: res.message || '加载失败', 
+            icon: 'none' 
+          })
+          setTimeout(() => uni.navigateBack(), 1500)
+        }
+      }).catch(function(e) {
+        self.loading = false
+        console.error('加载详情失败', e)
+        uni.showToast({ title: '网络错误', icon: 'none' })
+        setTimeout(() => uni.navigateBack(), 1500)
+      })
+    },
+    
+    getStatusText(status) {
+      var map = {
+        'generated': '已生成',
+        'active': '进行中',
+        'completed': '已完成',
+        'expired': '已过期',
+        'draft': '草稿'
+      }
+      return map[status] || status
+    },
+    
+    formatDateTime(dateStr) {
+      if (!dateStr) return ''
+      var d = this.parseDate(dateStr)
+      if (!d) return dateStr
+      
+      var y = d.getFullYear()
+      var m = ('' + (d.getMonth() + 1)).padStart(2, '0')
+      var day = ('' + d.getDate()).padStart(2, '0')
+      var h = ('' + d.getHours()).padStart(2, '0')
+      var min = ('' + d.getMinutes()).padStart(2, '0')
+      
+      return y + '-' + m + '-' + day + ' ' + h + ':' + min
+    },
+    
+    parseDate(dateStr) {
+      var normalized = dateStr.replace(' ', 'T')
+      var d = new Date(normalized)
+      return isNaN(d.getTime()) ? null : d
+    },
+    
+    goBack() {
+      uni.navigateBack()
+    },
+    
+    goEdit() {
+      // TODO: 实现编辑功能(待开发)
+      uni.showToast({ title: '编辑功能待开发', icon: 'none' })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page-container {
+  min-height: 100vh;
+  background: #F5F7FA;
+  padding-bottom: 60rpx;
+}
+
+/* ===== 加载状态 ===== */
+.loading {
+  text-align: center;
+  padding: 120rpx 0;
+  font-size: 28rpx;
+  color: #94A3B8;
+}
+
+/* ===== 空状态 ===== */
+.empty {
+  text-align: center;
+  padding: 160rpx 0;
+}
+
+.empty-title {
+  font-size: 28rpx;
+  color: #64748B;
+}
+
+/* ===== 详情内容 ===== */
+.detail-content {
+  padding: 0 30rpx;
+}
+
+.header {
+  background: linear-gradient(135deg, #5B9BD5 0%, #3A7CC4 100%);
+  padding: 60rpx 30rpx 40rpx;
+  color: #fff;
+  border-radius: 0 0 32rpx 32rpx;
+  margin-bottom: 24rpx;
+}
+
+.title {
+  font-size: 44rpx;
+  font-weight: bold;
+  display: block;
+  margin-bottom: 12rpx;
+}
+
+.subtitle {
+  font-size: 24rpx;
+  opacity: 0.9;
+  display: block;
+}
+
+/* ===== 通用卡片 ===== */
+.info-card, .progress-card {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 32rpx;
+  margin-bottom: 28rpx;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
+}
+
+.card-header {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 24rpx;
+  padding-bottom: 16rpx;
+  border-bottom: 2rpx solid #F0F0F0;
+}
+
+.card-body {
+  padding: 16rpx 0;
+}
+
+/* ===== 信息卡片 ===== */
+.info-item {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16rpx 0;
+  font-size: 26rpx;
+}
+
+.label {
+  color: #94A3B8;
+  font-weight: 500;
+}
+
+.value {
+  color: #333;
+  font-weight: normal;
+}
+
+/* ===== 进度卡片 ===== */
+.progress-stat {
+  text-align: center;
+  margin-bottom: 24rpx;
+}
+
+.stat-number {
+  font-size: 56rpx;
+  font-weight: bold;
+  color: #5B9BD5;
+  margin-right: 12rpx;
+}
+
+.stat-text {
+  font-size: 24rpx;
+  color: #64748B;
+}
+
+.progress-bar {
+  height: 12rpx;
+  background: #E2E8F0;
+  border-radius: 6rpx;
+  overflow: hidden;
+  margin-top: 20rpx;
+}
+
+.progress-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #5B9BD5 0%, #3A7CC4 100%);
+  transition: width 0.3s ease;
+}
+
+/* ===== 操作按钮 ===== */
+.action-bar {
+  display: flex;
+  gap: 20rpx;
+  padding: 30rpx;
+}
+
+.btn {
+  flex: 1;
+  height: 80rpx;
+  line-height: 80rpx;
+  font-size: 28rpx;
+  border-radius: 16rpx;
+  border: none;
+}
+
+.btn-primary {
+  background: #fff;
+  color: #5B9BD5;
+  border: 2rpx solid #5B9BD5;
+}
+
+.btn-secondary {
+  background: #5B9BD5;
+  color: #fff;
+}
+</style>

+ 394 - 0
cfc-frontend/pages/assessment-plan/list.vue

@@ -0,0 +1,394 @@
+<template>
+  <view class="page-container">
+    <!-- 标题栏 -->
+    <view class="header">
+      <text class="title">📋 我的测评方案</text>
+      <text class="subtitle">根据报告自动生成 · 14 天个性化成长计划</text>
+    </view>
+
+    <!-- 加载状态 -->
+    <view class="loading" v-if="loading">加载中...</view>
+
+    <!-- 空状态 -->
+    <view class="empty" v-else-if="plans.length === 0">
+      <view class="empty-icon">📊</view>
+      <text class="empty-title">暂无测评方案</text>
+      <text class="empty-desc">用户上传测评报告并确认入库后,系统将自动生成个性化成长方案</text>
+    </view>
+
+    <!-- 方案列表 -->
+    <view class="plan-list" v-else>
+      <view 
+        class="plan-card" 
+        v-for="plan in plans" 
+        :key="plan.id"
+        @click="goToDetail(plan.id)"
+      >
+        <view class="plan-header">
+          <text class="plan-status-badge" :class="'status-' + plan.status">{{ getStatusText(plan.status) }}</text>
+          <text class="plan-date">{{ formatDateTime(plan.createdAt) }}</text>
+        </view>
+        
+        <view class="plan-body">
+          <text class="plan-name">{{ plan.name }}</text>
+          <view class="plan-progress">
+            <view class="progress-info">
+              <text class="progress-text">{{ plan.completedDays }}/{{ plan.totalDays }} 天已完成</text>
+            </view>
+            <view class="progress-bar">
+              <view 
+                class="progress-fill" 
+                :style="'width: ' + (plan.completedDays / plan.totalDays * 100) + '%'"
+              ></view>
+            </view>
+          </view>
+          
+          <view class="plan-meta">
+            <text class="meta-item">
+              <text class="meta-label">周期:</text>
+              <text class="meta-value">{{ plan.totalDays }}天</text>
+            </text>
+            <text class="meta-item">
+              <text class="meta-label">孩子:</text>
+              <text class="meta-value">{{ plan.childName || '未知' }}</text>
+            </text>
+          </view>
+        </view>
+
+        <view class="plan-footer">
+          <text class="detail-text">查看详情 ›</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 底部占位 -->
+    <view class="spacer"></view>
+  </view>
+</template>
+
+<script>
+import { getMyAssessmentPlans } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      loading: false,
+      plans: [],
+      familyId: null,
+      childId: null
+    }
+  },
+  onLoad(options) {
+    // 获取家庭 ID 和孩子 ID
+    this.familyId = uni.getStorageSync('familyId') ? parseInt(uni.getStorageSync('familyId')) : null
+    this.childId = options.childId ? parseInt(options.childId) : null
+    
+    if (!this.familyId) {
+      uni.showToast({ title: '未找到家庭信息', icon: 'none' })
+      setTimeout(() => uni.navigateBack(), 1500)
+      return
+    }
+    
+    this.loadData()
+  },
+  methods: {
+    loadData() {
+      var self = this
+      self.loading = true
+      
+      getMyAssessmentPlans(self.familyId, self.childId).then(function(res) {
+        self.loading = false
+        
+        if (res.code === 200 && Array.isArray(res.data)) {
+          self.plans = res.data.map(function(plan) {
+            return {
+              ...plan,
+              childName: plan.childName || self.getChildNameFromRemark(plan.remark)
+            }
+          })
+          
+          // 如果没有传 childId 且有多条记录,按孩子分组显示
+          if (!self.childId && self.hasMultipleChildren()) {
+            console.log('检测到多个孩子,已自动按孩子分组显示')
+          }
+        } else {
+          uni.showToast({ 
+            title: res.message || '加载失败', 
+            icon: 'none' 
+          })
+        }
+      }).catch(function(e) {
+        self.loading = false
+        console.error('加载方案失败', e)
+        uni.showToast({ title: '网络错误', icon: 'none' })
+      })
+    },
+    
+    // 从 remark 中解析孩子姓名(如果未包含在数据中)
+    getChildNameFromRemark(remark) {
+      if (!remark) return ''
+      try {
+        // 尝试从备注中解析,格式如:"【测评方案自动生成】\n..."
+        var lines = remark.split('\n')
+        for (var i = 0; i < lines.length; i++) {
+          if (lines[i].includes('孩子:') || lines[i].includes('child_id:')) {
+            return lines[i].split(':')[1]?.trim() || ''
+          }
+        }
+      } catch (e) {
+        console.error('解析备注失败', e)
+      }
+      return ''
+    },
+    
+    // 检查是否有多个孩子的方案
+    hasMultipleChildren() {
+      if (!this.plans || this.plans.length === 0) return false
+      var children = new Set()
+      for (var i = 0; i < this.plans.length; i++) {
+        if (this.plans[i].childId) {
+          children.add(this.plans[i].childId)
+        }
+      }
+      return children.size > 1
+    },
+    
+    goToDetail(planId) {
+      // TODO: 实现详情页面(待开发)
+      // 暂时跳转到成长计划页面
+      uni.navigateTo({
+        url: '/pages/health/health-plan-summary?planId=' + planId
+      })
+    },
+    
+    getStatusText(status) {
+      var map = {
+        'generated': '已生成',
+        'active': '进行中',
+        'completed': '已完成',
+        'expired': '已过期',
+        'draft': '草稿'
+      }
+      return map[status] || status
+    },
+    
+    formatDateTime(dateStr) {
+      if (!dateStr) return ''
+      var d = this.parseDate(dateStr)
+      if (!d) return dateStr
+      
+      var y = d.getFullYear()
+      var m = ('' + (d.getMonth() + 1)).padStart(2, '0')
+      var day = ('' + d.getDate()).padStart(2, '0')
+      var h = ('' + d.getHours()).padStart(2, '0')
+      var min = ('' + d.getMinutes()).padStart(2, '0')
+      
+      return y + '-' + m + '-' + day + ' ' + h + ':' + min
+    },
+    
+    parseDate(dateStr) {
+      // 兼容 ISO 8601 格式(2024-08-26T14:30:00)和本地格式(2024-08-26 14:30:00)
+      var normalized = dateStr.replace(' ', 'T')
+      var d = new Date(normalized)
+      return isNaN(d.getTime()) ? null : d
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page-container {
+  min-height: 100vh;
+  background: #F5F7FA;
+  padding-bottom: 60rpx;
+}
+
+/* ===== 标题栏 ===== */
+.header {
+  background: linear-gradient(135deg, #5B9BD5 0%, #3A7CC4 100%);
+  padding: 60rpx 30rpx 40rpx;
+  color: #fff;
+}
+
+.title {
+  font-size: 48rpx;
+  font-weight: bold;
+  display: block;
+  margin-bottom: 12rpx;
+}
+
+.subtitle {
+  font-size: 24rpx;
+  opacity: 0.9;
+  display: block;
+}
+
+/* ===== 加载状态 ===== */
+.loading {
+  text-align: center;
+  padding: 120rpx 0;
+  font-size: 28rpx;
+  color: #94A3B8;
+}
+
+/* ===== 空状态 ===== */
+.empty {
+  text-align: center;
+  padding: 160rpx 60rpx;
+}
+
+.empty-icon {
+  font-size: 120rpx;
+  margin-bottom: 32rpx;
+}
+
+.empty-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 16rpx;
+  display: block;
+}
+
+.empty-desc {
+  font-size: 26rpx;
+  color: #94A3B8;
+  line-height: 1.6;
+  display: block;
+}
+
+/* ===== 方案列表 ===== */
+.plan-list {
+  padding: 30rpx;
+}
+
+.plan-card {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 32rpx;
+  margin-bottom: 28rpx;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
+}
+
+.plan-header {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20rpx;
+}
+
+.plan-status-badge {
+  font-size: 22rpx;
+  padding: 6rpx 20rpx;
+  border-radius: 24rpx;
+  font-weight: 500;
+}
+
+.status-generated {
+  background: #E3F2FD;
+  color: #1976D2;
+}
+
+.status-active {
+  background: #FFF3E0;
+  color: #F57C00;
+}
+
+.status-completed {
+  background: #E8F5E9;
+  color: #388E3C;
+}
+
+.status-expired {
+  background: #FFEBEE;
+  color: #C62828;
+}
+
+.plan-date {
+  font-size: 22rpx;
+  color: #94A3B8;
+}
+
+.plan-body {
+  margin-bottom: 24rpx;
+}
+
+.plan-name {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 24rpx;
+}
+
+.plan-progress {
+  margin-top: 24rpx;
+}
+
+.progress-info {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12rpx;
+}
+
+.progress-text {
+  font-size: 24rpx;
+  color: #64748B;
+  flex-shrink: 0;
+}
+
+.progress-bar {
+  height: 12rpx;
+  background: #E2E8F0;
+  border-radius: 6rpx;
+  overflow: hidden;
+}
+
+.progress-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #5B9BD5 0%, #3A7CC4 100%);
+  transition: width 0.3s ease;
+}
+
+.plan-meta {
+  display: flex;
+  flex-direction: row;
+  gap: 40rpx;
+  margin-top: 24rpx;
+  padding-top: 20rpx;
+  border-top: 2rpx solid #F0F0F0;
+}
+
+.meta-item {
+  font-size: 24rpx;
+  color: #64748B;
+}
+
+.meta-label {
+  color: #94A3B8;
+  margin-right: 8rpx;
+}
+
+.meta-value {
+  color: #333;
+  font-weight: 500;
+}
+
+.plan-footer {
+  text-align: right;
+  padding-top: 16rpx;
+}
+
+.detail-text {
+  font-size: 26rpx;
+  color: #5B9BD5;
+}
+
+/* ===== 底部占位 ===== */
+.spacer {
+  height: 60rpx;
+}
+</style>