Просмотр исходного кода

feat: refactor growth record frontend pages with source types, upload, and supplement

Refactor create.vue with source-type selector (manual/upload/assessment). Enhance detail.vue with source badge, dimension score grid, supplement records list, and supplement button. Add source-type filter tabs to index.vue (全部/手动/测评/上传/补充). Create upload.vue with 2-step upload+parse+confirm flow using uni.chooseMessageFile for PDF. Create supplement.vue for adding supplementary reports to existing records.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
liaoxg 2 месяцев назад
Родитель
Сommit
d677d66f49

+ 120 - 115
cfc-frontend/pages/growth/create.vue

@@ -2,10 +2,28 @@
   <view class="container">
     <view class="header">
       <text class="title">创建成长档案</text>
-      <text class="subtitle">填写孩子基本信息</text>
+      <text class="subtitle">选择创建方式</text>
     </view>
 
-    <view class="form">
+    <view class="source-type-section">
+      <view class="source-card" :class="sourceType === 'manual' ? 'source-active' : ''" @click="sourceType = 'manual'">
+        <text class="source-icon">📝</text>
+        <text class="source-name">手动创建</text>
+        <text class="source-desc">填写孩子基本信息</text>
+      </view>
+      <view class="source-card" :class="sourceType === 'upload' ? 'source-active' : ''" @click="sourceType = 'upload'">
+        <text class="source-icon">📄</text>
+        <text class="source-name">上传报告</text>
+        <text class="source-desc">上传PDF测评报告</text>
+      </view>
+      <view class="source-card" :class="sourceType === 'assessment' ? 'source-active' : ''" @click="sourceType = 'assessment'">
+        <text class="source-icon">🎓</text>
+        <text class="source-name">购买测评</text>
+        <text class="source-desc">购买DAN测评服务</text>
+      </view>
+    </view>
+
+    <view class="form" v-if="sourceType === 'manual'">
       <view class="form-item">
         <text class="label">孩子</text>
         <view class="child-select" @click="selectChild">
@@ -69,39 +87,35 @@
         <input class="input" v-model="formData.schoolStreet" placeholder="选填" />
       </view>
 
-<view class="form-item checkbox-item">
-<label class="checkbox-label">
-  <input type="checkbox" :checked="alsoPurchase" @change="onAlsoPurchaseChange" />
-  <text>同时购买 DAN 测评</text>
-</label>
-<text class="checkbox-hint">创建档案后将直接进入测评购买流程</text>
-</view>
+      <button class="btn-submit" @click="submitManual">创建档案</button>
+    </view>
 
-<button class="btn-submit" @click="submit">创建档案{{ alsoPurchase ? '并购买测评' : '' }}</button>
-</view>
+    <view class="redirect-section" v-if="sourceType === 'upload'">
+      <view class="redirect-card">
+        <text class="redirect-desc">上传PDF格式的DAN测评报告,系统将自动解析报告内容并创建成长档案。</text>
+        <button class="btn-submit" @click="goUpload">上传测评报告</button>
+      </view>
+    </view>
 
-<view class="assessment-section" v-if="selectedChild && !alsoPurchase">
-<view class="section-divider"></view>
-<view class="section-header">
-<text class="section-title">关联测评</text>
-<text class="section-hint">创建完成后,可购买测评服务完善档案</text>
-</view>
-<button class="btn-assessment" @click="goToPurchase">购买测评服务</button>
-</view>
-</view>
+    <view class="redirect-section" v-if="sourceType === 'assessment'">
+      <view class="redirect-card">
+        <text class="redirect-desc">购买DAN测评服务后,规划师完成测评录入将自动创建成长档案,无需手动填写。</text>
+        <button class="btn-submit" @click="goPurchase">购买测评服务</button>
+      </view>
+    </view>
+  </view>
 </template>
 
 <script>
 import { getChildren, createGrowthRecord } from '../../utils/api.js'
-import { getPackagePrice } from '../../utils/api.js'
 
 export default {
   data() {
     return {
+      sourceType: 'manual',
       children: [],
       selectedChild: null,
       targetChildId: '',
-      alsoPurchase: false,
       gradeOptions: ['一年级','二年级','三年级','四年级','五年级','六年级','初一','初二','初三','高一','高二','高三'],
       formData: {
         childId: null,
@@ -120,6 +134,9 @@ export default {
   },
   onLoad(options) {
     this.targetChildId = options.childId || ''
+    if (options.sourceType) {
+      this.sourceType = options.sourceType
+    }
     this.loadChildren()
     if (this.targetChildId) {
       this.formData.recordTitle = '成长档案 - ' + this.getCurrentDate()
@@ -127,18 +144,17 @@ export default {
   },
   methods: {
     getCurrentDate() {
-      const d = new Date()
+      var d = new Date()
       return d.getFullYear() + '年' + (d.getMonth() + 1) + '月'
     },
     async loadChildren() {
       try {
-        const res = await getChildren()
+        var res = await getChildren()
         if (res.code === 200) {
           this.children = res.data || []
-          // 优先选中从上级页面传入的孩子
-          let target = null
+          var target = null
           if (this.targetChildId) {
-            target = this.children.find(c => String(c.id) === String(this.targetChildId))
+            target = this.children.find(function(c) { return String(c.id) === String(this.targetChildId) }.bind(this))
           }
           if (!target && this.children.length > 0) {
             target = this.children[0]
@@ -153,14 +169,15 @@ export default {
       }
     },
     selectChild() {
-      const items = this.children.map(c => c.nickname || c.name)
+      var items = this.children.map(function(c) { return c.nickname || c.name })
+      var self = this
       uni.showActionSheet({
         itemList: items,
-        success: (res) => {
-          const idx = res.tapIndex
-          if (idx >= 0 && idx < this.children.length) {
-            this.selectedChild = this.children[idx]
-            this.formData.childId = this.selectedChild.id
+        success: function(res) {
+          var idx = res.tapIndex
+          if (idx >= 0 && idx < self.children.length) {
+            self.selectedChild = self.children[idx]
+            self.formData.childId = self.selectedChild.id
           }
         }
       })
@@ -168,19 +185,16 @@ export default {
     onGradeChange(e) {
       this.formData.grade = e.detail.value + 1
     },
-    onAlsoPurchaseChange(e) {
-      this.alsoPurchase = e.detail.value
-    },
-    async submit() {
+    async submitManual() {
       if (!this.formData.childId) {
         uni.showToast({ title: '请选择孩子', icon: 'none' })
         return
       }
 
-      uni.showLoading({ title: this.alsoPurchase ? '创建并购买中...' : '创建中...' })
+      uni.showLoading({ title: '创建中...' })
       try {
-        const familyId = uni.getStorageSync('familyId')
-        const submitData = {
+        var familyId = uni.getStorageSync('familyId')
+        var submitData = {
           childId: this.formData.childId,
           recordTitle: this.formData.recordTitle || '成长档案',
           height: this.formData.height || null,
@@ -195,30 +209,15 @@ export default {
           familyId: familyId
         }
 
-        if (this.alsoPurchase) {
-          // 同时购买测评
-          submitData.alsoPurchaseAssessment = true
-          submitData.packageId = 1 // 默认套餐 ID,实际应从套餐列表选择
-          submitData.totalPrice = 29900 // 单位分,299 元
-          submitData.discountAmount = 0
-        }
-
-        const res = await createGrowthRecord(submitData)
+        var res = await createGrowthRecord(submitData)
         uni.hideLoading()
 
         if (res.code === 200) {
           uni.showToast({ title: '创建成功', icon: 'success' })
-          setTimeout(() => {
-            if (this.alsoPurchase && res.data.order) {
-              // 跳转到支付页面
-              uni.navigateTo({
-                url: '/pages/assessment/pay?orderNo=' + res.data.order.orderNo
-              })
-            } else {
-              uni.navigateTo({
-                url: '/pages/growth/detail?recordId=' + res.data.id
-              })
-            }
+          setTimeout(function() {
+            uni.navigateTo({
+              url: '/pages/growth/detail?recordId=' + res.data.id
+            })
           }, 500)
         } else {
           uni.showToast({ title: res.message || '创建失败', icon: 'none' })
@@ -228,7 +227,14 @@ export default {
         uni.showToast({ title: e.message || '创建失败', icon: 'none' })
       }
     },
-    goToPurchase() {
+    goUpload() {
+      var url = '/pages/growth/upload'
+      if (this.targetChildId) {
+        url += '?childId=' + this.targetChildId
+      }
+      uni.navigateTo({ url: url })
+    },
+    goPurchase() {
       uni.navigateTo({
         url: '/pages/assessment/purchase'
       })
@@ -258,6 +264,40 @@ export default {
   font-size: 26rpx;
   color: #999;
 }
+.source-type-section {
+  display: flex;
+  gap: 16rpx;
+  margin-bottom: 30rpx;
+}
+.source-card {
+  flex: 1;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx 16rpx;
+  text-align: center;
+  border: 2rpx solid #E0E0E0;
+}
+.source-active {
+  border-color: #667eea;
+  background: #f5f3ff;
+}
+.source-icon {
+  font-size: 48rpx;
+  display: block;
+  margin-bottom: 8rpx;
+}
+.source-name {
+  font-size: 26rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 4rpx;
+}
+.source-desc {
+  font-size: 20rpx;
+  color: #999;
+  display: block;
+}
 .form {
   background: #fff;
   border-radius: 20rpx;
@@ -311,6 +351,15 @@ export default {
   font-size: 32rpx;
   color: #ccc;
 }
+.picker-input {
+  height: 76rpx;
+  line-height: 76rpx;
+  border: 1rpx solid #E0E0E0;
+  border-radius: 10rpx;
+  padding: 0 20rpx;
+  font-size: 28rpx;
+  color: #333;
+}
 .btn-submit {
   width: 100%;
   height: 88rpx;
@@ -321,64 +370,20 @@ export default {
   font-size: 32rpx;
   font-weight: bold;
   text-align: center;
-  margin-top: 10rpx;
 }
-.assessment-section {
-  margin-top: 30rpx;
-}
-.section-divider {
-  height: 1rpx;
-  background: #E0E0E0;
-  margin-bottom: 30rpx;
-}
-.section-header {
-  margin-bottom: 20rpx;
-}
-.section-title {
-  font-size: 30rpx;
-  font-weight: bold;
-  color: #333;
-  display: block;
-}
-.section-hint {
-  font-size: 24rpx;
-  color: #999;
-  margin-top: 8rpx;
-  display: block;
+.redirect-section {
+  margin-top: 20rpx;
 }
-.btn-assessment {
-  width: 100%;
-  height: 88rpx;
-  line-height: 88rpx;
+.redirect-card {
   background: #fff;
-  color: #667eea;
-  border: 2rpx solid #667eea;
-  border-radius: 44rpx;
-  font-size: 30rpx;
-  font-weight: bold;
-  text-align: center;
-}
-.checkbox-item {
-  margin-top: 20rpx;
-  padding: 20rpx;
-  background: #f5f5f5;
-  border-radius: 12rpx;
+  border-radius: 20rpx;
+  padding: 40rpx 30rpx;
 }
-.checkbox-label {
-  display: flex;
-  align-items: center;
+.redirect-desc {
   font-size: 28rpx;
-  color: #333;
-}
-.checkbox-label input[type="checkbox"] {
-  margin-right: 12rpx;
-  width: 36rpx;
-  height: 36rpx;
-}
-.checkbox-hint {
-  font-size: 24rpx;
-  color: #999;
-  margin-top: 8rpx;
+  color: #666;
+  line-height: 1.6;
   display: block;
+  margin-bottom: 30rpx;
 }
-</style>
+</style>

+ 244 - 10
cfc-frontend/pages/growth/detail.vue

@@ -10,11 +10,18 @@
             {{ record.status === 'completed' ? '已完成' : '待同步' }}
           </text>
         </view>
-        <text class="external-badge" v-if="record.externalSync">来自外部系统</text>
+        <view class="header-badges">
+          <text class="source-badge" v-if="record.sourceType">{{ sourceTypeLabel }}</text>
+          <text class="external-badge" v-if="record.externalSync">来自外部系统</text>
+        </view>
       </view>
 
       <view class="info-card">
         <text class="card-title">基本信息</text>
+        <view class="info-row" v-if="record.reportDate">
+          <text class="info-label">报告日期</text>
+          <text class="info-value">{{ record.reportDate }}</text>
+        </view>
         <view class="info-row">
           <text class="info-label">综合得分</text>
           <text class="info-value">{{ record.overallScore != null ? record.overallScore : '暂无' }}</text>
@@ -47,12 +54,66 @@
           <text class="info-label">学校区域</text>
           <text class="info-value">{{ (record.schoolProvince||'') + (record.schoolCity||'') + (record.schoolDistrict||'') + (record.schoolStreet||'') }}</text>
         </view>
+        <view class="info-row" v-if="record.sourceFileUrl">
+          <text class="info-label">报告文件</text>
+          <text class="info-value link" @click="copyFileUrl">复制链接</text>
+        </view>
         <view class="info-row">
           <text class="info-label">创建时间</text>
           <text class="info-value">{{ formatDate(record.createdAt) }}</text>
         </view>
       </view>
 
+      <view class="section-card" v-if="hasScores">
+        <text class="card-title">测评维度</text>
+        <view class="score-grid">
+          <view class="score-item" v-if="record.danLevel">
+            <text class="score-label">DAN等级</text>
+            <text class="score-value">{{ record.danLevel }}</text>
+          </view>
+          <view class="score-item" v-if="record.overallScore">
+            <text class="score-label">综合得分</text>
+            <text class="score-value">{{ record.overallScore }}</text>
+          </view>
+          <view class="score-item" v-if="record.attentionScore">
+            <text class="score-label">注意力</text>
+            <text class="score-value">{{ record.attentionScore }}</text>
+          </view>
+          <view class="score-item" v-if="record.focusScore">
+            <text class="score-label">专注力</text>
+            <text class="score-value">{{ record.focusScore }}</text>
+          </view>
+          <view class="score-item" v-if="record.memoryScore">
+            <text class="score-label">记忆力</text>
+            <text class="score-value">{{ record.memoryScore }}</text>
+          </view>
+          <view class="score-item" v-if="record.logicScore">
+            <text class="score-label">逻辑力</text>
+            <text class="score-value">{{ record.logicScore }}</text>
+          </view>
+          <view class="score-item" v-if="record.perceptionScore">
+            <text class="score-label">感知力</text>
+            <text class="score-value">{{ record.perceptionScore }}</text>
+          </view>
+          <view class="score-item" v-if="record.spatialScore">
+            <text class="score-label">空间力</text>
+            <text class="score-value">{{ record.spatialScore }}</text>
+          </view>
+          <view class="score-item" v-if="record.processingSpeedScore">
+            <text class="score-label">处理速度</text>
+            <text class="score-value">{{ record.processingSpeedScore }}</text>
+          </view>
+          <view class="score-item" v-if="record.emotionScore">
+            <text class="score-label">情绪力</text>
+            <text class="score-value">{{ record.emotionScore }}</text>
+          </view>
+          <view class="score-item" v-if="record.resilienceScore">
+            <text class="score-label">抗挫力</text>
+            <text class="score-value">{{ record.resilienceScore }}</text>
+          </view>
+        </view>
+      </view>
+
       <view class="section-card" v-if="record.assessmentSummary">
         <text class="card-title">测评摘要</text>
         <text class="card-content">{{ record.assessmentSummary }}</text>
@@ -69,7 +130,26 @@
         <text class="card-content">{{ record.taskRecommendations }}</text>
       </view>
 
+      <view class="section-card" v-if="supplements.length > 0">
+        <view class="card-title-row">
+          <text class="card-title">补充材料</text>
+          <text class="supplement-count">{{ supplements.length }}条</text>
+        </view>
+        <view class="supplement-card" v-for="item in supplements" :key="item.id" @click="viewSupplement(item)">
+          <view class="supplement-header">
+            <text class="supplement-date">{{ item.reportDate || formatDate(item.createdAt) }}</text>
+            <text class="supplement-source" v-if="item.sourceType">{{ sourceTypeLabelOf(item.sourceType) }}</text>
+          </view>
+          <view class="supplement-scores" v-if="item.danLevel || item.overallScore">
+            <text class="supplement-score" v-if="item.danLevel">DAN: {{ item.danLevel }}</text>
+            <text class="supplement-score" v-if="item.overallScore">得分: {{ item.overallScore }}</text>
+          </view>
+          <text class="supplement-summary" v-if="item.assessmentSummary">{{ item.assessmentSummary }}</text>
+        </view>
+      </view>
+
       <view class="footer-actions">
+        <button class="btn-supplement" @click="goSupplement" v-if="record.sourceType !== 'supplement'">补充测评材料</button>
         <button class="btn-delete" @click="handleDelete">删除档案</button>
       </view>
     </template>
@@ -77,7 +157,7 @@
 </template>
 
 <script>
-import { getGrowthRecordDetail, deleteGrowthRecord } from '../../utils/api.js'
+import { getGrowthRecordDetail, deleteGrowthRecord, getGrowthSupplements } from '../../utils/api.js'
 
 export default {
   data() {
@@ -85,20 +165,39 @@ export default {
       loading: true,
       record: null,
       recordId: null,
+      supplements: [],
       gradeNames: {1:'一年级',2:'二年级',3:'三年级',4:'四年级',5:'五年级',6:'六年级',7:'初一',8:'初二',9:'初三',10:'高一',11:'高二',12:'高三'}
     }
   },
+  computed: {
+    sourceTypeLabel() {
+      if (!this.record || !this.record.sourceType) return ''
+      return this.sourceTypeLabelOf(this.record.sourceType)
+    },
+    hasScores() {
+      if (!this.record) return false
+      return this.record.danLevel || this.record.attentionScore || this.record.focusScore ||
+        this.record.memoryScore || this.record.logicScore || this.record.perceptionScore ||
+        this.record.spatialScore || this.record.processingSpeedScore || this.record.emotionScore ||
+        this.record.resilienceScore
+    }
+  },
   onLoad(options) {
     this.recordId = options.recordId
     this.loadDetail()
   },
   methods: {
+    sourceTypeLabelOf(type) {
+      var map = { 'manual': '手动创建', 'assessment': '测评录入', 'upload': '报告上传', 'supplement': '补充材料' }
+      return map[type] || type
+    },
     async loadDetail() {
       this.loading = true
       try {
-        const res = await getGrowthRecordDetail(this.recordId)
+        var res = await getGrowthRecordDetail(this.recordId)
         if (res.code === 200) {
           this.record = res.data
+          this.loadSupplements()
         } else {
           uni.showToast({ title: '加载失败', icon: 'none' })
         }
@@ -109,24 +208,55 @@ export default {
         this.loading = false
       }
     },
+    async loadSupplements() {
+      try {
+        var res = await getGrowthSupplements(this.recordId)
+        if (res.code === 200) {
+          this.supplements = res.data || []
+        }
+      } catch (e) {
+        console.error(e)
+      }
+    },
     viewAssessment() {
       uni.navigateTo({
         url: '/pages/growth/assessment-result?recordId=' + this.recordId + '&assessmentId=' + this.record.assessmentId
       })
     },
+    viewSupplement(item) {
+      uni.navigateTo({
+        url: '/pages/growth/detail?recordId=' + item.id
+      })
+    },
+    goSupplement() {
+      var childId = this.record && this.record.childId ? this.record.childId : ''
+      uni.navigateTo({
+        url: '/pages/growth/supplement?recordId=' + this.recordId + '&childId=' + childId
+      })
+    },
+    copyFileUrl() {
+      if (this.record && this.record.sourceFileUrl) {
+        uni.setClipboardData({
+          data: this.record.sourceFileUrl,
+          success: function() {
+            uni.showToast({ title: '链接已复制', icon: 'success' })
+          }
+        })
+      }
+    },
     handleDelete() {
       uni.showModal({
         title: '确认删除',
         content: '删除后无法恢复,确定要删除此成长档案吗?',
-        success: async (res) => {
+        success: async function(res) {
           if (res.confirm) {
             uni.showLoading({ title: '删除中...' })
             try {
-              const result = await deleteGrowthRecord(this.recordId)
+              var result = await deleteGrowthRecord(this.recordId)
               uni.hideLoading()
               if (result.code === 200) {
                 uni.showToast({ title: '删除成功', icon: 'success' })
-                setTimeout(() => {
+                setTimeout(function() {
                   uni.navigateBack()
                 }, 500)
               } else {
@@ -137,7 +267,7 @@ export default {
               uni.showToast({ title: '删除失败', icon: 'none' })
             }
           }
-        }
+        }.bind(this)
       })
     },
     formatDate(date) {
@@ -189,11 +319,21 @@ export default {
   background: rgba(255,152,0,0.3);
   color: #fff;
 }
+.header-badges {
+  margin-top: 12rpx;
+  display: flex;
+  gap: 10rpx;
+}
+.source-badge {
+  font-size: 22rpx;
+  color: rgba(255,255,255,0.8);
+  background: rgba(255,255,255,0.2);
+  padding: 4rpx 12rpx;
+  border-radius: 10rpx;
+}
 .external-badge {
   font-size: 22rpx;
   color: rgba(255,255,255,0.8);
-  margin-top: 10rpx;
-  display: inline-block;
   background: rgba(255,255,255,0.2);
   padding: 4rpx 12rpx;
   border-radius: 10rpx;
@@ -211,6 +351,19 @@ export default {
   display: block;
   margin-bottom: 20rpx;
 }
+.card-title-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20rpx;
+}
+.card-title-row .card-title {
+  margin-bottom: 0;
+}
+.supplement-count {
+  font-size: 24rpx;
+  color: #999;
+}
 .info-row {
   display: flex;
   justify-content: space-between;
@@ -226,6 +379,32 @@ export default {
   color: #333;
   font-weight: bold;
 }
+.info-value.link {
+  color: #667eea;
+}
+.score-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+}
+.score-item {
+  background: #f5f3ff;
+  border-radius: 12rpx;
+  padding: 16rpx 20rpx;
+  min-width: 140rpx;
+  text-align: center;
+}
+.score-label {
+  font-size: 22rpx;
+  color: #999;
+  display: block;
+  margin-bottom: 4rpx;
+}
+.score-value {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #667eea;
+}
 .card-content {
   font-size: 28rpx;
   color: #666;
@@ -243,9 +422,64 @@ export default {
   text-align: center;
   margin-top: 20rpx;
 }
+.supplement-card {
+  background: #f9f9ff;
+  border-radius: 12rpx;
+  padding: 20rpx;
+  margin-bottom: 12rpx;
+}
+.supplement-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8rpx;
+}
+.supplement-date {
+  font-size: 26rpx;
+  color: #333;
+  font-weight: bold;
+}
+.supplement-source {
+  font-size: 22rpx;
+  color: #667eea;
+  background: #f0f0ff;
+  padding: 2rpx 10rpx;
+  border-radius: 8rpx;
+}
+.supplement-scores {
+  display: flex;
+  gap: 16rpx;
+  margin-bottom: 8rpx;
+}
+.supplement-score {
+  font-size: 24rpx;
+  color: #666;
+}
+.supplement-summary {
+  font-size: 24rpx;
+  color: #999;
+  line-height: 1.5;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+}
 .footer-actions {
   padding: 30rpx 0;
 }
+.btn-supplement {
+  width: 100%;
+  height: 80rpx;
+  line-height: 80rpx;
+  background: #fff;
+  color: #667eea;
+  border: 2rpx solid #667eea;
+  border-radius: 40rpx;
+  font-size: 28rpx;
+  text-align: center;
+  margin-bottom: 20rpx;
+}
 .btn-delete {
   width: 100%;
   height: 80rpx;
@@ -257,4 +491,4 @@ export default {
   font-size: 28rpx;
   text-align: center;
 }
-</style>
+</style>

+ 93 - 31
cfc-frontend/pages/growth/index.vue

@@ -26,19 +26,45 @@
       <view class="section-header">
         <text class="section-title">{{ selectedChildName }} 的成长档案</text>
       </view>
-      <view class="record-card" v-for="record in records" :key="record.id" @click="viewDetail(record)">
+
+      <view class="filter-tabs">
+        <view class="filter-tab" :class="activeFilter === 'all' ? 'tab-active' : ''" @click="setFilter('all')">
+          <text class="tab-text" :class="activeFilter === 'all' ? 'tab-active-text' : ''">全部</text>
+        </view>
+        <view class="filter-tab" :class="activeFilter === 'manual' ? 'tab-active' : ''" @click="setFilter('manual')">
+          <text class="tab-text" :class="activeFilter === 'manual' ? 'tab-active-text' : ''">手动</text>
+        </view>
+        <view class="filter-tab" :class="activeFilter === 'assessment' ? 'tab-active' : ''" @click="setFilter('assessment')">
+          <text class="tab-text" :class="activeFilter === 'assessment' ? 'tab-active-text' : ''">测评</text>
+        </view>
+        <view class="filter-tab" :class="activeFilter === 'upload' ? 'tab-active' : ''" @click="setFilter('upload')">
+          <text class="tab-text" :class="activeFilter === 'upload' ? 'tab-active-text' : ''">上传</text>
+        </view>
+        <view class="filter-tab" :class="activeFilter === 'supplement' ? 'tab-active' : ''" @click="setFilter('supplement')">
+          <text class="tab-text" :class="activeFilter === 'supplement' ? 'tab-active-text' : ''">补充</text>
+        </view>
+      </view>
+
+      <view class="record-card" v-for="record in filteredRecords" :key="record.id" @click="viewDetail(record)">
         <view class="record-header">
           <text class="record-title">{{ record.recordTitle || '成长档案' }}</text>
-          <text class="record-tag" :class="record.status === 'completed' ? 'tag-completed' : 'tag-pending'">
-            {{ record.status === 'completed' ? '已完成' : '待同步' }}
-          </text>
+          <view class="record-badges">
+            <text class="record-source-tag" v-if="record.sourceType">{{ sourceTypeLabel(record.sourceType) }}</text>
+            <text class="record-tag" :class="record.status === 'completed' ? 'tag-completed' : 'tag-pending'">
+              {{ record.status === 'completed' ? '已完成' : '待同步' }}
+            </text>
+          </view>
         </view>
-        <view class="record-scores" v-if="record.danLevel || record.overallScore">
+        <view class="record-scores" v-if="record.danLevel || record.overallScore || record.reportDate">
           <text class="score-item" v-if="record.danLevel">DAN: {{ record.danLevel }}</text>
           <text class="score-item" v-if="record.overallScore">得分: {{ record.overallScore }}</text>
-          <text class="score-item" v-if="record.createdAt">{{ formatDate(record.createdAt) }}</text>
+          <text class="score-item" v-if="record.reportDate">{{ record.reportDate }}</text>
+          <text class="score-item" v-if="!record.reportDate && record.createdAt">{{ formatDate(record.createdAt) }}</text>
         </view>
       </view>
+      <view class="empty-state" v-if="filteredRecords.length === 0 && records.length > 0">
+        <text class="empty-text">该类型暂无记录</text>
+      </view>
       <view class="empty-state" v-if="records.length === 0">
         <text class="empty-text">暂无成长档案记录</text>
       </view>
@@ -65,9 +91,17 @@ export default {
       selectedChildName: '',
       records: [],
       childId: '',
+      activeFilter: 'all',
       shareData: null
     }
   },
+  computed: {
+    filteredRecords: function() {
+      if (this.activeFilter === 'all') return this.records
+      var filter = this.activeFilter
+      return this.records.filter(function(r) { return r.sourceType === filter })
+    }
+  },
   onShareAppMessage() {
     if (this.shareData) {
       return {
@@ -82,15 +116,21 @@ export default {
     this.loadChildren()
   },
   methods: {
+    sourceTypeLabel(type) {
+      var map = { 'manual': '手动', 'assessment': '测评', 'upload': '上传', 'supplement': '补充' }
+      return map[type] || type
+    },
+    setFilter(type) {
+      this.activeFilter = type
+    },
     async loadChildren() {
       this.loading = true
       try {
-        const res = await getChildren()
+        var res = await getChildren()
         if (res.code === 200) {
           this.children = res.data || []
-          // 如果指定了 childId,自动选中该孩子并加载档案
           if (this.childId && this.children.length > 0) {
-            const matched = this.children.find(c => String(c.id) === String(this.childId))
+            var matched = this.children.find(function(c) { return String(c.id) === String(this.childId) }.bind(this))
             if (matched) {
               this.viewChildRecords(matched)
             }
@@ -107,7 +147,7 @@ export default {
       this.selectedChildName = child.nickname || child.name
       this.loading = true
       try {
-        const res = await getChildRecords(child.id)
+        var res = await getChildRecords(child.id)
         if (res.code === 200) {
           this.records = res.data || []
         }
@@ -133,15 +173,15 @@ export default {
     },
     async prepareGuideInvite() {
       try {
-        const familyId = uni.getStorageSync('familyId')
+        var familyId = uni.getStorageSync('familyId')
         if (!familyId) {
           uni.showToast({ title: '您暂未加入家庭', icon: 'none' })
           return
         }
-        const userInfo = uni.getStorageSync('userInfo')
-        const { generateInviteCard } = require('../../utils/api.js')
-        const res = await generateInviteCard('guide', familyId)
-        const code = res.data.code
+        var userInfo = uni.getStorageSync('userInfo')
+        var generateInviteCard = require('../../utils/api.js').generateInviteCard
+        var res = await generateInviteCard('guide', familyId)
+        var code = res.data.code
         this.shareData = {
           title: (userInfo && userInfo.nickname ? userInfo.nickname : '家长') + ' 邀请你成为成长规划师',
           path: '/pages/login/login?invite_code=' + code
@@ -199,21 +239,6 @@ export default {
   margin-top: 6rpx;
   display: block;
 }
-.child-meta {
-  text-align: center;
-  margin: 0 30rpx;
-}
-.meta-label {
-  font-size: 22rpx;
-  color: #999;
-  display: block;
-}
-.meta-value {
-  font-size: 28rpx;
-  color: #667eea;
-  font-weight: bold;
-  display: block;
-}
 .arrow {
   width: 40rpx;
 }
@@ -232,6 +257,30 @@ export default {
   font-weight: bold;
   color: #333;
 }
+.filter-tabs {
+  display: flex;
+  gap: 16rpx;
+  margin-bottom: 20rpx;
+}
+.filter-tab {
+  flex: 1;
+  text-align: center;
+  padding: 12rpx 0;
+  border-radius: 20rpx;
+  background: #fff;
+  border: 1rpx solid #E0E0E0;
+}
+.tab-active {
+  background: #667eea;
+  border-color: #667eea;
+}
+.tab-text {
+  font-size: 24rpx;
+  color: #666;
+}
+.tab-active-text {
+  color: #fff;
+}
 .record-card {
   background: #fff;
   border-radius: 16rpx;
@@ -248,6 +297,19 @@ export default {
   font-size: 28rpx;
   color: #333;
   font-weight: bold;
+  flex: 1;
+}
+.record-badges {
+  display: flex;
+  gap: 8rpx;
+  flex-shrink: 0;
+}
+.record-source-tag {
+  font-size: 20rpx;
+  padding: 4rpx 10rpx;
+  border-radius: 8rpx;
+  background: #f0f0ff;
+  color: #667eea;
 }
 .record-tag {
   font-size: 22rpx;
@@ -315,4 +377,4 @@ export default {
   font-weight: bold;
   text-align: center;
 }
-</style>
+</style>

+ 338 - 0
cfc-frontend/pages/growth/supplement.vue

@@ -0,0 +1,338 @@
+<template>
+  <view class="container">
+    <view class="header">
+      <text class="title">补充测评材料</text>
+      <text class="subtitle">为已有成长档案补充额外报告</text>
+    </view>
+
+    <view class="form" v-if="step === 1">
+      <view class="form-item">
+        <text class="label">报告日期</text>
+        <picker mode="date" :value="reportDate" @change="onDateChange">
+          <view class="picker-input">{{ reportDate || '请选择日期' }}</view>
+        </view>
+      </view>
+
+      <view class="form-item">
+        <text class="label">选择报告文件</text>
+        <view class="upload-area" @click="chooseFile">
+          <text class="upload-icon" v-if="!fileName">+</text>
+          <text class="upload-text" v-if="!fileName">点击选择PDF文件</text>
+          <text class="file-name" v-if="fileName">{{ fileName }}</text>
+        </view>
+      </view>
+
+      <view class="form-item">
+        <text class="label">DAN等级(选填)</text>
+        <input class="input" v-model="formData.danLevel" placeholder="A/B/C/D" />
+      </view>
+
+      <view class="form-item">
+        <text class="label">综合得分(选填)</text>
+        <input class="input" v-model="formData.overallScore" type="number" placeholder="0-100" />
+      </view>
+
+      <button class="btn-submit" @click="uploadAndParse" :disabled="!canUpload">上传并解析</button>
+    </view>
+
+    <view class="form" v-if="step === 2">
+      <view class="form-item">
+        <text class="label">解析结果预览</text>
+      </view>
+
+      <view class="preview-card" v-if="parsedData">
+        <view class="preview-row" v-if="parsedData.danLevel">
+          <text class="preview-label">DAN等级</text>
+          <text class="preview-value">{{ parsedData.danLevel }}</text>
+        </view>
+        <view class="preview-row" v-if="parsedData.overallScore">
+          <text class="preview-label">综合得分</text>
+          <text class="preview-value">{{ parsedData.overallScore }}</text>
+        </view>
+        <view class="preview-row" v-if="parsedData.assessmentSummary">
+          <text class="preview-label">测评总结</text>
+          <text class="preview-value summary">{{ parsedData.assessmentSummary }}</text>
+        </view>
+      </view>
+
+      <view class="btn-group">
+        <button class="btn-secondary" @click="reset">重新上传</button>
+        <button class="btn-submit" @click="confirmCreate">确认补充</button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { uploadAndParseReport, createGrowthSupplement, getChildren } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      step: 1,
+      parentRecordId: null,
+      childId: null,
+      reportDate: '',
+      fileName: '',
+      filePath: '',
+      sourceFileUrl: '',
+      parsedData: null,
+      formData: {
+        danLevel: '',
+        overallScore: ''
+      }
+    }
+  },
+  computed: {
+    canUpload() {
+      return this.reportDate && this.filePath
+    }
+  },
+  onLoad(options) {
+    this.parentRecordId = options.recordId || null
+    this.childId = options.childId || null
+    if (!this.parentRecordId) {
+      uni.showToast({ title: '缺少档案ID', icon: 'none' })
+    }
+  },
+  methods: {
+    onDateChange(e) {
+      this.reportDate = e.detail.value
+    },
+    chooseFile() {
+      var self = this
+      uni.chooseMessageFile({
+        count: 1,
+        type: 'file',
+        extension: ['pdf'],
+        success: function(res) {
+          if (res.tempFiles && res.tempFiles.length > 0) {
+            self.fileName = res.tempFiles[0].name
+            self.filePath = res.tempFiles[0].path
+          }
+        },
+        fail: function() {
+          uni.showToast({ title: '请选择PDF文件', icon: 'none' })
+        }
+      })
+    },
+    async uploadAndParse() {
+      if (!this.canUpload) return
+      uni.showLoading({ title: '上传解析中...' })
+      try {
+        var res = await uploadAndParseReport(this.filePath)
+        uni.hideLoading()
+        if (res.code === 200) {
+          this.sourceFileUrl = res.data.sourceFileUrl
+          this.parsedData = res.data.parsed
+          if (this.parsedData && this.parsedData.danLevel && !this.formData.danLevel) {
+            this.formData.danLevel = this.parsedData.danLevel
+          }
+          if (this.parsedData && this.parsedData.overallScore && !this.formData.overallScore) {
+            this.formData.overallScore = String(this.parsedData.overallScore)
+          }
+          this.step = 2
+        } else {
+          uni.showToast({ title: res.message || '解析失败', icon: 'none' })
+        }
+      } catch (e) {
+        uni.hideLoading()
+        uni.showToast({ title: e.message || '上传失败', icon: 'none' })
+      }
+    },
+    async confirmCreate() {
+      uni.showLoading({ title: '创建中...' })
+      try {
+        var submitData = {
+          parentRecordId: this.parentRecordId,
+          childId: this.childId,
+          sourceFileUrl: this.sourceFileUrl,
+          reportDate: this.reportDate
+        }
+        if (this.parsedData) {
+          if (this.parsedData.danLevel) submitData.danLevel = this.parsedData.danLevel
+          if (this.parsedData.overallScore) submitData.overallScore = this.parsedData.overallScore
+          if (this.parsedData.attentionScore) submitData.attentionScore = this.parsedData.attentionScore
+          if (this.parsedData.focusScore) submitData.focusScore = this.parsedData.focusScore
+          if (this.parsedData.memoryScore) submitData.memoryScore = this.parsedData.memoryScore
+          if (this.parsedData.logicScore) submitData.logicScore = this.parsedData.logicScore
+          if (this.parsedData.perceptionScore) submitData.perceptionScore = this.parsedData.perceptionScore
+          if (this.parsedData.spatialScore) submitData.spatialScore = this.parsedData.spatialScore
+          if (this.parsedData.processingSpeedScore) submitData.processingSpeedScore = this.parsedData.processingSpeedScore
+          if (this.parsedData.emotionScore) submitData.emotionScore = this.parsedData.emotionScore
+          if (this.parsedData.resilienceScore) submitData.resilienceScore = this.parsedData.resilienceScore
+          if (this.parsedData.assessmentSummary) submitData.assessmentSummary = this.parsedData.assessmentSummary
+          if (this.parsedData.growthSuggestions) submitData.growthSuggestions = this.parsedData.growthSuggestions
+        } else {
+          if (this.formData.danLevel) submitData.danLevel = this.formData.danLevel
+          if (this.formData.overallScore) submitData.overallScore = parseInt(this.formData.overallScore)
+        }
+        var res = await createGrowthSupplement(submitData)
+        uni.hideLoading()
+        if (res.code === 200) {
+          uni.showToast({ title: '补充成功', icon: 'success' })
+          setTimeout(function() {
+            uni.navigateBack()
+          }, 500)
+        } else {
+          uni.showToast({ title: res.message || '创建失败', icon: 'none' })
+        }
+      } catch (e) {
+        uni.hideLoading()
+        uni.showToast({ title: e.message || '创建失败', icon: 'none' })
+      }
+    },
+    reset() {
+      this.step = 1
+      this.fileName = ''
+      this.filePath = ''
+      this.sourceFileUrl = ''
+      this.parsedData = null
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+  background: #F8F8F8;
+  min-height: 100vh;
+}
+.header {
+  text-align: center;
+  padding: 40rpx 0 30rpx;
+}
+.title {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 10rpx;
+}
+.subtitle {
+  font-size: 26rpx;
+  color: #999;
+}
+.form {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+}
+.form-item {
+  margin-bottom: 30rpx;
+}
+.label {
+  font-size: 28rpx;
+  color: #333;
+  display: block;
+  margin-bottom: 16rpx;
+  font-weight: bold;
+}
+.input {
+  width: 100%;
+  height: 76rpx;
+  border: 1rpx solid #E0E0E0;
+  border-radius: 10rpx;
+  padding: 0 20rpx;
+  font-size: 28rpx;
+  box-sizing: border-box;
+}
+.upload-area {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 200rpx;
+  border: 2rpx dashed #ccc;
+  border-radius: 16rpx;
+  background: #fafafa;
+}
+.upload-icon {
+  font-size: 64rpx;
+  color: #999;
+  line-height: 1;
+}
+.upload-text {
+  font-size: 26rpx;
+  color: #999;
+  margin-top: 10rpx;
+}
+.file-name {
+  font-size: 28rpx;
+  color: #667eea;
+  word-break: break-all;
+  text-align: center;
+}
+.picker-input {
+  height: 76rpx;
+  line-height: 76rpx;
+  border: 1rpx solid #E0E0E0;
+  border-radius: 10rpx;
+  padding: 0 20rpx;
+  font-size: 28rpx;
+  color: #333;
+}
+.btn-submit {
+  width: 100%;
+  height: 88rpx;
+  line-height: 88rpx;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  border-radius: 44rpx;
+  font-size: 32rpx;
+  font-weight: bold;
+  text-align: center;
+}
+.btn-submit[disabled] {
+  opacity: 0.5;
+}
+.preview-card {
+  background: #f9f9ff;
+  border-radius: 12rpx;
+  padding: 24rpx;
+  margin-bottom: 30rpx;
+}
+.preview-row {
+  display: flex;
+  margin-bottom: 16rpx;
+}
+.preview-label {
+  font-size: 26rpx;
+  color: #666;
+  width: 160rpx;
+  flex-shrink: 0;
+}
+.preview-value {
+  font-size: 26rpx;
+  color: #333;
+  flex: 1;
+}
+.preview-value.summary {
+  max-height: 200rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 4;
+  -webkit-box-orient: vertical;
+}
+.btn-group {
+  display: flex;
+  gap: 20rpx;
+}
+.btn-secondary {
+  flex: 1;
+  height: 88rpx;
+  line-height: 88rpx;
+  background: #fff;
+  color: #667eea;
+  border: 2rpx solid #667eea;
+  border-radius: 44rpx;
+  font-size: 30rpx;
+  font-weight: bold;
+  text-align: center;
+}
+.btn-group .btn-submit {
+  flex: 1;
+}
+</style>

+ 380 - 0
cfc-frontend/pages/growth/upload.vue

@@ -0,0 +1,380 @@
+<template>
+  <view class="container">
+    <view class="header">
+      <text class="title">上传测评报告</text>
+      <text class="subtitle">上传PDF测评报告,自动解析数据</text>
+    </view>
+
+    <view class="form" v-if="step === 1">
+      <view class="form-item">
+        <text class="label">孩子</text>
+        <view class="child-select" @click="selectChild">
+          <text class="select-text" v-if="selectedChild">{{ selectedChild.nickname || selectedChild.name }}</text>
+          <text class="select-placeholder" v-else>请选择孩子</text>
+          <text class="select-arrow">›</text>
+        </view>
+      </view>
+
+      <view class="form-item">
+        <text class="label">选择报告文件</text>
+        <view class="upload-area" @click="chooseFile">
+          <text class="upload-icon" v-if="!fileName">+</text>
+          <text class="upload-text" v-if="!fileName">点击选择PDF文件</text>
+          <text class="file-name" v-if="fileName">{{ fileName }}</text>
+        </view>
+      </view>
+
+      <button class="btn-submit" @click="uploadAndParse" :disabled="!canUpload">上传并解析</button>
+    </view>
+
+    <view class="form" v-if="step === 2">
+      <view class="form-item">
+        <text class="label">解析结果预览</text>
+      </view>
+
+      <view class="preview-card" v-if="parsedData">
+        <view class="preview-row" v-if="parsedData.danLevel">
+          <text class="preview-label">DAN等级</text>
+          <text class="preview-value">{{ parsedData.danLevel }}</text>
+        </view>
+        <view class="preview-row" v-if="parsedData.overallScore">
+          <text class="preview-label">综合得分</text>
+          <text class="preview-value">{{ parsedData.overallScore }}</text>
+        </view>
+        <view class="preview-row" v-if="reportDateStr">
+          <text class="preview-label">报告日期</text>
+          <text class="preview-value">{{ reportDateStr }}</text>
+        </view>
+        <view class="preview-row" v-if="parsedData.assessmentSummary">
+          <text class="preview-label">测评总结</text>
+          <text class="preview-value summary">{{ parsedData.assessmentSummary }}</text>
+        </view>
+      </view>
+
+      <view class="form-item" v-if="!parsedData || !parsedData.reportDate">
+        <text class="label">报告日期(手动输入)</text>
+        <picker mode="date" :value="manualDate" @change="onDateChange">
+          <view class="picker-input">{{ manualDate || '请选择日期' }}</view>
+        </picker>
+      </view>
+
+      <view class="btn-group">
+        <button class="btn-secondary" @click="reset">重新上传</button>
+        <button class="btn-submit" @click="confirmCreate">确认创建</button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { uploadAndParseReport, createGrowthRecordFromUpload, checkGrowthDuplicate, getChildren } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      step: 1,
+      children: [],
+      selectedChild: null,
+      fileName: '',
+      filePath: '',
+      sourceFileUrl: '',
+      parsedData: null,
+      reportDateStr: '',
+      manualDate: ''
+    }
+  },
+  computed: {
+    canUpload() {
+      return this.selectedChild && this.filePath
+    }
+  },
+  onLoad(options) {
+    this.loadChildren(options.childId)
+  },
+  methods: {
+    async loadChildren(targetChildId) {
+      try {
+        var res = await getChildren()
+        if (res.code === 200) {
+          this.children = res.data || []
+          var target = null
+          if (targetChildId) {
+            target = this.children.find(function(c) { return String(c.id) === String(targetChildId) })
+          }
+          if (!target && this.children.length > 0) {
+            target = this.children[0]
+          }
+          if (target) {
+            this.selectedChild = target
+          }
+        }
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    selectChild() {
+      var items = this.children.map(function(c) { return c.nickname || c.name })
+      var self = this
+      uni.showActionSheet({
+        itemList: items,
+        success: function(res) {
+          var idx = res.tapIndex
+          if (idx >= 0 && idx < self.children.length) {
+            self.selectedChild = self.children[idx]
+          }
+        }
+      })
+    },
+    chooseFile() {
+      var self = this
+      uni.chooseMessageFile({
+        count: 1,
+        type: 'file',
+        extension: ['pdf'],
+        success: function(res) {
+          if (res.tempFiles && res.tempFiles.length > 0) {
+            self.fileName = res.tempFiles[0].name
+            self.filePath = res.tempFiles[0].path
+          }
+        },
+        fail: function() {
+          uni.showToast({ title: '请选择PDF文件', icon: 'none' })
+        }
+      })
+    },
+    async uploadAndParse() {
+      if (!this.canUpload) return
+      uni.showLoading({ title: '上传解析中...' })
+      try {
+        var res = await uploadAndParseReport(this.filePath)
+        uni.hideLoading()
+        if (res.code === 200) {
+          this.sourceFileUrl = res.data.sourceFileUrl
+          this.parsedData = res.data.parsed
+          this.reportDateStr = res.data.reportDateStr
+          this.step = 2
+        } else {
+          uni.showToast({ title: res.message || '解析失败', icon: 'none' })
+        }
+      } catch (e) {
+        uni.hideLoading()
+        uni.showToast({ title: e.message || '上传失败', icon: 'none' })
+      }
+    },
+    onDateChange(e) {
+      this.manualDate = e.detail.value
+    },
+    async confirmCreate() {
+      var reportDate = this.manualDate || this.reportDateStr
+      if (!reportDate && (!this.parsedData || !this.parsedData.reportDate)) {
+        uni.showToast({ title: '请输入报告日期', icon: 'none' })
+        return
+      }
+      uni.showLoading({ title: '创建中...' })
+      try {
+        var submitData = {
+          childId: this.selectedChild.id,
+          sourceFileUrl: this.sourceFileUrl
+        }
+        if (this.parsedData) {
+          if (this.parsedData.danLevel) submitData.danLevel = this.parsedData.danLevel
+          if (this.parsedData.overallScore) submitData.overallScore = this.parsedData.overallScore
+          if (this.parsedData.attentionScore) submitData.attentionScore = this.parsedData.attentionScore
+          if (this.parsedData.focusScore) submitData.focusScore = this.parsedData.focusScore
+          if (this.parsedData.memoryScore) submitData.memoryScore = this.parsedData.memoryScore
+          if (this.parsedData.logicScore) submitData.logicScore = this.parsedData.logicScore
+          if (this.parsedData.perceptionScore) submitData.perceptionScore = this.parsedData.perceptionScore
+          if (this.parsedData.spatialScore) submitData.spatialScore = this.parsedData.spatialScore
+          if (this.parsedData.processingSpeedScore) submitData.processingSpeedScore = this.parsedData.processingSpeedScore
+          if (this.parsedData.emotionScore) submitData.emotionScore = this.parsedData.emotionScore
+          if (this.parsedData.resilienceScore) submitData.resilienceScore = this.parsedData.resilienceScore
+          if (this.parsedData.assessmentSummary) submitData.assessmentSummary = this.parsedData.assessmentSummary
+          if (this.parsedData.growthSuggestions) submitData.growthSuggestions = this.parsedData.growthSuggestions
+        }
+        if (this.manualDate) {
+          submitData.manualReportDate = this.manualDate
+        }
+        var res = await createGrowthRecordFromUpload(submitData)
+        uni.hideLoading()
+        if (res.code === 200) {
+          uni.showToast({ title: '创建成功', icon: 'success' })
+          setTimeout(function() {
+            uni.navigateTo({ url: '/pages/growth/detail?recordId=' + res.data.id })
+          }, 500)
+        } else {
+          uni.showToast({ title: res.message || '创建失败', icon: 'none' })
+        }
+      } catch (e) {
+        uni.hideLoading()
+        uni.showToast({ title: e.message || '创建失败', icon: 'none' })
+      }
+    },
+    reset() {
+      this.step = 1
+      this.fileName = ''
+      this.filePath = ''
+      this.sourceFileUrl = ''
+      this.parsedData = null
+      this.reportDateStr = ''
+      this.manualDate = ''
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+  background: #F8F8F8;
+  min-height: 100vh;
+}
+.header {
+  text-align: center;
+  padding: 40rpx 0 30rpx;
+}
+.title {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+  margin-bottom: 10rpx;
+}
+.subtitle {
+  font-size: 26rpx;
+  color: #999;
+}
+.form {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx;
+}
+.form-item {
+  margin-bottom: 30rpx;
+}
+.label {
+  font-size: 28rpx;
+  color: #333;
+  display: block;
+  margin-bottom: 16rpx;
+  font-weight: bold;
+}
+.child-select {
+  display: flex;
+  align-items: center;
+  height: 76rpx;
+  border: 1rpx solid #E0E0E0;
+  border-radius: 10rpx;
+  padding: 0 20rpx;
+}
+.select-text {
+  flex: 1;
+  font-size: 28rpx;
+  color: #333;
+}
+.select-placeholder {
+  flex: 1;
+  font-size: 28rpx;
+  color: #ccc;
+}
+.select-arrow {
+  font-size: 32rpx;
+  color: #ccc;
+}
+.upload-area {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 200rpx;
+  border: 2rpx dashed #ccc;
+  border-radius: 16rpx;
+  background: #fafafa;
+}
+.upload-icon {
+  font-size: 64rpx;
+  color: #999;
+  line-height: 1;
+}
+.upload-text {
+  font-size: 26rpx;
+  color: #999;
+  margin-top: 10rpx;
+}
+.file-name {
+  font-size: 28rpx;
+  color: #667eea;
+  word-break: break-all;
+  text-align: center;
+}
+.btn-submit {
+  width: 100%;
+  height: 88rpx;
+  line-height: 88rpx;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #fff;
+  border-radius: 44rpx;
+  font-size: 32rpx;
+  font-weight: bold;
+  text-align: center;
+}
+.btn-submit[disabled] {
+  opacity: 0.5;
+}
+.preview-card {
+  background: #f9f9ff;
+  border-radius: 12rpx;
+  padding: 24rpx;
+  margin-bottom: 30rpx;
+}
+.preview-row {
+  display: flex;
+  margin-bottom: 16rpx;
+}
+.preview-label {
+  font-size: 26rpx;
+  color: #666;
+  width: 160rpx;
+  flex-shrink: 0;
+}
+.preview-value {
+  font-size: 26rpx;
+  color: #333;
+  flex: 1;
+}
+.preview-value.summary {
+  max-height: 200rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 4;
+  -webkit-box-orient: vertical;
+}
+.picker-input {
+  height: 76rpx;
+  line-height: 76rpx;
+  border: 1rpx solid #E0E0E0;
+  border-radius: 10rpx;
+  padding: 0 20rpx;
+  font-size: 28rpx;
+  color: #333;
+}
+.btn-group {
+  display: flex;
+  gap: 20rpx;
+}
+.btn-secondary {
+  flex: 1;
+  height: 88rpx;
+  line-height: 88rpx;
+  background: #fff;
+  color: #667eea;
+  border: 2rpx solid #667eea;
+  border-radius: 44rpx;
+  font-size: 30rpx;
+  font-weight: bold;
+  text-align: center;
+}
+.btn-group .btn-submit {
+  flex: 1;
+}
+</style>